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