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