Skip to main content

futu_backend/trade_query/orders/
query_fills.rs

1//! trade_query/orders/query_fills — query_order_fills* + query_order_fills_inner + order_fill_proto
2//! (v1.4.110 CC Batch J: 拆自 orders.rs L667-780 + 889-1072)
3
4use futu_cache::trd_cache::TrdCache;
5use futu_core::error::Result;
6
7use super::super::common::hash_str_to_u64;
8use crate::conn::BackendConn;
9use crate::proto_internal::{odr_sys_cmn, order_sys_interface};
10
11use super::types::{OrderFillInfo, QueryErrorMode};
12
13use super::builders::*;
14use super::helpers::*;
15use super::status_helpers::*;
16
17pub async fn query_order_fills(
18    backend: &BackendConn,
19    acc_id: u64,
20    trd_cache: &TrdCache,
21) -> Result<Vec<OrderFillInfo>> {
22    query_order_fills_inner(backend, acc_id, trd_cache, QueryErrorMode::LenientCacheRead).await
23}
24
25/// v1.4.106 codex 0932 F3 [P2]: strict 版 query_order_fills for push-refresh path.
26///
27/// decode 失败 / `result != 0` → 返 `Err`, retry_with_exp_backoff 触发 retry.
28/// 4 attempts 全失败 → caller 应 record F2 exhaust + log warn (F4 wired in
29/// dispatcher, see codex 0932 F4). v1.4.106 codex 0955 F4: 同样接 `trd_cache`.
30pub async fn query_order_fills_strict_for_push_refresh(
31    backend: &BackendConn,
32    acc_id: u64,
33    trd_cache: &TrdCache,
34) -> Result<Vec<OrderFillInfo>> {
35    query_order_fills_inner(
36        backend,
37        acc_id,
38        trd_cache,
39        QueryErrorMode::StrictPushRefresh,
40    )
41    .await
42}
43
44pub fn order_fill_proto_to_info_with_market_and_jp_acc_type(
45    f: &odr_sys_cmn::OrderFill,
46    trd_market: Option<i32>,
47    jp_acc_type: Option<i32>,
48) -> OrderFillInfo {
49    let (qty, price) = backend_order_fill_qty_price_for_ftapi(f);
50    let fill_id_ex = f.id.clone().unwrap_or_default();
51    // v1.4.106 codex 0226 F6 (parity with query_orders): backend fill_id
52    // (= server-generated fill ID) 也是 alphanumeric 字符串. 用 hash
53    // 派生 numeric id, 对齐 C++ 的 hash chain. empty 时返 0.
54    let fill_id: u64 = if fill_id_ex.is_empty() {
55        0
56    } else {
57        hash_str_to_u64(&fill_id_ex)
58    };
59    let order_id_ex = f.order_id.clone().unwrap_or_default();
60    let order_id: u64 = if order_id_ex.is_empty() {
61        0
62    } else {
63        hash_str_to_u64(&order_id_ex)
64    };
65    let create_timestamp = f.create_time.map(|t| t as f64 / 1_000_000.0);
66    let update_timestamp = f.update_time.map(|t| t as f64 / 1_000_000.0);
67    let counter_broker_id = f
68        .counter_broker_id
69        .as_ref()
70        .and_then(|s| s.parse::<i32>().ok());
71    // v1.4.106 codex 0955 F6: 用 shared helper, 与 active read path
72    // (GetOrderFillListHandler) 一致 (0/1/2 对齐 C++ NN_DealStatus).
73    let status = Some(backend_deal_status_to_ftapi(
74        f.is_cancelled.unwrap_or(false),
75        f.is_corrected.unwrap_or(false),
76    ));
77
78    OrderFillInfo {
79        fill_id,
80        fill_id_ex,
81        order_id,
82        order_id_ex,
83        code: f.symbol.as_ref().cloned().unwrap_or_default(),
84        name: f.stock_name.as_ref().cloned().unwrap_or_default(),
85        trd_side: f.side.unwrap_or(0) as i32,
86        qty,
87        price,
88        create_timestamp,
89        update_timestamp,
90        counter_broker_id,
91        status,
92        trd_market,
93        jp_acc_type,
94    }
95}
96
97/// Backend `odr_sys_cmn::OrderFill` → OpenD cache/API `OrderFillInfo`.
98///
99/// Ref: FutuOpenD/Src/NNProtoCenter/Trade/Deal/NNProto_Trd_DealReal.cpp:13-122.
100/// C++ `UnPackDealItem` drops rows unless `id/side/market/symbol/exchange/
101/// price/qty/create_time` are present, `price/qty` parse as doubles, and the
102/// real-account market passes `IsValidTrdMarket(NN_TrdEnv_Real, market)`.
103///
104/// Simulated deal query is unsupported in C++ (`NNProto_Trd_DealSimulate.cpp:13-67`),
105/// while `NOTICE_TYPE_ORDER_FILL_NTF` also calls `DealReal::UnPackDealList`, so
106/// Rust uses the same real deal row gate for query-cache and direct-push paths.
107pub fn try_order_fill_proto_to_info_like_cpp(f: &odr_sys_cmn::OrderFill) -> Option<OrderFillInfo> {
108    try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(f, None)
109}
110
111pub fn try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(
112    f: &odr_sys_cmn::OrderFill,
113    jp_acc_type: Option<i32>,
114) -> Option<OrderFillInfo> {
115    f.id.as_ref()?;
116    f.side?;
117    let trd_market = backend_real_market_to_trd_market_like_cpp(f.market?);
118    if !is_valid_real_trd_market_like_cpp(trd_market) {
119        return None;
120    }
121    f.symbol.as_ref()?;
122    f.exchange.as_ref()?;
123    f.price.as_deref()?.parse::<f64>().ok()?;
124    f.qty.as_deref()?.parse::<f64>().ok()?;
125    f.create_time?;
126
127    Some(order_fill_proto_to_info_with_market_and_jp_acc_type(
128        f,
129        Some(trd_market),
130        jp_acc_type,
131    ))
132}
133
134/// Match C++ `CheckRspHeaderAndGetSvrRet` response gating.
135///
136/// Ref: `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.h:20-30`.
137pub async fn query_order_fill_info_strict_for_push_refresh(
138    backend: &BackendConn,
139    acc_id: u64,
140    trd_cache: &TrdCache,
141    order_fill_ids: &[String],
142    security_type: Option<u32>,
143    exchange: Option<&str>,
144) -> Result<Vec<OrderFillInfo>> {
145    use prost::Message;
146
147    let filtered_ids: Vec<String> = order_fill_ids
148        .iter()
149        .filter(|id| !id.is_empty())
150        .cloned()
151        .collect();
152    if filtered_ids.is_empty() {
153        return Ok(vec![]);
154    }
155
156    let (trd_env, _enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
157    let cmd_id = order_fill_info_cmd_for_env(trd_env);
158    let req = build_order_fill_info_req_base(acc_id, security_type, exchange, &filtered_ids);
159    let resp = backend.request(cmd_id, req.encode_to_vec()).await?;
160    let parsed: order_sys_interface::OrderFillInfoRsp = Message::decode(resp.body.as_ref())?;
161
162    if let Err(status_err) = backend_order_fill_info_status_like_cpp(
163        parsed.result,
164        parsed.msg_header.as_ref(),
165        parsed.err_msg.as_deref(),
166        acc_id,
167        filtered_ids.len(),
168        parsed.order_fills.len(),
169    ) {
170        return Err(futu_core::error::FutuError::ServerError {
171            ret_type: status_err.result,
172            msg: format!("{} (acc_id={acc_id})", status_err.message),
173        });
174    }
175
176    Ok(parsed
177        .order_fills
178        .iter()
179        .filter_map(|fill| {
180            let jp_acc_type = fill
181                .sub_account_id
182                .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
183            try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
184        })
185        .collect())
186}
187
188pub async fn query_order_fills_inner(
189    backend: &BackendConn,
190    acc_id: u64,
191    trd_cache: &TrdCache,
192    error_mode: QueryErrorMode,
193) -> Result<Vec<OrderFillInfo>> {
194    use prost::Message;
195
196    // v1.4.106 codex 0955 F4: req shape 对齐 C++ QueryDealList_Inner.
197    let (trd_env, enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
198    let req = build_order_fill_list_req_base(acc_id, trd_env, &enabled_markets);
199    let cmd_id = order_fill_list_cmd_for_env(trd_env);
200
201    let mut all_fills = Vec::new();
202    let mut page_flag: Option<String> = None;
203    // v1.4.106 codex 0955 F5: 显式 track completion (parity with query_orders).
204    const MAX_PAGES_FILLS: usize = 100;
205    let mut fills_completed = false;
206
207    for _page in 0..MAX_PAGES_FILLS {
208        let mut paged_req = req.clone();
209        paged_req.page_flag = page_flag.clone();
210
211        let resp = match backend.request(cmd_id, paged_req.encode_to_vec()).await {
212            Ok(r) => r,
213            Err(e) => {
214                tracing::debug!(acc_id, cmd_id, error = %e, "fill query failed");
215                return Err(e);
216            }
217        };
218
219        let parsed: order_sys_interface::OrderFillListRsp =
220            match Message::decode(resp.body.as_ref()) {
221                Ok(p) => p,
222                Err(e) => {
223                    // v1.4.106 codex 0932 F3 [P2]: strict 模式 → 返 Err 让上层 retry.
224                    if error_mode.is_strict() {
225                        tracing::warn!(
226                            acc_id,
227                            error = %e,
228                            "v1.4.106 F3 strict: fill response decode failed → \
229                             returning Err to trigger retry (push-refresh path)"
230                        );
231                        return Err(futu_core::error::FutuError::from(e));
232                    }
233                    tracing::debug!(acc_id, error = %e, "fill response decode failed (lenient)");
234                    return Ok(vec![]);
235                }
236            };
237
238        if let Err(status_err) = backend_order_fill_list_status_like_cpp(
239            parsed.result,
240            parsed.msg_header.as_ref(),
241            parsed.err_msg.as_deref(),
242            acc_id,
243        ) {
244            // v1.4.110: C++ CheckRspHeaderAndGetSvrRet also rejects missing or
245            // mismatched msg_header.account_id. Treat structural response
246            // issues as retryable even on lenient read paths so they cannot
247            // masquerade as a legitimate empty fill snapshot.
248            if error_mode.is_strict() || !status_err.is_backend_error {
249                tracing::warn!(
250                    acc_id,
251                    result = status_err.result,
252                    error = %status_err.message,
253                    "v1.4.110: fill query status/header invalid -> returning Err \
254                     to trigger retry (push-refresh path)"
255                );
256                return Err(futu_core::error::FutuError::ServerError {
257                    ret_type: status_err.result,
258                    msg: format!("{} (acc_id={acc_id})", status_err.message),
259                });
260            }
261            return Ok(vec![]);
262        }
263
264        all_fills.extend(parsed.order_fills.iter().filter_map(|fill| {
265            let jp_acc_type = fill
266                .sub_account_id
267                .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
268            try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
269        }));
270
271        // v1.4.106 codex 0955 F5: 与 query_orders 同 partial-pagination 防御.
272        let parsed_completed = parsed.completed.unwrap_or(false);
273        if parsed_completed {
274            fills_completed = true;
275            break;
276        }
277        match parsed.page_flag {
278            Some(ref pf) if !pf.is_empty() => {
279                page_flag = Some(pf.clone());
280            }
281            _ => {
282                tracing::warn!(
283                    acc_id,
284                    partial_count = all_fills.len(),
285                    "v1.4.106 audit 0955 F5: backend completed=false 但 page_flag \
286                     缺失/空 — partial response, 不返 partial 列表防 caller 误用"
287                );
288                return Err(futu_core::error::FutuError::Codec(
289                    "query_order_fills: partial response (completed=false + missing page_flag)"
290                        .to_string(),
291                ));
292            }
293        }
294    }
295
296    if !fills_completed {
297        tracing::warn!(
298            acc_id,
299            max_pages = MAX_PAGES_FILLS,
300            partial_count = all_fills.len(),
301            "v1.4.106 audit 0955 F5: pagination 超 MAX_PAGES 仍未 completed — partial"
302        );
303        return Err(futu_core::error::FutuError::Codec(format!(
304            "query_order_fills: pagination exceeded {MAX_PAGES_FILLS} pages without completed=true"
305        )));
306    }
307
308    tracing::debug!(acc_id, count = all_fills.len(), "order fills queried");
309    Ok(all_fills)
310}