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//! `/Users/leaf/ai-lab/o-src/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 futu_core::error::{FutuError, Result};
35use prost::Message;
36
37use crate::conn::BackendConn;
38use crate::proto_internal::market_trading_day::{MarketInfoReq, MarketInfoRsp};
39
40/// CMD 号 — 行情拉取市场状态.
41pub const CMD_FETCH_MARKET_INFO: u16 = 6825;
42
43/// `MarketInfoRsp` 解析后的单条市场状态条目.
44///
45/// 对齐 C++ `MarketKey(market_id, broker_id_or_0)`:
46/// - non-crypto market: `broker_id = 0` (`pbF3CMarketInfo.has_broker_id() == false`).
47/// - crypto market: `broker_id` 为具体 broker (1001/1007/1008 etc.), 同一
48/// `market_id` 可能有多条 (不同 broker 不同状态).
49#[derive(Debug, Clone)]
50pub struct MarketStatusItem {
51 /// `market_tradingDay::MarketID` enum (1=HK Main, 10-29=US, 30-40=A 股, etc).
52 pub market_id: i32,
53 /// `0` = no-broker (普通 market); 非 0 = crypto broker (1001/1007/1008 etc).
54 pub broker_id: u32,
55 /// `MarketTradeStatus` enum: 3=Morning, 6=Closed, 13=NightOpen, etc.
56 pub status: u32,
57 /// 暂未由 backend 下发 (本 proto 字段 `market_tradingDay::MarketInfoItem` 没有 status_text);
58 /// 保留与旧 `MarketStatus` 结构兼容字段, 但永远为空.
59 pub status_text: String,
60}
61
62/// 发 CMD 6825 `MarketInfoReq`, 解析 response 成 `MarketStatusItem` 列表.
63///
64/// 与 C++ `PullMarketState()` 对齐:
65/// 1. `market_id` 字段填 request 集合 (caller 传).
66/// 2. `is_need_crypto_multi_broker = Some(true)` 无条件设 (C++ 行为).
67/// 3. 解 response `infos` 数组, 每条转为 `MarketStatusItem`, `broker_id` 缺失 → 0.
68///
69/// 失败模式:
70/// - 网络错误 / decode 错 → Err (caller 决定 fallback)
71/// - response 中 `code != 0` → Err with `msg` 字段
72pub async fn pull_market_info(
73 backend: &BackendConn,
74 market_ids: &[i32],
75) -> Result<Vec<MarketStatusItem>> {
76 let req = MarketInfoReq {
77 market_id: market_ids.to_vec(),
78 is_contain_ba: None,
79 is_contain_overnight: None,
80 // v1.4.110 codex Slice 6e: 对齐 C++ NNBiz_Qot_MarketState.cpp:24
81 // `pbReq.set_is_need_crypto_multi_broker(true);` 无条件设 true.
82 is_need_crypto_multi_broker: Some(true),
83 };
84 let body = req.encode_to_vec();
85 tracing::debug!(
86 body_len = body.len(),
87 market_id_count = market_ids.len(),
88 "sending CMD6825 MarketInfoReq (broker-aware market state)"
89 );
90
91 let resp = backend.request(CMD_FETCH_MARKET_INFO, body).await?;
92 let rsp = MarketInfoRsp::decode(resp.body.as_ref())
93 .map_err(|e| FutuError::Codec(format!("CMD6825 decode: {e}")))?;
94
95 let code = rsp.code.unwrap_or(-1);
96 if code != 0 {
97 return Err(FutuError::ServerError {
98 ret_type: code,
99 msg: format!(
100 "CMD6825 code={code} msg={:?}",
101 rsp.msg.as_deref().unwrap_or("")
102 ),
103 });
104 }
105
106 let items: Vec<MarketStatusItem> = rsp
107 .infos
108 .iter()
109 .map(|info| MarketStatusItem {
110 market_id: info.market_id.unwrap_or(0),
111 // C++ `has_broker_id() ? broker_id() : 0`.
112 broker_id: info
113 .broker_id
114 .map(|b| if b < 0 { 0 } else { b as u32 })
115 .unwrap_or(0),
116 status: info.market_status.unwrap_or(0),
117 status_text: String::new(),
118 })
119 .collect();
120
121 tracing::debug!(
122 item_count = items.len(),
123 "v1.4.110 CMD6825 MarketInfoRsp parsed"
124 );
125 Ok(items)
126}
127
128#[cfg(test)]
129mod tests;