Skip to main content

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//!   `/Users/leaf/ai-lab/o-src/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.110 codex audit P3 #9: **当前 Rust daemon 不读此字段** — broker channel
51    /// 建连仍走 `auth_code_list` (HTTP auth 拿到的) + CMD20176 (sanity check),
52    /// 见 `futu-backend::valid_brokers` 模块文档. 保留此字段作 future fallback
53    /// 数据源 (若 auth_code_list 滞后/失效, 可切到 9419 connect_brokers 作权威).
54    /// codex 调研 12:02 增量 §B 列了 3 个数据源的语义差异.
55    #[allow(dead_code)]
56    pub connect_brokers: Vec<u32>,
57    /// 数字货币主推券商 (按 backend 下发顺序, QOT default broker 解析关键)
58    pub crypto_brokers: Vec<u32>,
59}
60
61impl MainBrokerSnapshot {
62    /// C++ `INNData_Trd_MainBrokerage::GetCryptoSupportedDefaultMainBroker()` 等价 (line 70-123).
63    ///
64    /// 选择顺序:
65    /// 1. 如果 caller 提供了已开户 crypto account 数 == 1, 直接用该 account 的 broker
66    ///    (caller 责任注入, 本 fn 不查 trd_cache).
67    /// 2. crypto_brokers 第一个支持 crypto 的 main broker.
68    /// 3. main_brokers 第一个支持 crypto 的 main broker.
69    /// 4. 兜底 `FALLBACK_DEFAULT_CRYPTO_BROKER` (1007).
70    pub fn default_crypto_broker(&self, single_crypto_account_broker: Option<u32>) -> u32 {
71        if let Some(b) = single_crypto_account_broker {
72            return b;
73        }
74        for b in &self.crypto_brokers {
75            if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
76                return *b;
77            }
78        }
79        for b in &self.main_brokers {
80            if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
81                return *b;
82            }
83        }
84        FALLBACK_DEFAULT_CRYPTO_BROKER
85    }
86}
87
88/// 发 CMD9419, 解析 response 成 `MainBrokerSnapshot`.
89///
90/// 失败模式:
91/// - 网络错误 / decode 错 → Err (caller log + 继续, 不阻塞 daemon 启动)
92/// - `ret_code != 0` → Err with backend err_message
93///
94/// 行为对齐 C++ `NNProto_Trd_MainBrokerage::PullMainBrokerage()` 全 path.
95pub async fn fetch_main_brokers(backend: &BackendConn) -> Result<MainBrokerSnapshot> {
96    let req = MainBrokerageReq { reserved: None };
97    let body = req.encode_to_vec();
98    tracing::debug!(
99        body_len = body.len(),
100        "sending CMD9419 MainBrokerageReq (fetch main brokers + crypto brokers)"
101    );
102
103    let resp = backend.request(CMD_FETCH_MAIN_BROKER, body).await?;
104
105    let rsp = MainBrokerageRsp::decode(resp.body.as_ref())
106        .map_err(|e| FutuError::Codec(format!("CMD9419 decode: {e}")))?;
107
108    let ret_code = rsp.ret_code.unwrap_or(-1);
109    if ret_code != 0 {
110        return Err(FutuError::ServerError {
111            ret_type: ret_code,
112            msg: format!(
113                "CMD9419 ret_code={ret_code} msg={:?}",
114                rsp.err_message.as_deref().unwrap_or("")
115            ),
116        });
117    }
118
119    let snapshot = MainBrokerSnapshot {
120        main_brokers: brokers_to_ids(&rsp.main_brokers),
121        connect_brokers: brokers_to_ids(&rsp.connect_brokers),
122        crypto_brokers: brokers_to_ids(&rsp.crypto_brokers),
123    };
124
125    tracing::info!(
126        main_brokers = ?snapshot.main_brokers,
127        crypto_brokers = ?snapshot.crypto_brokers,
128        connect_brokers = ?snapshot.connect_brokers,
129        "CMD9419 main brokers received (v1.4.110 codex QOT alignment Slice 2)"
130    );
131    Ok(snapshot)
132}
133
134fn brokers_to_ids(brokers: &[BrokerInfo]) -> Vec<u32> {
135    brokers
136        .iter()
137        .filter_map(|b| b.broker_id.map(|id| id as u32))
138        .collect()
139}
140
141#[cfg(test)]
142mod tests;