futu_backend/main_broker_svr.rs
1//! CMD 9419 `kCmdFetchMainBroker` — 拉主推券商 + 数字货币主推券商.
2//!
3//! 对齐 C++ `FutuOpenD/Src/FTGateway/GTWCmdAndPushReply.cpp:928-930` +
4//! `NNProtoCenter/Trade/NNProto_Trd_MainBrokerage.cpp:34-73`:
5//!
6//! - C++ FTGateway 平台 TCP login 成功后调
7//! `INNProto_Trd_MainBrokerage::PullMainBrokerage()` 发 CMD9419.
8//! - response `MainBrokerageRsp.main_brokers` + `crypto_brokers` 写入
9//! `INNData_Trd_MainBrokerage::SetMainBrokers` / `SetCryptoMainBrokers`.
10//! - QOT `securityFirm=Unknown` 时调 `GetCryptoSupportedDefaultMainBroker()`
11//! 从 `crypto_brokers` / `main_brokers` 顺序选 default broker.
12//!
13//! v1.4.110 codex QOT C++ alignment Slice 2: Rust 之前**没有** 9419 caller,
14//! 所以 `securityFirm=Unknown` 无法对齐 C++ default broker 行为. 本模块补这条.
15//!
16//! ## Hardcoded / Assumption Ledger
17//!
18//! - CMD9419 `NN_ProtoCmd_Trd_BaseMainBroker = 9419` 来源:
19//! `FutuOpenD/Src/NNBase/NNBase_Define_ProtoCmd.h:83`.
20//! 该 cmd 是 trade-side broker discovery (encrypted by default), 不进
21//! `is_unencrypted_proto` 白名单.
22//! - QOT crypto-supported broker 候选硬编码集 {1001 FUTU_HK, 1007 FUTU_US,
23//! 1008 FUTU_SG}, 来源 `NNData_Trd_MainBrokerage.cpp:50-68`. 这是 C++ 自己
24//! 的硬编码 (协议常量), 不是服务端动态下发, 因此 Rust 复刻可接受.
25
26use futu_core::error::{FutuError, Result};
27use prost::Message;
28
29use crate::conn::BackendConn;
30use crate::proto_internal::main_broker_svr::{BrokerInfo, MainBrokerageReq, MainBrokerageRsp};
31
32/// CMD 号 — 主推券商
33pub const CMD_FETCH_MAIN_BROKER: u16 = 9419;
34
35/// 当前 QOT crypto 行情支持 broker_id 候选集 (C++ 协议常量, broker_market_svr.cpp:50-68).
36/// 与 `qot_security_firm_to_broker_id` 形成闭环: firm 1/2/3 → broker 1001/1007/1008.
37pub const CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES: &[u32] = &[1001, 1007, 1008];
38
39/// 兜底 broker_id (C++ `NNData_Trd_MainBrokerage.cpp:118` 最后 fallback).
40/// 用户暂无 crypto account / main_brokers 缺时使用.
41pub const FALLBACK_DEFAULT_CRYPTO_BROKER: u32 = 1007; // moomoo US
42
43/// `MainBrokerageRsp` 解析后的 snapshot, 用于 `MainBrokerCache`.
44#[derive(Debug, Clone, Default)]
45pub struct MainBrokerSnapshot {
46 /// 主推券商 (按 backend 下发顺序)
47 pub main_brokers: Vec<u32>,
48 /// 主推 + 已开户 (建 broker channel 时用, 9419 主要应用)
49 ///
50 /// v1.4.111: **当前 Rust daemon 不用此字段决定 broker channel**。
51 /// 对齐 FTLogin 10.6 `logger.cpp:1425` / `1496-1575`,通道有效性权威源
52 /// 是 CMD20176 `FetchValidBrokerList`;`auth_code_list` 只提供 HTTP auth 票据,
53 /// CMD20176 失败时才作为 fallback。9419 `connect_brokers` 保留为主推券商
54 /// 语义数据,不把它提升为 channel creation authority。
55 pub connect_brokers: Vec<u32>,
56 /// 数字货币主推券商 (按 backend 下发顺序, QOT default broker 解析关键)
57 pub crypto_brokers: Vec<u32>,
58}
59
60impl MainBrokerSnapshot {
61 /// C++ `INNData_Trd_MainBrokerage::GetCryptoSupportedDefaultMainBroker()` 等价 (line 70-123).
62 ///
63 /// 选择顺序:
64 /// 1. 如果 caller 提供了已开户 crypto account 数 == 1, 直接用该 account 的 broker
65 /// (caller 责任注入, 本 fn 不查 trd_cache).
66 /// 2. crypto_brokers 第一个支持 crypto 的 main broker.
67 /// 3. main_brokers 第一个支持 crypto 的 main broker.
68 /// 4. 兜底 `FALLBACK_DEFAULT_CRYPTO_BROKER` (1007).
69 pub fn default_crypto_broker(&self, single_crypto_account_broker: Option<u32>) -> u32 {
70 if let Some(b) = single_crypto_account_broker {
71 return b;
72 }
73 for b in &self.crypto_brokers {
74 if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
75 return *b;
76 }
77 }
78 for b in &self.main_brokers {
79 if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
80 return *b;
81 }
82 }
83 FALLBACK_DEFAULT_CRYPTO_BROKER
84 }
85}
86
87/// 发 CMD9419, 解析 response 成 `MainBrokerSnapshot`.
88///
89/// 失败模式:
90/// - 网络错误 / decode 错 → Err (caller log + 继续, 不阻塞 daemon 启动)
91/// - `ret_code != 0` → Err with backend err_message
92///
93/// 行为对齐 C++ `NNProto_Trd_MainBrokerage::PullMainBrokerage()` 全 path.
94pub async fn fetch_main_brokers(backend: &BackendConn) -> Result<MainBrokerSnapshot> {
95 let req = MainBrokerageReq { reserved: None };
96 let body = req.encode_to_vec();
97 tracing::debug!(
98 body_len = body.len(),
99 "sending CMD9419 MainBrokerageReq (fetch main brokers + crypto brokers)"
100 );
101
102 let resp = backend.request(CMD_FETCH_MAIN_BROKER, body).await?;
103
104 let rsp = MainBrokerageRsp::decode(resp.body.as_ref())
105 .map_err(|e| FutuError::Codec(format!("CMD9419 decode: {e}")))?;
106
107 let ret_code = rsp.ret_code.unwrap_or(-1);
108 if ret_code != 0 {
109 return Err(FutuError::ServerError {
110 ret_type: ret_code,
111 msg: format!(
112 "CMD9419 ret_code={ret_code} msg={:?}",
113 rsp.err_message.as_deref().unwrap_or("")
114 ),
115 });
116 }
117
118 let snapshot = MainBrokerSnapshot {
119 main_brokers: brokers_to_ids(&rsp.main_brokers),
120 connect_brokers: brokers_to_ids(&rsp.connect_brokers),
121 crypto_brokers: brokers_to_ids(&rsp.crypto_brokers),
122 };
123
124 tracing::info!(
125 main_brokers = ?snapshot.main_brokers,
126 crypto_brokers = ?snapshot.crypto_brokers,
127 connect_brokers = ?snapshot.connect_brokers,
128 "CMD9419 main brokers received (v1.4.110 audit QOT alignment Slice 2)"
129 );
130 Ok(snapshot)
131}
132
133fn brokers_to_ids(brokers: &[BrokerInfo]) -> Vec<u32> {
134 brokers
135 .iter()
136 .enumerate()
137 .filter_map(|(index, b)| match b.broker_id {
138 Some(id) if id > 0 => Some(id as u32),
139 Some(id) => {
140 tracing::warn!(
141 index,
142 broker_id = id,
143 "CMD9419 broker entry skipped: non-positive broker_id"
144 );
145 None
146 }
147 None => None,
148 })
149 .collect()
150}
151
152#[cfg(test)]
153mod tests;