Skip to main content

futu_backend/trade_query/orders/
status_helpers.rs

1//! trade_query/orders/status_helpers — backend response_status_like_cpp + backend_order_status/type/market_to_ftapi + init_trade_data
2//! (v1.4.110 CC Batch J: 拆自 orders.rs L781-888 + 1073-1213)
3
4use std::sync::Arc;
5
6use futu_cache::trd_cache::TrdCache;
7
8use super::super::*;
9use crate::conn::BackendConn;
10use crate::proto_internal::odr_sys_cmn;
11
12use super::types::BackendResponseStatusError;
13
14pub fn backend_trade_response_status_like_cpp(
15    message_name: &str,
16    result: Option<i32>,
17    msg_header: Option<&odr_sys_cmn::MsgHeader>,
18    err_msg: Option<&str>,
19    acc_id: u64,
20) -> std::result::Result<(), BackendResponseStatusError> {
21    let msg_header = msg_header.ok_or_else(|| BackendResponseStatusError {
22        result: -1,
23        message: format!("{message_name} missing msg_header"),
24        is_backend_error: false,
25    })?;
26    backend_trade_response_status_by_account_like_cpp(
27        message_name,
28        result,
29        msg_header.account_id,
30        err_msg,
31        acc_id,
32    )
33}
34
35pub(super) fn backend_trade_response_status_by_account_like_cpp(
36    message_name: &str,
37    result: Option<i32>,
38    backend_acc_id: Option<u64>,
39    err_msg: Option<&str>,
40    acc_id: u64,
41) -> std::result::Result<(), BackendResponseStatusError> {
42    let result = result.ok_or_else(|| BackendResponseStatusError {
43        result: -1,
44        message: format!("{message_name} missing result"),
45        is_backend_error: false,
46    })?;
47    let backend_acc_id = backend_acc_id.ok_or_else(|| BackendResponseStatusError {
48        result: -1,
49        message: format!("{message_name} msg_header missing account_id"),
50        is_backend_error: false,
51    })?;
52    if backend_acc_id != acc_id {
53        return Err(BackendResponseStatusError {
54            result: -1,
55            message: format!(
56                "{message_name} msg_header.account_id mismatch, got {backend_acc_id}, expected {acc_id}"
57            ),
58            is_backend_error: false,
59        });
60    }
61    if result != 0 {
62        return Err(BackendResponseStatusError {
63            result,
64            message: err_msg
65                .unwrap_or("trade query backend rejected")
66                .to_string(),
67            is_backend_error: true,
68        });
69    }
70    Ok(())
71}
72
73/// Match C++ order-list response gating for backend refresh helpers.
74///
75/// Ref:
76/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderReal.cpp:245-288`
77/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderSimulate.cpp:239-282`
78pub fn backend_order_list_status_like_cpp(
79    result: Option<i32>,
80    msg_header: Option<&odr_sys_cmn::MsgHeader>,
81    err_msg: Option<&str>,
82    acc_id: u64,
83) -> std::result::Result<(), BackendResponseStatusError> {
84    backend_trade_response_status_like_cpp("OrderListRsp", result, msg_header, err_msg, acc_id)
85}
86
87/// Match C++ single-order-info response gating.
88///
89/// Ref:
90/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderReal.cpp:368-418`
91/// - `FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderSimulate.cpp:362-412`
92pub fn backend_order_info_status_like_cpp(
93    result: Option<i32>,
94    msg_header: Option<&odr_sys_cmn::MsgHeader>,
95    err_msg: Option<&str>,
96    acc_id: u64,
97    expected_count: usize,
98    actual_count: usize,
99) -> std::result::Result<(), BackendResponseStatusError> {
100    backend_trade_response_status_like_cpp("OrderDetailRsp", result, msg_header, err_msg, acc_id)?;
101    if actual_count == 0 || actual_count != expected_count {
102        return Err(BackendResponseStatusError {
103            result: -1,
104            message: format!(
105                "OrderDetailRsp orders count mismatch, got {actual_count}, expected {expected_count}"
106            ),
107            is_backend_error: false,
108        });
109    }
110    Ok(())
111}
112
113pub(super) fn backend_order_info_status_by_account_like_cpp(
114    result: Option<i32>,
115    backend_acc_id: Option<u64>,
116    err_msg: Option<&str>,
117    acc_id: u64,
118    expected_count: usize,
119    actual_count: usize,
120) -> std::result::Result<(), BackendResponseStatusError> {
121    backend_trade_response_status_by_account_like_cpp(
122        "OrderDetailRsp",
123        result,
124        backend_acc_id,
125        err_msg,
126        acc_id,
127    )?;
128    if actual_count == 0 || actual_count != expected_count {
129        return Err(BackendResponseStatusError {
130            result: -1,
131            message: format!(
132                "OrderDetailRsp orders count mismatch, got {actual_count}, expected {expected_count}"
133            ),
134            is_backend_error: false,
135        });
136    }
137    Ok(())
138}
139
140/// Match C++ current-deal response gating for backend refresh helpers.
141///
142/// Ref:
143/// - `FutuOpenD/Src/NNProtoCenter/Trade/Deal/NNProto_Trd_DealReal.cpp:259-305`
144/// - `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.h:20-30`
145pub fn backend_order_fill_list_status_like_cpp(
146    result: Option<i32>,
147    msg_header: Option<&odr_sys_cmn::MsgHeader>,
148    err_msg: Option<&str>,
149    acc_id: u64,
150) -> std::result::Result<(), BackendResponseStatusError> {
151    backend_trade_response_status_like_cpp("OrderFillListRsp", result, msg_header, err_msg, acc_id)
152}
153
154/// Match C++ single-deal-info response gating.
155///
156/// Ref: `FutuOpenD/Src/NNProtoCenter/Trade/Deal/NNProto_Trd_DealReal.cpp:382-431`.
157pub fn backend_order_fill_info_status_like_cpp(
158    result: Option<i32>,
159    msg_header: Option<&odr_sys_cmn::MsgHeader>,
160    err_msg: Option<&str>,
161    acc_id: u64,
162    expected_count: usize,
163    actual_count: usize,
164) -> std::result::Result<(), BackendResponseStatusError> {
165    backend_trade_response_status_like_cpp(
166        "OrderFillInfoRsp",
167        result,
168        msg_header,
169        err_msg,
170        acc_id,
171    )?;
172    if actual_count == 0 || actual_count != expected_count {
173        return Err(BackendResponseStatusError {
174            result: -1,
175            message: format!(
176                "OrderFillInfoRsp order_fills count mismatch, got {actual_count}, expected {expected_count}"
177            ),
178            is_backend_error: false,
179        });
180    }
181    Ok(())
182}
183
184/// 指定成交 ID 查询 (push `NOTICE_TYPE_ORDER_FILL_UPDATE` 路径).
185///
186/// C++ 在 `NNProto_Trd_OnPush.cpp:151-161` 收到 notice_type=6 后调用
187/// `QueryDealInfo`,请求体为 `OrderFillInfoReq`,命令为 4715/14715;
188/// 不会用 4710/14710 全量成交列表代替。
189pub fn backend_order_status_to_ftapi(status: u32) -> i32 {
190    match status {
191        1 => 2,    // PENGDING_NEW → Submitting
192        2 => 5,    // NEW → Submitted
193        3 => 10,   // PARTIAL_FILL → FilledPart
194        4 => 11,   // FILL → FilledAll
195        5 => 15,   // CANCELLED → CancelledAll
196        6 => 21,   // REJECTED → Failed
197        7 => 14,   // PARTIAL_FILL_CANCELLED → CancelledPart
198        101 => 22, // DISABLE → Disabled
199        102 => 1,  // WAITING_NEW → WaitingSubmit
200        103 => 24, // FILL_CANCELLED → FillCancelled
201        104 => 23, // DELETE → Deleted
202        _ => -1,   // Unknown(对齐 C++ default 分支 NN_OrderStatus_Unknown)
203    }
204}
205
206/// Backend `odr_sys_cmn::OrderType` → FTAPI `Trd_Common::OrderType`.
207///
208/// C++ source of truth:
209/// `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp`
210/// `Trd_OrderTypeConv_S2C`.
211///
212/// The two HK common special cases matter:
213/// - backend `ORDER_TYPE_LIMIT=1` means FTAPI `AbsoluteLimit=5`
214/// - backend `ORDER_TYPE_ENHANCED_LIMIT=2` means FTAPI `Normal=1`
215///
216/// Directly passing backend `2` through would make OpenAPI clients read a HK
217/// enhanced limit order as FTAPI `Market=2`, which is a C++ parity break.
218pub fn backend_order_type_to_ftapi(
219    order_type: u32,
220    trd_market: Option<i32>,
221    security_type: Option<i32>,
222) -> i32 {
223    const TRD_MARKET_HK: i32 = 1;
224    const SECURITY_TYPE_COMMON: i32 = 1;
225    let is_hk_common =
226        trd_market == Some(TRD_MARKET_HK) && security_type == Some(SECURITY_TYPE_COMMON);
227
228    match order_type {
229        1 if is_hk_common => 5, // LIMIT + HK common → AbsoluteLimit
230        1 => 1,                 // LIMIT elsewhere → Normal
231        2 => 1,                 // ENHANCED_LIMIT → Normal
232        3 => 2,                 // MARKET
233        4 => 6,                 // AUCTION
234        5 => 7,                 // AUCTION_LIMIT
235        6 => 9,                 // SPECIAL_FOK → SpecialLimit_All
236        7 => 8,                 // SPECIAL_FAK → SpecialLimit
237        8 => 10,                // STOP
238        9 => 11,                // STOP_LIMIT
239        10 => 12,               // MIT
240        11 => 13,               // LIT
241        12 => 14,               // TRAIL
242        13 => 15,               // TRAIL_LIMIT
243        21 => 16,               // ALGO_TWAP_MARKET
244        22 => 17,               // ALGO_TWAP_LIMIT
245        23 => 18,               // ALGO_VWAP_MARKET
246        24 => 19,               // ALGO_VWAP_LIMIT
247        _ => -1,
248    }
249}
250
251/// 后端 market → FTAPI TrdMarket
252pub fn map_backend_market_to_trd_market(market: u32) -> i32 {
253    match market {
254        1 => 1,  // HK
255        2 => 2,  // US
256        3 => 3,  // CN
257        10 => 5, // Futures
258        15 => 6, // SG
259        v => v as i32,
260    }
261}
262
263pub fn backend_real_market_to_trd_market_like_cpp(market: u32) -> i32 {
264    match market {
265        7 => 200,  // Crypto
266        11 => 111, // MY
267        12 => 112, // CA
268        13 => 113, // HK fund
269        14 => 114, // Fund
270        15 => 15,  // JP
271        23 => 123, // US fund
272        24 => 124, // SG fund
273        v => v as i32,
274    }
275}
276
277pub fn is_valid_real_trd_market_like_cpp(trd_market: i32) -> bool {
278    matches!(trd_market, 1 | 2 | 4 | 5 | 6 | 8 | 15 | 111 | 112 | 200)
279}
280
281pub fn backend_order_unpacked_trd_market_like_cpp(
282    trd_env: i32,
283    o: &odr_sys_cmn::Order,
284) -> Option<Option<i32>> {
285    // Ref: FutuOpenD/Src/NNProtoCenter/Trade/Order/NNProto_Trd_OrderReal.cpp:40-98
286    // and NNProto_Trd_OrderSimulate.cpp:40-98. C++ drops rows missing required
287    // fields before they enter INNData_Trd_Order.
288    o.order_id.as_ref()?;
289    o.side?;
290    let trd_market = if trd_env == 1 {
291        let trd_market = backend_real_market_to_trd_market_like_cpp(o.market?);
292        if !is_valid_real_trd_market_like_cpp(trd_market) {
293            return None;
294        }
295        Some(trd_market)
296    } else {
297        o.market.map(map_backend_market_to_trd_market)
298    };
299    o.symbol.as_ref()?;
300    o.create_time?;
301    o.status?;
302    Some(trd_market)
303}
304
305/// 为所有真实账户查询资金和持仓 (CMD 3020)
306pub async fn init_trade_data(backend: &Arc<BackendConn>, trd_cache: &Arc<TrdCache>) {
307    let accounts = trd_cache.get_accounts();
308    let real_accounts: Vec<u64> = accounts
309        .iter()
310        .filter(|a| a.trd_env == 1) // 只查真实账户
311        .map(|a| a.acc_id)
312        .collect();
313
314    tracing::info!(
315        count = real_accounts.len(),
316        "querying trade data for real accounts"
317    );
318
319    for &acc_id in &real_accounts {
320        // v1.4.106 codex 1556 F1+F3 + v1.4.107 fanout:
321        // bootstrap query 不知 user-requested currency / asset_category, 全传
322        // None 走 backend helper 的 C++ category expansion (JP margin=Foreign,
323        // JP derivative=Domestic+Foreign, other=None).
324        if let Err(e) = query_account_info(backend, acc_id, trd_cache, None, None).await {
325            tracing::warn!(
326                acc_id,
327                error = %e,
328                "init trade data account info query failed"
329            );
330        }
331    }
332
333    tracing::info!("trade data initialization complete");
334}