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