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(¤cy).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;