Skip to main content

futu_backend/
market_info_svr.rs

1//! CMD 6825 `NN_ProtoCmd_Qot_Pull_MarketState` — 拉取市场状态 (broker-aware).
2//!
3//! 对齐 C++:
4//! - `NNProtoCenter/Quote/NNBiz_Qot_MarketState.cpp:17-28` `PullMarketState()`:
5//!   构造 `market_tradingDay::MarketInfoReq`, 加 `add_market_id(...)` (按
6//!   request market_id 集合),  **无条件设** `set_is_need_crypto_multi_broker(true)`,
7//!   走 `NN_TCPProtoCategory_Other` 发 CMD 6825.
8//! - `NNProtoCenter/Quote/NNBiz_Qot_MarketState.cpp:113-114` `OnReply_*`:
9//!   遍历 `pbF3C.infos()` (`MarketInfoItem`), 用
10//!   `MarketKey(market_id, has_broker_id ? broker_id : 0)` 作为 `mapIDStatus` key,
11//!   保存 `market_status`. 双维 lookup 时, 非 crypto 市场用 `broker_id=0`.
12//! - `APIServer/Business/Quote/APIServer_Qot_MarketState.cpp:207-237` 回填:
13//!   `i32_t nBrokerID = pbF3CMarketInfo.has_broker_id() ? pbF3CMarketInfo.broker_id() : 0;`
14//!   `mapIDStatus[MarketKey(market_id, nBrokerID)] = market_status;`
15//!
16//! ## v1.4.110 codex QOT Phase 3 Slice 6e
17//!
18//! 之前 Rust GetMarketStateHandler 走 CMD6823 `MarketStatus_Req` 拉全市场
19//! snapshot (不带 broker_id), 与 C++ 行为发散 (C++ 自 10.5.6508 已切 CMD 6825 +
20//! `MarketInfoReq.is_need_crypto_multi_broker=true`). 本模块补这条 caller,
21//! response 解析为按 `(market_id, broker_id_or_0)` 双维的 `MarketStatusItem`.
22//!
23//! ## Hardcoded / Assumption Ledger
24//!
25//! - CMD 6825 (`NN_ProtoCmd_Qot_Pull_MarketState`) 来源:
26//!   `FutuOpenD/Src/NNBase/NNBase_Define_ProtoCmd.h:135`.
27//!   该 cmd 已在 `is_unencrypted_proto` 白名单 (`nn_codec.rs:397`).
28//! - `is_need_crypto_multi_broker = Some(true)` 对齐 C++ **无条件设** (line 24 /
29//!   line 73). 即使本次 request 全部 non-crypto market_id, C++ 仍设, Rust 也设.
30//!   理由: backend 对 non-crypto market 不依赖此 flag, 设为 true 不产生副作用.
31//! - `broker_id` 缺失时默认 0 对齐 C++ `pbF3CMarketInfo.has_broker_id() ?
32//!   pbF3CMarketInfo.broker_id() : 0`. Non-crypto market backend 不下发该字段.
33
34use bytes::Bytes;
35use futu_command_spec::SystemReadOperation;
36use futu_core::error::{FutuError, Result};
37use futu_domain_qot_market_state::ensure_market_info_backend_success;
38use prost::Message;
39
40use crate::conn::BackendConn;
41use crate::proto_internal::market_trading_day::{MarketInfoReq, MarketInfoRsp};
42
43pub use futu_command_spec::CMD_SYSTEM_PULL_MARKET_STATE as CMD_FETCH_MARKET_INFO;
44
45/// `MarketInfoRsp` 解析后的单条市场状态条目.
46///
47/// 对齐 C++ `MarketKey(market_id, broker_id_or_0)`:
48/// - non-crypto market: `broker_id = 0` (`pbF3CMarketInfo.has_broker_id() == false`).
49/// - crypto market: `broker_id` 为具体 broker (1001/1007/1008 etc.), 同一
50///   `market_id` 可能有多条 (不同 broker 不同状态).
51#[derive(Debug, Clone)]
52pub struct MarketStatusItem {
53    /// `market_tradingDay::MarketID` enum (1=HK Main, 10-29=US, 30-40=A 股, etc).
54    pub market_id: i32,
55    /// `0` = no-broker (普通 market); 非 0 = crypto broker (1001/1007/1008 etc).
56    pub broker_id: u32,
57    /// `MarketTradeStatus` enum: 3=Morning, 6=Closed, 13=NightOpen, etc.
58    pub status: u32,
59    /// Backend proto `market_tradingDay::MarketInfoItem` has no `status_text`
60    /// field; keep this as a legacy `MarketStatus` compatibility field and
61    /// always expose an empty string.
62    pub status_text: String,
63}
64
65/// 发 CMD 6825 `MarketInfoReq`, 解析 response 成 `MarketStatusItem` 列表.
66///
67/// 与 C++ `PullMarketState()` 对齐:
68/// 1. `market_id` 字段填 request 集合 (caller 传).
69/// 2. `is_need_crypto_multi_broker = Some(true)` 无条件设 (C++ 行为).
70/// 3. 解 response `infos` 数组, 每条转为 `MarketStatusItem`, `broker_id` 缺失 → 0.
71///
72/// 失败模式:
73/// - 网络错误 / decode 错 → Err (caller 决定 fallback)
74/// - response 中 `code != 0` → Err with `msg` 字段
75pub async fn pull_market_info(
76    backend: &BackendConn,
77    market_ids: &[i32],
78) -> Result<Vec<MarketStatusItem>> {
79    let req = MarketInfoReq {
80        market_id: market_ids.to_vec(),
81        is_contain_ba: None,
82        is_contain_overnight: None,
83        // v1.4.110 codex Slice 6e: 对齐 C++ NNBiz_Qot_MarketState.cpp:24
84        //   `pbReq.set_is_need_crypto_multi_broker(true);` 无条件设 true.
85        is_need_crypto_multi_broker: Some(true),
86    };
87    let body = req.encode_to_vec();
88    tracing::debug!(
89        body_len = body.len(),
90        market_id_count = market_ids.len(),
91        "sending CMD6825 MarketInfoReq (broker-aware market state)"
92    );
93
94    let resp = crate::command_runtime::execute_system_read(
95        backend,
96        SystemReadOperation::MarketState,
97        Bytes::from(body),
98    )
99    .await?;
100    let rsp = MarketInfoRsp::decode(resp.body.as_ref())
101        .map_err(|e| FutuError::Codec(format!("CMD6825 decode: {e}")))?;
102
103    if let Err(err) = ensure_market_info_backend_success(rsp.code) {
104        let code = err.result_code();
105        return Err(FutuError::ServerError {
106            ret_type: code,
107            msg: format!(
108                "CMD6825 code={code} msg={:?}",
109                rsp.msg.as_deref().unwrap_or("")
110            ),
111        });
112    }
113
114    let items: Vec<MarketStatusItem> = rsp
115        .infos
116        .iter()
117        .map(|info| MarketStatusItem {
118            market_id: info.market_id.unwrap_or(0),
119            // C++ `has_broker_id() ? broker_id() : 0`.
120            broker_id: info
121                .broker_id
122                .map(|b| if b < 0 { 0 } else { b as u32 })
123                .unwrap_or(0),
124            status: info.market_status.unwrap_or(0),
125            status_text: String::new(),
126        })
127        .collect();
128
129    tracing::debug!(
130        item_count = items.len(),
131        "v1.4.110 CMD6825 MarketInfoRsp parsed"
132    );
133    Ok(items)
134}
135
136#[cfg(test)]
137mod tests;