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 const BACKEND_SECURITY_TYPE_COMMON: u32 = 1;
10pub const BACKEND_SECURITY_TYPE_OPTION: u32 = 2;
11pub const BACKEND_SECURITY_TYPE_FUTURES: u32 = 4;
12pub const BACKEND_SECURITY_TYPE_MULTILEG: u32 = 7;
13
14pub fn is_valid_trd_market(trd_env: i32, market: i32) -> bool {
15    if trd_env == 0 {
16        // Simulate
17        return true;
18    }
19    matches!(market, 1 | 2 | 4 | 5 | 6 | 8 | 15 | 111 | 112)
20}
21
22/// C++ `NNProto_Trd_OrderReal::FillOrderListReq` /
23/// `FillHistoryOrderListReq` branch on account `m_enTrdMkt`.
24///
25/// Ref:
26/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderReal.cpp:522-533`
27/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderReal.cpp:555-566`
28/// - `FutuOpenD/Src/NNBase/NNBase_Define_Enum.h:168,174-177`
29pub fn is_futures_trd_market_like_cpp(market: i32) -> bool {
30    matches!(market, 5 | 10 | 11 | 12 | 13)
31}
32
33/// Security type list for order-list/history-order-list query body.
34///
35/// C++ real order query is intentionally different from deal/fill query:
36/// - Real futures account: `FUTURES` only.
37/// - Real non-futures account: `COMMON`, `OPTION`, `MULTILEG`.
38/// - Sim order query: `COMMON`, `OPTION`, `FUTURES`.
39///
40/// Do not reuse this helper for order fill list; C++ deal query still uses
41/// `COMMON`, `OPTION`, `FUTURES`.
42pub fn order_query_security_types_like_cpp(
43    trd_env: i32,
44    account_trd_market: Option<i32>,
45) -> Vec<u32> {
46    if trd_env == 1 {
47        if account_trd_market.is_some_and(is_futures_trd_market_like_cpp) {
48            vec![BACKEND_SECURITY_TYPE_FUTURES]
49        } else {
50            vec![
51                BACKEND_SECURITY_TYPE_COMMON,
52                BACKEND_SECURITY_TYPE_OPTION,
53                BACKEND_SECURITY_TYPE_MULTILEG,
54            ]
55        }
56    } else {
57        vec![
58            BACKEND_SECURITY_TYPE_COMMON,
59            BACKEND_SECURITY_TYPE_OPTION,
60            BACKEND_SECURITY_TYPE_FUTURES,
61        ]
62    }
63}
64
65/// `backend_deal_status_to_ftapi` — 对齐 C++ `NN_DealStatus`
66/// (`NNBase_Define_Enum.h:377-381`) + C++ `NNProto_Trd_Deal.cpp:68-91` 转换:
67/// `is_cancelled=true → 1 (Cancelled)`, `is_corrected=true → 2 (Changed)`,
68/// otherwise → `0 (OK)`.
69///
70/// **v1.4.106 codex 0955 F6**: 之前 `query_order_fills` 用错值 (1/2/3 代
71/// 0/1/2), 与 active read path (`GetOrderFillListHandler`) 不一致. 现在抽
72/// 共享 helper 让 active 读 + push refresh + cache 全部用同一映射.
73pub fn backend_deal_status_to_ftapi(is_cancelled: bool, is_corrected: bool) -> i32 {
74    if is_cancelled {
75        1 // Cancelled
76    } else if is_corrected {
77        2 // Changed
78    } else {
79        0 // OK
80    }
81}
82
83pub fn parse_backend_decimal(value: &Option<String>) -> f64 {
84    value
85        .as_ref()
86        .and_then(|v| v.parse::<f64>().ok())
87        .unwrap_or(0.0)
88}
89
90/// Backend `odr_sys_cmn::OrderFill` -> FTAPI 对外 qty/price。
91///
92/// C++ `NNProto_Trd_Deal.cpp:70-86` 在成交被取消时有特殊逻辑:
93/// 若 backend 同时下发 `original_price` 与 `original_qty`,对外展示使用
94/// original price/qty,而不是取消记录当前 price/qty。active read、
95/// history read 与 TradeReQuery push-refresh 都必须共享同一套派生,避免三
96/// 条路径显示不同数值。
97pub fn backend_order_fill_qty_price_for_ftapi(fill: &odr_sys_cmn::OrderFill) -> (f64, f64) {
98    let use_original = fill.is_cancelled.unwrap_or(false)
99        && fill.original_price.is_some()
100        && fill.original_qty.is_some();
101    let qty = if use_original {
102        parse_backend_decimal(&fill.original_qty)
103    } else {
104        parse_backend_decimal(&fill.qty)
105    };
106    let price = if use_original {
107        parse_backend_decimal(&fill.original_price)
108    } else {
109        parse_backend_decimal(&fill.price)
110    };
111    (qty, price)
112}
113
114/// `NN_TrdMarket_ConvC2S` — 对齐 C++ `NNBase_Define_Inline.h:641-672`.
115///
116/// FTAPI client `NN_TrdMarket` enum value → backend wire market id.
117/// 大部分 market 直传, 仅 MY=111→11 / CA=112→12 / HK_Fund=113→13 /
118/// Fund=114→14 / US_Fund=123→23 / SG_Fund=124→24 这几个特殊 (C++ 历史
119/// enum 值与 wire id 不一致).
120pub fn convert_trd_market_c2s(client_market: i32) -> u32 {
121    match client_market {
122        111 => 11, // MY
123        112 => 12, // CA
124        113 => 13, // HK_Fund
125        114 => 14, // Fund
126        123 => 23, // US_Fund
127        124 => 24, // SG_Fund
128        // JP=15 / SG=6 / AU=8 / HK=1 / US=2 / HKCC=4 / Futures=5 等直传
129        m if m >= 0 => m as u32,
130        _ => 0,
131    }
132}
133
134/// 从 cache 派生 (trd_env, account_trd_market, enabled_markets), 用于 `build_*_req` helper.
135///
136/// **空 list 兜底**: 如 cache 未填 (启动期 / 账户 list 未到), 返
137/// `(env=0, account_market=None, vec![])` — 让 caller 仍能发请求 (backend 默认行为是返全部),
138/// 不阻塞主流程. 这与 C++ 行为略偏: C++ `accItem.nEnableMarketLength = 0`
139/// 时 add_market 0 次, 等价我们 vec![] (backend 视为不过滤).
140pub fn derive_acc_query_context(trd_cache: &TrdCache, acc_id: u64) -> (i32, Option<i32>, Vec<i32>) {
141    if let Some(entry) = trd_cache.accounts.get(&acc_id) {
142        let acc = entry.value();
143        return (
144            acc.trd_env,
145            acc.trd_market,
146            acc.trd_market_auth_list.clone(),
147        );
148    }
149    (0, None, Vec::new())
150}
151
152/// 派生 backend 用的 market list:
153/// `enabled_markets` → 过 `is_valid_trd_market(env, m)` → `convert_trd_market_c2s(m)`.
154pub fn derive_backend_market_list(trd_env: i32, enabled_markets: &[i32]) -> Vec<u32> {
155    enabled_markets
156        .iter()
157        .copied()
158        .filter(|m| is_valid_trd_market(trd_env, *m))
159        .map(convert_trd_market_c2s)
160        .collect()
161}
162
163pub fn order_fill_list_cmd_for_env(trd_env: i32) -> u16 {
164    if trd_env == 1 {
165        trade_query_command(TradeQueryOperation::Fills).real_cmd
166    } else {
167        trade_query_command(TradeQueryOperation::Fills).sim_cmd
168    }
169}
170
171pub fn order_info_cmd_for_env(trd_env: i32) -> u16 {
172    if trd_env == 1 {
173        trade_query_command(TradeQueryOperation::OrderInfo).real_cmd
174    } else {
175        trade_query_command(TradeQueryOperation::OrderInfo).sim_cmd
176    }
177}
178
179pub fn order_fill_info_cmd_for_env(trd_env: i32) -> u16 {
180    if trd_env == 1 {
181        trade_query_command(TradeQueryOperation::FillInfo).real_cmd
182    } else {
183        trade_query_command(TradeQueryOperation::FillInfo).sim_cmd
184    }
185}
186
187pub fn history_order_list_cmd_for_env(trd_env: i32) -> u16 {
188    if trd_env == 1 {
189        trade_query_command(TradeQueryOperation::HistoryOrders).real_cmd
190    } else {
191        trade_query_command(TradeQueryOperation::HistoryOrders).sim_cmd
192    }
193}
194
195pub fn history_order_fill_list_cmd_for_env(trd_env: i32) -> u16 {
196    if trd_env == 1 {
197        trade_query_command(TradeQueryOperation::HistoryFills).real_cmd
198    } else {
199        trade_query_command(TradeQueryOperation::HistoryFills).sim_cmd
200    }
201}