Skip to main content

futu_backend/trade_query/orders/
helpers.rs

1//! trade_query/orders/helpers — validation / decimal / market derivation / cmd dispatch (12 fn)
2//! (v1.4.110 CC Batch J: 拆自 orders.rs L70-204)
3
4use futu_cache::trd_cache::TrdCache;
5
6use super::super::*;
7use crate::proto_internal::odr_sys_cmn;
8
9pub fn is_valid_trd_market(trd_env: i32, market: i32) -> bool {
10    if trd_env == 0 {
11        // Simulate
12        return true;
13    }
14    matches!(market, 1 | 2 | 4 | 5 | 6 | 8 | 15 | 111 | 112)
15}
16
17/// `backend_deal_status_to_ftapi` — 对齐 C++ `NN_DealStatus`
18/// (`NNBase_Define_Enum.h:377-381`) + C++ `NNProto_Trd_Deal.cpp:68-91` 转换:
19/// `is_cancelled=true → 1 (Cancelled)`, `is_corrected=true → 2 (Changed)`,
20/// otherwise → `0 (OK)`.
21///
22/// **v1.4.106 codex 0955 F6**: 之前 `query_order_fills` 用错值 (1/2/3 代
23/// 0/1/2), 与 active read path (`GetOrderFillListHandler`) 不一致. 现在抽
24/// 共享 helper 让 active 读 + push refresh + cache 全部用同一映射.
25pub fn backend_deal_status_to_ftapi(is_cancelled: bool, is_corrected: bool) -> i32 {
26    if is_cancelled {
27        1 // Cancelled
28    } else if is_corrected {
29        2 // Changed
30    } else {
31        0 // OK
32    }
33}
34
35pub fn parse_backend_decimal(value: &Option<String>) -> f64 {
36    value
37        .as_ref()
38        .and_then(|v| v.parse::<f64>().ok())
39        .unwrap_or(0.0)
40}
41
42/// Backend `odr_sys_cmn::OrderFill` -> FTAPI 对外 qty/price。
43///
44/// C++ `NNProto_Trd_Deal.cpp:70-86` 在成交被取消时有特殊逻辑:
45/// 若 backend 同时下发 `original_price` 与 `original_qty`,对外展示使用
46/// original price/qty,而不是取消记录当前 price/qty。active read、
47/// history read 与 TradeReQuery push-refresh 都必须共享同一套派生,避免三
48/// 条路径显示不同数值。
49pub fn backend_order_fill_qty_price_for_ftapi(fill: &odr_sys_cmn::OrderFill) -> (f64, f64) {
50    let use_original = fill.is_cancelled.unwrap_or(false)
51        && fill.original_price.is_some()
52        && fill.original_qty.is_some();
53    let qty = if use_original {
54        parse_backend_decimal(&fill.original_qty)
55    } else {
56        parse_backend_decimal(&fill.qty)
57    };
58    let price = if use_original {
59        parse_backend_decimal(&fill.original_price)
60    } else {
61        parse_backend_decimal(&fill.price)
62    };
63    (qty, price)
64}
65
66/// `NN_TrdMarket_ConvC2S` — 对齐 C++ `NNBase_Define_Inline.h:641-672`.
67///
68/// FTAPI client `NN_TrdMarket` enum value → backend wire market id.
69/// 大部分 market 直传, 仅 MY=111→11 / CA=112→12 / HK_Fund=113→13 /
70/// Fund=114→14 / US_Fund=123→23 / SG_Fund=124→24 这几个特殊 (C++ 历史
71/// enum 值与 wire id 不一致).
72pub fn convert_trd_market_c2s(client_market: i32) -> u32 {
73    match client_market {
74        111 => 11, // MY
75        112 => 12, // CA
76        113 => 13, // HK_Fund
77        114 => 14, // Fund
78        123 => 23, // US_Fund
79        124 => 24, // SG_Fund
80        // JP=15 / SG=6 / AU=8 / HK=1 / US=2 / HKCC=4 / Futures=5 等直传
81        m if m >= 0 => m as u32,
82        _ => 0,
83    }
84}
85
86/// 从 cache 派生 (trd_env, enabled_markets), 用于 `build_*_req` helper.
87///
88/// **空 list 兜底**: 如 cache 未填 (启动期 / 账户 list 未到), 返
89/// `(env=0, vec![])` — 让 caller 仍能发请求 (backend 默认行为是返全部),
90/// 不阻塞主流程. 这与 C++ 行为略偏: C++ `accItem.nEnableMarketLength = 0`
91/// 时 add_market 0 次, 等价我们 vec![] (backend 视为不过滤).
92pub fn derive_acc_query_context(trd_cache: &TrdCache, acc_id: u64) -> (i32, Vec<i32>) {
93    if let Some(entry) = trd_cache.accounts.get(&acc_id) {
94        let acc = entry.value();
95        return (acc.trd_env, acc.trd_market_auth_list.clone());
96    }
97    (0, Vec::new())
98}
99
100/// 派生 backend 用的 market list:
101/// `enabled_markets` → 过 `is_valid_trd_market(env, m)` → `convert_trd_market_c2s(m)`.
102pub fn derive_backend_market_list(trd_env: i32, enabled_markets: &[i32]) -> Vec<u32> {
103    enabled_markets
104        .iter()
105        .copied()
106        .filter(|m| is_valid_trd_market(trd_env, *m))
107        .map(convert_trd_market_c2s)
108        .collect()
109}
110
111pub fn order_fill_list_cmd_for_env(trd_env: i32) -> u16 {
112    if trd_env == 1 {
113        trade_query_command(TradeQueryOperation::Fills).real_cmd
114    } else {
115        trade_query_command(TradeQueryOperation::Fills).sim_cmd
116    }
117}
118
119pub fn order_fill_info_cmd_for_env(trd_env: i32) -> u16 {
120    if trd_env == 1 {
121        trade_query_command(TradeQueryOperation::FillInfo).real_cmd
122    } else {
123        trade_query_command(TradeQueryOperation::FillInfo).sim_cmd
124    }
125}
126
127pub fn history_order_list_cmd_for_env(trd_env: i32) -> u16 {
128    if trd_env == 1 {
129        trade_query_command(TradeQueryOperation::HistoryOrders).real_cmd
130    } else {
131        trade_query_command(TradeQueryOperation::HistoryOrders).sim_cmd
132    }
133}
134
135pub fn history_order_fill_list_cmd_for_env(trd_env: i32) -> u16 {
136    if trd_env == 1 {
137        trade_query_command(TradeQueryOperation::HistoryFills).real_cmd
138    } else {
139        trade_query_command(TradeQueryOperation::HistoryFills).sim_cmd
140    }
141}