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 backend_currency_to_api(c: u32) -> i32 {
116    // C++ NN_TrdCurrency_ConvS2C: 后端值和客户端值相同
117    c as i32
118}
119
120/// 从 `fund_info_list` 构建 API `market_info_list` 的原生币种展示值。
121///
122/// C++ `FillFunds` 固定输出 8 个市场: HK, US, HKCC, JP, SG, AU, CA, MY
123/// (`APIServer_Trd_GetFunds.cpp:196-224`)。因为 Rust 目前没有单独的
124/// `(acc, currency) -> Vec<MarketFund>` cache,read-side native marketInfo
125/// 继续沿用 backend `fund_info_list` 的分币种 total_asset;证券资产合计另见
126/// `sum_diff_market_fund_assets_in_response_currency`,不可再从这里反推。
127pub(super) fn build_market_info_list(
128    fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
129) -> Vec<CachedMarketInfo> {
130    use std::collections::HashMap;
131    // 建立 currency → total_asset 映射
132    let mut currency_assets: HashMap<i32, f64> = HashMap::new();
133    for fi in fund_info_list {
134        if let Some(c) = fi.currency {
135            let api_currency = backend_currency_to_api(c);
136            let assets = pf(&fi.total_asset);
137            currency_assets.insert(api_currency, assets);
138        }
139    }
140
141    // C++ 8 个预定义市场及对应币种
142    // vecMarket = { HK(1), US(2), HKCC(4), JP(15), SG(6), AU(8), CA(112), MY(111) }
143    let markets_currencies: [(i32, i32); 8] = [
144        (1, 1),   // HK → HKD
145        (2, 2),   // US → USD
146        (4, 3),   // HKCC → CNH
147        (15, 4),  // JP → JPY
148        (6, 5),   // SG → SGD
149        (8, 6),   // AU → AUD
150        (112, 7), // CA → CAD
151        (111, 8), // MY → MYR
152    ];
153
154    markets_currencies
155        .iter()
156        .map(|&(trd_market, currency)| CachedMarketInfo {
157            trd_market,
158            // C++ 数据层存储时有舍入,这里对齐到 2 位小数
159            assets: round2(currency_assets.get(&currency).copied().unwrap_or(0.0)),
160        })
161        .collect()
162}
163
164/// Sum `diff_market_fund_info_list` in the response union currency.
165///
166/// C++ parses each backend `diff_market_fund_info_list` item into
167/// `Ndt_Trd_MarketFund` only when `stock_market` maps through
168/// `StockMarket2NNTrdMarket` (`NNProto_Trd_AccReal.cpp:148-158`), stores that
169/// vector under the response currency (`NNProto_Trd_AccReal.cpp:541-552`), and
170/// later sums all market funds from the requested currency bucket for
171/// `securitiesAssets` (`APIServer_Trd_GetFunds.cpp:196-224`).
172pub(super) fn sum_diff_market_fund_assets_in_response_currency(
173    diff_market_fund_info_list: &[crate::proto_internal::asset_query::AccFundInfo],
174) -> Option<f64> {
175    if diff_market_fund_info_list.is_empty() {
176        return None;
177    }
178
179    let mut sum = 0.0;
180    let mut saw_valid_market = false;
181    for fund_info in diff_market_fund_info_list {
182        let Some(stock_market) = fund_info.stock_market else {
183            continue;
184        };
185        let trd_market = backend_market_to_trd_market(stock_market);
186        if !matches!(trd_market, 1 | 2 | 4 | 5 | 6 | 8 | 15 | 111 | 112 | 7) {
187            continue;
188        }
189        saw_valid_market = true;
190        sum += pf(&fund_info.total_asset);
191    }
192
193    saw_valid_market.then_some(sum)
194}
195
196/// Raw C++ `HashStrToU64` without Rust's numeric-id compatibility shortcut.
197fn cxx_hash_str_to_u64(s: &str) -> u64 {
198    // BKDR Hash (seed=131)
199    let mut bkdr: u32 = 0;
200    for b in s.bytes() {
201        bkdr = bkdr.wrapping_mul(131).wrapping_add(u32::from(b));
202    }
203    bkdr &= 0x7fff_ffff;
204
205    // AP Hash
206    let mut ap: u32 = 0;
207    for (i, b) in s.bytes().enumerate() {
208        let byte = u32::from(b);
209        ap = if (i & 1) == 0 {
210            ap ^ ((ap << 7) ^ byte ^ (ap >> 3))
211        } else {
212            ap ^ !((ap << 11) ^ byte ^ (ap >> 5))
213        };
214    }
215
216    (u64::from(bkdr) << 32) | u64::from(ap)
217}
218
219/// Create C++-shaped backend `MsgHeader.req_id`.
220///
221/// Ref:
222/// - `/Users/leaf/ai-lab/o-src/FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:9-23`
223/// - `/Users/leaf/ai-lab/o-src/FutuOpenD/Src/NNProtoCenter/NNProtoCenter_Inner_Macro_Send.h:16-24`
224/// - `proto-internal/odr_sys_cmn.proto:819-824`
225///
226/// C++ packs 8 bytes of hash plus a 16-byte random unique id, base64-encodes
227/// the 24 bytes into 32 chars, then overwrites the first bytes with `OD|`.
228/// Rust uses local time plus random data in the hash seed because gateway
229/// translators do not carry the login server-time clock; uniqueness and the
230/// OpenD/base64 wire shape are the backend contract here.
231pub fn create_backend_req_id(acc_id: u64) -> String {
232    let mut rng = rand::thread_rng();
233    let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
234        Ok(duration) => duration.as_secs(),
235        Err(_) => 0,
236    };
237    let hash_seed = format!("{}{}{}", timestamp, acc_id, rng.next_u32());
238    let hash = cxx_hash_str_to_u64(&hash_seed);
239
240    let mut raw = [0u8; REQ_ID_RAW_LEN];
241    raw[..8].copy_from_slice(&hash.to_le_bytes());
242    rng.fill_bytes(&mut raw[8..]);
243
244    let mut req_id = base64::engine::general_purpose::STANDARD.encode(raw);
245    req_id.replace_range(0..REQ_ID_PREFIX.len(), REQ_ID_PREFIX);
246    req_id
247}
248
249/// C++ `HashStrToU64` 对齐实现,用于把 backend alphanumeric order/fill id
250/// 投影成 FTAPI `uint64 orderID/fillID`.
251///
252/// Ref:
253/// - `/Users/leaf/ai-lab/o-src/OM/Src/OMBase/API/OMBase_API_StrHash.cpp:52-55`
254/// - `/Users/leaf/ai-lab/o-src/OM/Src/OMBase/Define/OMBase_Define_Macro.h:169`
255///
256/// C++ 语义是 `MakeToU64(APHash(str), BKDRHash(str))`,即 AP hash 放低
257/// 32 位,`BKDR & 0x7fffffff` 放高 32 位。纯数字 id 仍按数字直传以兼容
258/// 旧 backend / 测试数据。
259pub fn hash_str_to_u64(s: &str) -> u64 {
260    if s.is_empty() {
261        return 0;
262    }
263    if let Ok(n) = s.parse::<u64>() {
264        return n;
265    }
266
267    cxx_hash_str_to_u64(s)
268}
269
270/// 后端 stock_market (server值) → FTAPI TrdSecMarket
271/// C++ 流程: server值 → NN_TrdMarket_ConvS2C → GetTrdSecMarket
272pub(super) fn backend_stock_market_to_sec_market(svr_market: u32) -> i32 {
273    // 先做 NN_TrdMarket_ConvS2C (server→client) 转换
274    let client_market = match svr_market {
275        11 => 111,      // NN_TrdMarket_MY
276        12 => 112,      // NN_TrdMarket_CA
277        13 => 113,      // NN_TrdMarket_HK_Fund
278        14 => 114,      // NN_TrdMarket_Fund
279        15 => 15,       // NN_TrdMarket_JP (不变)
280        23 => 123,      // NN_TrdMarket_US_Fund
281        24 => 124,      // NN_TrdMarket_SG_Fund
282        other => other, // 其他直传
283    };
284
285    // 再做 GetTrdSecMarket (client→API SecMarket)
286    // API TrdSecMarket: HK=1, US=2, CN_SH=31, CN_SZ=32, SG=41, JP=51, AU=61, MY=71, CA=81
287    match client_market {
288        1 | 9 | 113 => 1,       // HK, Sim_HK_Option, HK_Fund → TrdSecMarket_HK
289        2 | 7 | 100 | 123 => 2, // US, Sim_US_Option, Sim_US_Margin, US_Fund → TrdSecMarket_US
290        3 | 4 => 31,            // CN, HKCC → TrdSecMarket_CN_SH (默认)
291        6 | 124 => 41,          // SG, SG_Fund → TrdSecMarket_SG
292        8 => 61,                // AU → TrdSecMarket_AU
293        15 => 51,               // JP → TrdSecMarket_JP
294        111 => 71,              // MY → TrdSecMarket_MY
295        112 => 81,              // CA → TrdSecMarket_CA
296        _ => 1,                 // 默认 HK
297    }
298}
299
300/// 后端 stock_market (server值) → FTAPI TrdMarket
301/// C++ 流程: server值 → NN_TrdMarket_ConvS2C → TrdMarket_NNToAPI
302/// NN_TrdMarket 和 API TrdMarket 使用相同的枚举值,TrdMarket_NNToAPI 大部分直传
303pub(super) fn backend_market_to_trd_market(svr_market: u32) -> i32 {
304    // NN_TrdMarket_ConvS2C (Real环境)
305    let client_market = match svr_market {
306        11 => 111,      // MY
307        12 => 112,      // CA
308        13 => 113,      // HK_Fund
309        14 => 114,      // Fund
310        15 => 15,       // JP (不变)
311        23 => 123,      // US_Fund
312        24 => 124,      // SG_Fund
313        other => other, // 其他直传
314    };
315    // TrdMarket_NNToAPI: NN_TrdMarket 和 API TrdMarket 值相同
316    client_market as i32
317}
318
319#[cfg(test)]
320mod tests;