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//!   `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_command_spec::BrokerDiscoveryOperation;
27pub use futu_command_spec::CMD_FETCH_MAIN_BROKER;
28use futu_core::{
29    broker_discovery::ensure_broker_discovery_backend_success,
30    error::{FutuError, Result},
31};
32use prost::Message;
33
34use crate::command_runtime::execute_broker_discovery;
35use crate::conn::BackendConn;
36use crate::proto_internal::main_broker_svr::{BrokerInfo, MainBrokerageReq, MainBrokerageRsp};
37
38/// 当前 QOT crypto 行情支持 broker_id 候选集 (C++ 协议常量, broker_market_svr.cpp:50-68).
39/// 与 `qot_security_firm_to_broker_id` 形成闭环: firm 1/2/3 → broker 1001/1007/1008.
40pub const CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES: &[u32] = &[1001, 1007, 1008];
41
42/// 兜底 broker_id (C++ `NNData_Trd_MainBrokerage.cpp:118` 最后 fallback).
43/// 用户暂无 crypto account / main_brokers 缺时使用.
44pub const FALLBACK_DEFAULT_CRYPTO_BROKER: u32 = 1007; // moomoo US
45
46/// `MainBrokerageRsp` 解析后的 snapshot, 用于 `MainBrokerCache`.
47#[derive(Debug, Clone, Default)]
48pub struct MainBrokerSnapshot {
49    /// 主推券商 (按 backend 下发顺序)
50    pub main_brokers: Vec<u32>,
51    /// 主推 + 已开户 (建 broker channel 时用, 9419 主要应用)
52    ///
53    /// v1.4.111: **当前 Rust daemon 不用此字段决定 broker channel**。
54    /// 对齐 FTLogin 10.6 `logger.cpp:1425` / `1496-1575`,通道有效性权威源
55    /// 是 CMD20176 `FetchValidBrokerList`;`auth_code_list` 只提供 HTTP auth 票据,
56    /// CMD20176 失败时才作为 fallback。9419 `connect_brokers` 保留为主推券商
57    /// 语义数据,不把它提升为 channel creation authority。
58    pub connect_brokers: Vec<u32>,
59    /// 数字货币主推券商 (按 backend 下发顺序, QOT default broker 解析关键)
60    pub crypto_brokers: Vec<u32>,
61}
62
63impl MainBrokerSnapshot {
64    /// C++ `INNData_Trd_MainBrokerage::GetCryptoSupportedDefaultMainBroker()` 等价 (line 70-123).
65    ///
66    /// 选择顺序:
67    /// 1. 如果 caller 提供了已开户 crypto account 数 == 1, 直接用该 account 的 broker
68    ///    (caller 责任注入, 本 fn 不查 trd_cache).
69    /// 2. crypto_brokers 第一个支持 crypto 的 main broker.
70    /// 3. main_brokers 第一个支持 crypto 的 main broker.
71    /// 4. 兜底 `FALLBACK_DEFAULT_CRYPTO_BROKER` (1007).
72    pub fn default_crypto_broker(&self, single_crypto_account_broker: Option<u32>) -> u32 {
73        if let Some(b) = single_crypto_account_broker {
74            return b;
75        }
76        for b in &self.crypto_brokers {
77            if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
78                return *b;
79            }
80        }
81        for b in &self.main_brokers {
82            if CRYPTO_SUPPORTED_MAIN_BROKER_CANDIDATES.contains(b) {
83                return *b;
84            }
85        }
86        FALLBACK_DEFAULT_CRYPTO_BROKER
87    }
88}
89
90/// 发 CMD9419, 解析 response 成 `MainBrokerSnapshot`.
91///
92/// 失败模式:
93/// - 网络错误 / decode 错 → Err (caller log + 继续, 不阻塞 daemon 启动)
94/// - `ret_code != 0` → Err with backend err_message
95///
96/// 行为对齐 C++ `NNProto_Trd_MainBrokerage::PullMainBrokerage()` 全 path.
97pub async fn fetch_main_brokers(backend: &BackendConn) -> Result<MainBrokerSnapshot> {
98    let req = MainBrokerageReq { reserved: None };
99    let body = req.encode_to_vec();
100    tracing::debug!(
101        body_len = body.len(),
102        "sending CMD9419 MainBrokerageReq (fetch main brokers + crypto brokers)"
103    );
104
105    let resp = execute_broker_discovery(
106        backend,
107        BrokerDiscoveryOperation::MainBrokerage,
108        body.into(),
109    )
110    .await?;
111
112    let rsp = MainBrokerageRsp::decode(resp.body.as_ref())
113        .map_err(|e| FutuError::Codec(format!("CMD9419 decode: {e}")))?;
114
115    if let Err(err) = ensure_broker_discovery_backend_success(rsp.ret_code) {
116        let ret_code = err.ret_code();
117        return Err(FutuError::ServerError {
118            ret_type: ret_code,
119            msg: format!(
120                "CMD9419 ret_code={ret_code} msg={:?}",
121                rsp.err_message.as_deref().unwrap_or("")
122            ),
123        });
124    }
125
126    let snapshot = MainBrokerSnapshot {
127        main_brokers: brokers_to_ids(&rsp.main_brokers),
128        connect_brokers: brokers_to_ids(&rsp.connect_brokers),
129        crypto_brokers: brokers_to_ids(&rsp.crypto_brokers),
130    };
131
132    tracing::info!(
133        main_brokers = ?snapshot.main_brokers,
134        crypto_brokers = ?snapshot.crypto_brokers,
135        connect_brokers = ?snapshot.connect_brokers,
136        "CMD9419 main brokers received (v1.4.110 audit QOT alignment Slice 2)"
137    );
138    Ok(snapshot)
139}
140
141fn brokers_to_ids(brokers: &[BrokerInfo]) -> Vec<u32> {
142    brokers
143        .iter()
144        .enumerate()
145        .filter_map(|(index, b)| match b.broker_id {
146            Some(id) if id > 0 => Some(id as u32),
147            Some(id) => {
148                tracing::warn!(
149                    index,
150                    broker_id = id,
151                    "CMD9419 broker entry skipped: non-positive broker_id"
152                );
153                None
154            }
155            None => None,
156        })
157        .collect()
158}
159
160#[cfg(test)]
161mod tests;