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;