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