Skip to main content

futu_backend/trade_query/
common.rs

1use super::*;
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use base64::Engine as _;
6use rand::RngCore;
7
8const OPEN_D_KEY_PREFIX: &str = "OD|";
9const REQ_ID_RAW_LEN: usize = 24;
10const REQ_ID_PREFIX: &str = "OD|";
11
12/// v1.4.106 codex F7 (P2): 解析 backend `Order.text` 字段, 对齐 C++
13/// `NNProto_Trd_Order.cpp:18-30` —— OpenD 写时把 text 包成
14/// `OD|<localID>|<userRemark>`, 客户端读时反向拆出 `(local_id, user_remark)`.
15///
16/// **输入**:
17/// - `text=Some("OD|123|hello")` → `Some((123, "hello"))`
18/// - `text=Some("OD|123|")` → `Some((123, ""))` (空 remark, daemon 写侧合法)
19/// - `text=Some("plain")` → `None` (非 OpenD 来源, 老订单 / 其他客户端)
20/// - `text=Some("OD|abc|x")` → `None` (localID 非 u64)
21/// - `text=None` → `None`
22///
23/// **返回 None 时 caller 应**:
24/// 1. `local_id` 字段保持 `None` (无 OpenD 标识)
25/// 2. `remark` 字段填**原始 text** (整段, 不剥前缀) — 用户可能用其他客户端
26///    下单的 free-form remark, 不强制 OpenD 格式.
27pub(super) fn parse_open_d_text(text: Option<&str>) -> Option<(u64, String)> {
28    let text = text?;
29    let body = text.strip_prefix(OPEN_D_KEY_PREFIX)?;
30    let (id_part, remark) = body.split_once('|')?;
31    let local_id: u64 = id_part.parse().ok()?;
32    Some((local_id, remark.to_string()))
33}
34
35/// 解析 optional string 为 f64
36pub(super) fn pf(s: &Option<String>) -> f64 {
37    s.as_ref()
38        .and_then(|v| v.parse::<f64>().ok())
39        .unwrap_or(0.0)
40}
41
42/// 解析 optional string 为 Option<f64>
43pub(super) fn pfo(s: &Option<String>) -> Option<f64> {
44    s.as_ref().and_then(|v| v.parse::<f64>().ok())
45}
46
47/// 四舍五入到 2 位小数 (对齐 C++ Round(value, 2))
48fn round2(v: f64) -> f64 {
49    (v * 100.0).round() / 100.0
50}
51
52/// 根据交易市场返回对应的币种。
53///
54/// v1.4.27 修(BUG-5,加拿大同事 v1.4.26 回归测试发现):原先 fund 子类型
55/// (HK_Fund / US_Fund 等)对应的**后端原始值**和 **NN_TrdMarket 枚举**
56/// 双轨值都没覆盖全,导致 fund 账户 currency fallback 到 HKD,CMD3020
57/// 返 result_code=3 "unknown"。
58///
59/// - 后端 market 原值(来自 `FTUsrTrdAcc.Account.market`):13=HK_Fund /
60///   23=US_Fund / 24=SG_Fund 等
61/// - NN_TrdMarket 枚举值(来自 auth_list):113=HK_Fund / 123=US_Fund /
62///   124=SG_Fund / 125=MY_Fund / 126=JP_Fund 等
63///
64/// `CachedTrdAcc.trd_market` 存的是后端原值(见 `bridge::account_to_cached`),
65/// 所以下面优先覆盖后端原值;NN 值作 tolerance fallback。
66pub(super) fn trd_market_to_currency(trd_market: i32) -> u32 {
67    match trd_market {
68        // AccountMarket / NN_TrdMarket main markets.
69        1 => 1,   // HK → CURRENCY_HKD
70        2 => 2,   // US → CURRENCY_USD
71        3 => 3,   // CN → CURRENCY_CNH
72        4 => 3,   // HKCC → CNH
73        5 => 1,   // Futures fallback → HKD (caller should pass explicit currency)
74        6 => 1,   // Universal fallback → HKD (caller should pass explicit currency)
75        8 => 6,   // AU → AUD
76        15 => 4,  // JP → JPY
77        111 => 8, // MY → MYR
78        112 => 7, // CA → CAD
79
80        // Fund 子市场 —— v1.4.27 新增
81        // 后端原值
82        13 => 1, // HK_Fund (后端) → HKD
83        22 => 2, // US_Fund (后端,旧编码)
84        23 => 2, // US_Fund (后端) → USD
85        24 => 5, // SG_Fund (后端) → SGD
86        // NN 枚举值
87        113 => 1, // NN_TrdMarket_HK_Fund → HKD
88        123 => 2, // NN_TrdMarket_US_Fund → USD
89        124 => 5, // NN_TrdMarket_SG_Fund → SGD
90        125 => 8, // NN_TrdMarket_MY_Fund → MYR
91        126 => 4, // NN_TrdMarket_JP_Fund → JPY
92
93        // 期货 / 期权
94        7 => 1,  // NN_TrdMarket_Futures 默认 HKD,具体按合约国别可能不准
95        12 => 1, // HK_Fund(C++ 老枚举,保留兼容)
96
97        _ => 1, // 默认 HKD(C++ fallback)
98    }
99}
100
101pub(super) fn currency_to_fund_bond_ccy(currency: u32) -> &'static str {
102    match currency {
103        1 => "HKD",
104        2 => "USD",
105        3 => "CNY",
106        4 => "JPY",
107        5 => "SGD",
108        6 => "AUD",
109        7 => "CAD",
110        8 => "MYR",
111        _ => "HKD",
112    }
113}
114
115pub(super) fn api_currency_to_backend(c: i32) -> Option<u32> {
116    // C++ `NN_TrdCurrency_ConvC2S`
117    //   FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:869-901
118    //
119    // Public FTAPI/NN `NN_TrdCurrency` is compact:
120    //   NNBase_Define_Enum.h:404-416 => AUD=6, CAD=7, MYR=8.
121    // Backend CMD3020 uses `odr_sys_cmn::Currency`:
122    //   odr_sys_cmn.proto:147-190 => AUD=8, CAD=16, MYR=41.
123    match c {
124        1 => Some(1),  // HKD
125        2 => Some(2),  // USD
126        3 => Some(3),  // CNH
127        4 => Some(4),  // JPY
128        5 => Some(5),  // SGD
129        6 => Some(8),  // AUD
130        7 => Some(16), // CAD
131        8 => Some(41), // MYR
132        _ => None,
133    }
134}
135
136pub(super) fn backend_currency_to_api(c: u32) -> i32 {
137    // C++ `NN_TrdCurrency_ConvS2C`
138    //   FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:834-867
139    //
140    // Do not return the raw backend enum for unknown values: C++ initializes
141    // the result as `NN_TrdCurrency_Unknown` and only assigns known cases.
142    match c {
143        1 => 1,  // HKD
144        2 => 2,  // USD
145        3 => 3,  // CNH
146        4 => 4,  // JPY
147        5 => 5,  // SGD
148        8 => 6,  // AUD
149        16 => 7, // CAD
150        41 => 8, // MYR
151        _ => 0,
152    }
153}
154
155/// 从 `fund_info_list` 构建 API `market_info_list` 的原生币种展示值。
156///
157/// C++ `FillFunds` 固定输出 8 个市场: HK, US, HKCC, JP, SG, AU, CA, MY
158/// (`APIServer_Trd_GetFunds.cpp:196-224`)。因为 Rust 目前没有单独的
159/// `(acc, currency) -> Vec<MarketFund>` cache,read-side native marketInfo
160/// 继续沿用 backend `fund_info_list` 的分币种 total_asset;证券资产合计另见
161/// `sum_diff_market_fund_assets_in_response_currency`,不可再从这里反推。
162pub(super) fn build_market_info_list(
163    fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
164) -> Vec<CachedMarketInfo> {
165    use std::collections::HashMap;
166    // 建立 currency → total_asset 映射
167    let mut currency_assets: HashMap<i32, f64> = HashMap::new();
168    for fi in fund_info_list {
169        if let Some(c) = fi.currency {
170            let api_currency = backend_currency_to_api(c);
171            let assets = pf(&fi.total_asset);
172            currency_assets.insert(api_currency, assets);
173        }
174    }
175
176    // C++ 8 个预定义市场及对应币种
177    // vecMarket = { HK(1), US(2), HKCC(4), JP(15), SG(6), AU(8), CA(112), MY(111) }
178    let markets_currencies: [(i32, i32); 8] = [
179        (1, 1),   // HK → HKD
180        (2, 2),   // US → USD
181        (4, 3),   // HKCC → CNH
182        (15, 4),  // JP → JPY
183        (6, 5),   // SG → SGD
184        (8, 6),   // AU → AUD
185        (112, 7), // CA → CAD
186        (111, 8), // MY → MYR
187    ];
188
189    markets_currencies
190        .iter()
191        .map(|&(trd_market, currency)| CachedMarketInfo {
192            trd_market,
193            // C++ 数据层存储时有舍入,这里对齐到 2 位小数
194            assets: round2(currency_assets.get(&currency).copied().unwrap_or(0.0)),
195        })
196        .collect()
197}
198
199/// Sum `diff_market_fund_info_list` in the response union currency.
200///
201/// C++ parses each backend `diff_market_fund_info_list` item into
202/// `Ndt_Trd_MarketFund` only when `stock_market` maps through
203/// `StockMarket2NNTrdMarket` (`NNProto_Trd_AccReal.cpp:148-158`), stores that
204/// vector under the response currency (`NNProto_Trd_AccReal.cpp:541-552`), and
205/// later sums all market funds from the requested currency bucket for
206/// `securitiesAssets` (`APIServer_Trd_GetFunds.cpp:196-224`).
207pub(super) fn sum_diff_market_fund_assets_in_response_currency(
208    diff_market_fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
209) -> Option<f64> {
210    if diff_market_fund_info_list.is_empty() {
211        return None;
212    }
213
214    let mut sum = 0.0;
215    let mut saw_valid_market = false;
216    for fund_info in diff_market_fund_info_list {
217        let Some(stock_market) = fund_info.stock_market else {
218            continue;
219        };
220        let trd_market = backend_market_to_trd_market(stock_market);
221        if !matches!(trd_market, 1 | 2 | 4 | 5 | 6 | 8 | 15 | 111 | 112 | 7) {
222            continue;
223        }
224        saw_valid_market = true;
225        sum += pf(&fund_info.total_asset);
226    }
227
228    saw_valid_market.then_some(sum)
229}
230
231/// Raw C++ `HashStrToU64` without Rust's numeric-id compatibility shortcut.
232fn cxx_hash_str_to_u64(s: &str) -> u64 {
233    // BKDR Hash (seed=131)
234    let mut bkdr: u32 = 0;
235    for b in s.bytes() {
236        bkdr = bkdr.wrapping_mul(131).wrapping_add(u32::from(b));
237    }
238    bkdr &= 0x7fff_ffff;
239
240    // AP Hash
241    let mut ap: u32 = 0;
242    for (i, b) in s.bytes().enumerate() {
243        let byte = u32::from(b);
244        ap = if (i & 1) == 0 {
245            ap ^ ((ap << 7) ^ byte ^ (ap >> 3))
246        } else {
247            ap ^ !((ap << 11) ^ byte ^ (ap >> 5))
248        };
249    }
250
251    (u64::from(bkdr) << 32) | u64::from(ap)
252}
253
254/// Create C++-shaped backend `MsgHeader.req_id`.
255///
256/// Ref:
257/// - `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:9-23`
258/// - `FutuOpenD/Src/NNProtoCenter/NNProtoCenter_Inner_Macro_Send.h:16-24`
259/// - `proto-internal/odr_sys_cmn.proto:819-824`
260///
261/// C++ packs 8 bytes of hash plus a 16-byte random unique id, base64-encodes
262/// the 24 bytes into 32 chars, then overwrites the first bytes with `OD|`.
263/// Rust uses local time plus random data in the hash seed because gateway
264/// translators do not carry the login server-time clock; uniqueness and the
265/// OpenD/base64 wire shape are the backend contract here.
266pub fn create_backend_req_id(acc_id: u64) -> String {
267    let mut rng = rand::thread_rng();
268    let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
269        Ok(duration) => duration.as_secs(),
270        Err(_) => 0,
271    };
272    let hash_seed = format!("{}{}{}", timestamp, acc_id, rng.next_u32());
273    let hash = cxx_hash_str_to_u64(&hash_seed);
274
275    let mut raw = [0u8; REQ_ID_RAW_LEN];
276    raw[..8].copy_from_slice(&hash.to_le_bytes());
277    rng.fill_bytes(&mut raw[8..]);
278
279    let mut req_id = base64::engine::general_purpose::STANDARD.encode(raw);
280    req_id.replace_range(0..REQ_ID_PREFIX.len(), REQ_ID_PREFIX);
281    req_id
282}
283
284/// C++ `HashStrToU64` 对齐实现,用于把 backend alphanumeric order/fill id
285/// 投影成 FTAPI `uint64 orderID/fillID`.
286///
287/// Ref:
288/// - `OM/Src/OMBase/API/OMBase_API_StrHash.cpp:52-55`
289/// - `OM/Src/OMBase/Define/OMBase_Define_Macro.h:169`
290///
291/// C++ 语义是 `MakeToU64(APHash(str), BKDRHash(str))`,即 AP hash 放低
292/// 32 位,`BKDR & 0x7fffffff` 放高 32 位。纯数字 id 仍按数字直传以兼容
293/// 旧 backend / 测试数据。
294pub fn hash_str_to_u64(s: &str) -> u64 {
295    if s.is_empty() {
296        return 0;
297    }
298    if let Ok(n) = s.parse::<u64>() {
299        return n;
300    }
301
302    cxx_hash_str_to_u64(s)
303}
304
305/// 后端 stock_market (server值) → FTAPI TrdSecMarket
306/// C++ 流程: server值 → NN_TrdMarket_ConvS2C → GetTrdSecMarket
307pub(super) fn backend_stock_market_to_sec_market(svr_market: u32) -> i32 {
308    // 先做 NN_TrdMarket_ConvS2C (server→client) 转换
309    let client_market = match svr_market {
310        11 => 111,      // NN_TrdMarket_MY
311        12 => 112,      // NN_TrdMarket_CA
312        13 => 113,      // NN_TrdMarket_HK_Fund
313        14 => 114,      // NN_TrdMarket_Fund
314        15 => 15,       // NN_TrdMarket_JP (不变)
315        23 => 123,      // NN_TrdMarket_US_Fund
316        24 => 124,      // NN_TrdMarket_SG_Fund
317        other => other, // 其他直传
318    };
319
320    // 再做 GetTrdSecMarket (client→API SecMarket)
321    // API TrdSecMarket: HK=1, US=2, CN_SH=31, CN_SZ=32, SG=41, JP=51, AU=61, MY=71, CA=81
322    match client_market {
323        1 | 9 | 113 => 1,       // HK, Sim_HK_Option, HK_Fund → TrdSecMarket_HK
324        2 | 7 | 100 | 123 => 2, // US, Sim_US_Option, Sim_US_Margin, US_Fund → TrdSecMarket_US
325        3 | 4 => 31,            // CN, HKCC → TrdSecMarket_CN_SH (默认)
326        6 | 124 => 41,          // SG, SG_Fund → TrdSecMarket_SG
327        8 => 61,                // AU → TrdSecMarket_AU
328        15 => 51,               // JP → TrdSecMarket_JP
329        111 => 71,              // MY → TrdSecMarket_MY
330        112 => 81,              // CA → TrdSecMarket_CA
331        _ => 1,                 // 默认 HK
332    }
333}
334
335/// 后端 stock_market (server值) → FTAPI TrdMarket
336/// C++ 流程: server值 → NN_TrdMarket_ConvS2C → TrdMarket_NNToAPI
337/// NN_TrdMarket 和 API TrdMarket 使用相同的枚举值,TrdMarket_NNToAPI 大部分直传
338pub(super) fn backend_market_to_trd_market(svr_market: u32) -> i32 {
339    // NN_TrdMarket_ConvS2C (Real环境)
340    let client_market = match svr_market {
341        11 => 111,      // MY
342        12 => 112,      // CA
343        13 => 113,      // HK_Fund
344        14 => 114,      // Fund
345        15 => 15,       // JP (不变)
346        23 => 123,      // US_Fund
347        24 => 124,      // SG_Fund
348        other => other, // 其他直传
349    };
350    // TrdMarket_NNToAPI: NN_TrdMarket 和 API TrdMarket 值相同
351    client_market as i32
352}
353
354#[cfg(test)]
355mod tests;