Skip to main content

futu_backend/
msg_header.rs

1//! 集中 `MsgHeader` builder (v1.4.110 P0-1)
2//!
3//! 取代 v1.4.110 之前散落 15+ 处直接 `odr_sys_cmn::MsgHeader { ... }` /
4//! `sim_odr_sys_cmn::MsgHeader { ... }` / `trade_cmn::CryptoMsgHeader { ... }`
5//! 构造模式. `req_id` 一律走 [`crate::trade_query::create_backend_req_id`]
6//! 派生, 防 v1.4.109 P0 类 dedupe bug (req_id 固定 → backend 幂等 cache →
7//! 误返 stale order echo). 见
8//! `essentials/2026-05-16-0725-v1.4.109-place-order-msgheader-req-id-not-unique-root-cause-codex-handoff-zh.md`.
9//!
10//! 配套 pre-push Check 6 (`.githooks/pre-push`, v1.4.110 P0-4) 用 grep 拦截
11//! 任何新加的直接构造, 强制走本 module 的 builder.
12//!
13//! # 三个 shape 差异
14//!
15//! - [`build_real`] → `odr_sys_cmn::MsgHeader` (real account, 7 fields,
16//!   含 `sub_account_id`)
17//! - [`build_sim`] → `sim_odr_sys_cmn::MsgHeader` (sim account, 7 fields,
18//!   含 `market` 替代 `sub_account_id`)
19//! - [`build_crypto`] → `trade_cmn::CryptoMsgHeader` (crypto, 仅 3 fields)
20
21#[cfg(test)]
22mod tests;
23
24use crate::proto_internal::{odr_sys_cmn, sim_odr_sys_cmn, trade_cmn};
25use crate::trade_query::create_backend_req_id;
26
27/// 构造 real-account `odr_sys_cmn::MsgHeader`.
28///
29/// # Args
30///
31/// - `acc_id`: 账户 id (填 `account_id` 字段; 同时作为 [`create_backend_req_id`]
32///   的派生种子).
33/// - `cipher`: trade cipher.
34///   - `Some(vec![])` = cache-only query (wire 上 field-present 但为空 bytes).
35///   - 非空 `Some` = 持 unlock token 的写路径 (PlaceOrder / ModifyOrder /
36///     CancelOrder / OrderFillInfoReq w/ security_type).
37///   - `None` = 无 trade 上下文 (如 sim_acc_list / login-time queries).
38/// - `security_type`: PlaceOrder / ModifyOrder / OrderFillInfoReq 等需要;
39///   纯 list / asset 查询填 `None`.
40/// - `input_source`: **仅 PlaceOrder 填** (C++ 行为, see
41///   `crates/futu-gateway-trd/src/handlers/trd/translate.rs` 的
42///   `PLACE_ORDER_MSG_INPUT_SOURCE`); modify / cancel / query 都 `None`.
43pub fn build_real(
44    acc_id: u64,
45    cipher: Option<Vec<u8>>,
46    security_type: Option<u32>,
47    input_source: Option<u32>,
48) -> odr_sys_cmn::MsgHeader {
49    odr_sys_cmn::MsgHeader {
50        req_id: Some(create_backend_req_id(acc_id)),
51        account_id: Some(acc_id),
52        cipher,
53        security_type,
54        exchange_code: None,
55        input_source,
56        sub_account_id: None,
57    }
58}
59
60/// 构造 sim-account `sim_odr_sys_cmn::MsgHeader`.
61///
62/// Sim shape 与 real shape 差异:
63/// - 多 `market` 字段 (sim 账户跨市场必填, see
64///   `futu_domain_trade_account::sim_account_header_market_like_cpp`).
65/// - 无 `sub_account_id`.
66///
67/// # Args
68///
69/// - `market`: C++ raw `NN_TrdMarket` (`Sim_US_Margin=100` 等也可出现)。
70///   `None` 仅用于确认为空 header 的测试/兼容路径。
71pub fn build_sim(
72    acc_id: u64,
73    cipher: Option<Vec<u8>>,
74    market: Option<u32>,
75    security_type: Option<u32>,
76) -> sim_odr_sys_cmn::MsgHeader {
77    sim_odr_sys_cmn::MsgHeader {
78        req_id: Some(create_backend_req_id(acc_id)),
79        account_id: Some(acc_id),
80        cipher,
81        security_type,
82        exchange_code: None,
83        input_source: None,
84        market,
85    }
86}
87
88/// 构造 crypto `trade_cmn::CryptoMsgHeader` (仅 3 字段).
89///
90/// 空 cipher 处理为 `None` 而非 `Some(vec![])` (对齐 v1.4.110 之前
91/// `crypto_trade.rs::build_crypto_msg_header` 原行为, C++ 参照见
92/// `NNProto_Trd_OrderOpCrypto.cpp:40-48`).
93pub fn build_crypto(acc_id: u64, cipher: Vec<u8>) -> trade_cmn::CryptoMsgHeader {
94    trade_cmn::CryptoMsgHeader {
95        req_id: Some(create_backend_req_id(acc_id)),
96        account_id: Some(acc_id),
97        cipher: if cipher.is_empty() {
98            None
99        } else {
100            Some(cipher)
101        },
102    }
103}
104
105/// Build the global account-list heartbeat header.
106///
107/// C++ deliberately writes only `req_id`, seeded with `1` for real trading;
108/// account id and cipher remain absent because the repeated account list owns
109/// the subscription identity.
110/// Ref: `NNProto_Trd_KeepAlive.cpp:84-95`.
111pub fn build_real_trade_account_heartbeat() -> odr_sys_cmn::MsgHeader {
112    odr_sys_cmn::MsgHeader {
113        req_id: Some(create_backend_req_id(1)),
114        account_id: None,
115        cipher: None,
116        security_type: None,
117        exchange_code: None,
118        input_source: None,
119        sub_account_id: None,
120    }
121}