Skip to main content

futu_backend/trade_query/orders/
query_orders.rs

1//! trade_query/orders/query_orders — query_orders + query_orders_inner (real + sim 双路径)
2//! (v1.4.110 CC Batch J: 拆自 orders.rs L336-666)
3
4use futu_cache::trd_cache::TrdCache;
5use futu_core::error::Result;
6
7use super::super::common::{hash_str_to_u64, parse_open_d_text};
8use super::super::*;
9use crate::conn::BackendConn;
10use crate::proto_internal::order_sys_interface;
11
12use super::types::QueryErrorMode;
13
14use super::builders::*;
15use super::helpers::*;
16use super::status_helpers::*;
17
18pub async fn query_orders(
19    backend: &BackendConn,
20    acc_id: u64,
21    trd_cache: &TrdCache,
22) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
23    query_orders_inner(backend, acc_id, trd_cache, QueryErrorMode::LenientCacheRead).await
24}
25
26/// v1.4.109: strict 版 query_orders for write/push safety paths.
27///
28/// 与 lenient 版区别: decode 失败 / `result != 0` → 返 `Err` (会被 retry 重试).
29/// 不再 silent 当 "Ok(empty)" → silent-success 反模式 D (CLAUDE.md #45) 的修复.
30///
31/// Callers that need a trustworthy order baseline before mutating order state
32/// must use this strict variant, not the read-path lenient cache-miss helper.
33pub async fn query_orders_strict(
34    backend: &BackendConn,
35    acc_id: u64,
36    trd_cache: &TrdCache,
37) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
38    query_orders_inner(
39        backend,
40        acc_id,
41        trd_cache,
42        QueryErrorMode::StrictPushRefresh,
43    )
44    .await
45}
46
47/// v1.4.106 codex 0932 F3 [P2]: strict 版 query_orders for push-refresh path.
48///
49/// **4 attempts 全失败**: caller (dispatcher) 应触发 F4 `record_f2_exhausted` +
50/// log warn. 不再 silent 当成功流转.
51pub async fn query_orders_strict_for_push_refresh(
52    backend: &BackendConn,
53    acc_id: u64,
54    trd_cache: &TrdCache,
55) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
56    query_orders_strict(backend, acc_id, trd_cache).await
57}
58
59pub async fn query_orders_inner(
60    backend: &BackendConn,
61    acc_id: u64,
62    trd_cache: &TrdCache,
63    error_mode: QueryErrorMode,
64) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
65    use futu_cache::trd_cache::CachedOrder;
66    use prost::Message;
67
68    // v1.4.106 codex 0955 F4: req shape 对齐 C++ FillOrderListReq.
69    // v1.4.106 14708 audit: C++ QueryOrderList_Inner sim 分支使用
70    // sim_order_sys_interface::OrderListReq + registry Orders.sim_cmd。
71    let (trd_env, enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
72    let real_req = build_order_list_req_base(acc_id, trd_env, &enabled_markets);
73    let sim_req = build_sim_order_list_req_base(acc_id, &enabled_markets);
74
75    let mut all_orders = Vec::new();
76    let mut page_flag: Option<String> = None;
77    // v1.4.106 codex 0955 F5: 显式 track completion. partial pagination
78    // (completed=false 但 page_flag 缺失 / 超 max_pages 未 completed) 必须
79    // 返 Err, 不能写 cache (stub orders 会被 partial 列表抹掉, 历史教训
80    // 见坑 #44 v1.4.73-90 saga).
81    const MAX_PAGES: usize = 100;
82    let mut completed = false;
83
84    for _page in 0..MAX_PAGES {
85        let spec = trade_query_command(TradeQueryOperation::Orders);
86        let (cmd_id, req_body) = if trd_env == 1 {
87            let mut paged_req = real_req.clone();
88            paged_req.page_flag = page_flag.clone();
89            (spec.real_cmd, paged_req.encode_to_vec())
90        } else {
91            let mut paged_req = sim_req.clone();
92            paged_req.page_flag = page_flag.clone();
93            (spec.sim_cmd, paged_req.encode_to_vec())
94        };
95
96        let resp = match backend.request(cmd_id, req_body).await {
97            Ok(r) => r,
98            Err(e) => {
99                tracing::debug!(acc_id, cmd_id, error = %e, "order query failed");
100                return Err(e);
101            }
102        };
103
104        let parsed: order_sys_interface::OrderListRsp = match Message::decode(resp.body.as_ref()) {
105            Ok(p) => p,
106            Err(e) => {
107                // v1.4.106 codex 0932 F3 [P2]: strict 模式 → 返 Err 让上层 retry,
108                // 不 silent 当 Ok(empty). lenient (read path) 保留老行为.
109                if error_mode.is_strict() {
110                    tracing::warn!(
111                        acc_id,
112                        error = %e,
113                        "v1.4.106 F3 strict: order response decode failed → \
114                         returning Err to trigger retry (push-refresh path)"
115                    );
116                    return Err(futu_core::error::FutuError::from(e));
117                }
118                tracing::debug!(acc_id, error = %e, "order response decode failed (lenient)");
119                return Ok(vec![]);
120            }
121        };
122
123        if let Err(status_err) = backend_order_list_status_like_cpp(
124            parsed.result,
125            parsed.msg_header.as_ref(),
126            parsed.err_msg.as_deref(),
127            acc_id,
128        ) {
129            // v1.4.110: C++ CheckRspHeaderAndGetSvrRet rejects missing or
130            // mismatched msg_header.account_id. Treat structural response
131            // issues as retryable even on lenient read paths so they cannot
132            // masquerade as a legitimate empty order snapshot.
133            if error_mode.is_strict() || !status_err.is_backend_error {
134                tracing::warn!(
135                    acc_id,
136                    result = status_err.result,
137                    error = %status_err.message,
138                    "v1.4.110: order query status/header invalid -> returning Err \
139                     to trigger retry (push-refresh path)"
140                );
141                return Err(futu_core::error::FutuError::ServerError {
142                    ret_type: status_err.result,
143                    msg: format!("{} (acc_id={acc_id})", status_err.message),
144                });
145            }
146            return Ok(vec![]);
147        }
148
149        let pf = |s: &Option<String>| -> f64 {
150            s.as_ref()
151                .and_then(|v| v.parse::<f64>().ok())
152                .unwrap_or(0.0)
153        };
154
155        for o in &parsed.orders {
156            let Some(trd_market) = backend_order_unpacked_trd_market_like_cpp(trd_env, o) else {
157                continue;
158            };
159            let order_id_ex = o.order_id.clone().unwrap_or_default();
160            // v1.4.106 codex 0226 F6: backend `order_id` (= szOrderID) 是 alphanumeric 字符串,
161            // 不是 numeric. C++ `NNProto_Trd_OrderOp.cpp:469` 用 `HashStrToU64(szOrderID)`
162            // 派生 hash 作 FTAPI 客户端 `Trd_Common.OrderItem.orderID`.
163            // 老实现 `order_id_ex.parse().unwrap_or(0)` 对 alphanumeric backend id
164            // silent 返 0 → FTAPI client 看到 orderID=0 → 任何按 (acc_id, order_id)
165            // 在 cache 中查找的逻辑 (modify / cancel) 全部失败 silent fall-through.
166            // 修法: 用 `hash_str_to_u64(&order_id_ex)` 派生 hash, 与 gateway place_order
167            // stub 写 cache 时用的 hash 一致.
168            let order_id: u64 = if order_id_ex.is_empty() {
169                0
170            } else {
171                hash_str_to_u64(&order_id_ex)
172            };
173            let create_timestamp = o.create_time.map(|t| t as f64 / 1_000_000.0);
174            let update_timestamp = o.update_time.map(|t| t as f64 / 1_000_000.0);
175            // 后端 OrderStatus → FTAPI OrderStatus 映射
176            let order_status = backend_order_status_to_ftapi(o.status.unwrap_or(0));
177            // 后端 Side → FTAPI TrdSide: 1=BUY, 2=SELL
178            let trd_side = o.side.unwrap_or(0) as i32;
179            let security_type = o.security_type.map(|v| v as i32);
180            // 后端 OrderType → FTAPI OrderType(对齐 C++ Trd_OrderTypeConv_S2C)
181            let order_type =
182                backend_order_type_to_ftapi(o.r#type.unwrap_or(0), trd_market, security_type);
183
184            // v1.4.106 codex 0219 Finding 4: backend snapshot 字段 (供 modify /
185            // cancel handler 用 — order_version / exchange_code / exchange /
186            // security_type 必填 backend req).
187            let backend_order_id = order_id_ex.clone();
188            let order_version = o.version.unwrap_or(0) as i32;
189            let exchange_code = o.exchange_code.unwrap_or(0) as i32;
190            let exchange = o.exchange.clone().unwrap_or_default();
191            let security_type = security_type.unwrap_or(0);
192
193            // v1.4.106 codex F7 (P2): backend `Order.text` (field 25) 在 OpenD
194            // 客户端写侧被包为 `OD|<localID>|<userRemark>` (见
195            // `futu-gateway::handlers::trd::translate::build_open_d_text`).
196            // 读侧反向解析: 命中 OD| 前缀 → 取 user_remark 部分; 否则
197            // (老订单 / 其他客户端 free-form text) 整段透传到 remark, 让
198            // 用户能看到原 raw text.
199            let extracted_remark = match parse_open_d_text(o.text.as_deref()) {
200                Some((_local_id, user_remark)) => Some(user_remark),
201                None => o.text.clone(),
202            };
203            all_orders.push(CachedOrder {
204                order_id,
205                order_id_ex,
206                code: o.symbol.as_ref().cloned().unwrap_or_default(),
207                name: o.stock_name.as_ref().cloned().unwrap_or_default(),
208                trd_side,
209                order_type,
210                order_status,
211                qty: pf(&o.qty),
212                price: pf(&o.price),
213                fill_qty: pf(&o.cum_qty),
214                fill_avg_price: pf(&o.avg_fill_price),
215                create_time: String::new(),
216                update_time: String::new(),
217                last_err_msg: o.last_err_msg.clone(),
218                sec_market: None,
219                create_timestamp,
220                update_timestamp,
221                remark: extracted_remark,
222                time_in_force: None,
223                fill_outside_rth: None,
224                aux_price: None,
225                trail_type: None,
226                trail_value: None,
227                trail_spread: None,
228                currency: o.currency.map(|c| c as i32),
229                trd_market,
230                // v1.4.106 codex 0219 Finding 4: backend snapshot 字段 (用于
231                // ModifyOrder / CancelOrder backend req 必填字段).
232                backend_order_id,
233                order_version,
234                exchange_code,
235                exchange,
236                security_type,
237                // v1.4.98 T1-8: session / jp_acc_type — backend OrderItem proto
238                // 现状不返这两 field (mobile App 实际有, 但 trade_query backend
239                // ParsedOrder 还没接). 留 None 让客户端识别 unknown vs no-data.
240                // 未来可加 backend 字段后填.
241                session: None,
242                // C++ `NNProto_Trd_MaxQty::QueryOptionIM` forwards cached
243                // `order.enOrderTradeTimeType` when `GetMaxTrdQtys` is called for
244                // a modify-order path. Backend order-list exposes the value under
245                // `Order.sup.order_trade_time_type` (odr_sys_cmn.proto:704).
246                order_trade_time_type: o.sup.as_ref().and_then(|s| s.order_trade_time_type),
247                jp_acc_type: None,
248                // v1.4.90 S BUG-e4da-009: backend authoritative orders are NOT stubs.
249                // `merge_preserving_stubs` 会再 reset 一次(防御),这里就近声明语义。
250                is_stub: false,
251                stub_inserted_at_ms: 0,
252                // v1.4.105 BUG-v1.4.104-001 (P0): backend authoritative list 必然是
253                // broker confirmed 的订单 (broker async confirm 后 backend 才把 order
254                // 写进 OrderListReq response). 强制 false.
255                is_pending_broker_confirm: false,
256            });
257        }
258
259        // v1.4.106 codex 0955 F5: 显式分三档处理:
260        // - completed=true → 全部拉完, 写 cache.
261        // - completed=false + page_flag 非空 → 继续 paginate.
262        // - completed=false + page_flag 空/缺 → partial failure, 不写 cache, 返 Err.
263        let parsed_completed = parsed.completed.unwrap_or(false);
264        if parsed_completed {
265            completed = true;
266            break;
267        }
268        match parsed.page_flag {
269            Some(ref pf) if !pf.is_empty() => {
270                page_flag = Some(pf.clone());
271            }
272            _ => {
273                // partial + page_flag 缺 → 不能继续, backend 状态机异常.
274                tracing::warn!(
275                    acc_id,
276                    partial_count = all_orders.len(),
277                    "v1.4.106 codex 0955 F5: backend completed=false 但 page_flag \
278                     缺失/空 — partial response, 不写 cache 防 stub 被抹"
279                );
280                return Err(futu_core::error::FutuError::Codec(
281                    "query_orders: partial response (completed=false + missing page_flag)"
282                        .to_string(),
283                ));
284            }
285        }
286    }
287
288    // v1.4.106 codex 0955 F5: 超 MAX_PAGES 仍未 completed → partial, 不写 cache.
289    if !completed {
290        tracing::warn!(
291            acc_id,
292            max_pages = MAX_PAGES,
293            partial_count = all_orders.len(),
294            "v1.4.106 codex 0955 F5: pagination 超 MAX_PAGES 仍未 completed — \
295             partial response, 不写 cache 防 stub 被抹"
296        );
297        return Err(futu_core::error::FutuError::Codec(format!(
298            "query_orders: pagination exceeded {MAX_PAGES} pages without completed=true"
299        )));
300    }
301
302    tracing::debug!(acc_id, count = all_orders.len(), "orders queried");
303    // v1.4.93 S3 BUG-5318-009 真修:用 merge_preserving_stubs 代替 update_orders
304    // (= orders.insert 整覆盖)。
305    //
306    // 历史坑(v1.4.90 S BUG-e4da-009 fix 漏修这一层):
307    // - v1.4.90 把 place_order.rs / modify_order.rs 三个 *outer* callsite 改成
308    //   merge,但 query_orders 内部仍 update_orders → orders.insert 整覆盖。
309    // - 实际执行顺序:
310    //   1. PlaceOrder handler upsert stub → cache: [stub]
311    //   2. spawn task 调 query_orders →
312    //   3. backend 返 vec![] (just-placed not yet authoritative) →
313    //   4. *trd_cache.update_orders(acc_id, vec![])* → cache: []  ← STUB 被抹
314    //   5. query_orders 返 Ok(vec![]) →
315    //   6. outer caller cache.merge_preserving_stubs(acc_id, vec![]) →
316    //      existing_stubs = [] → cache 仍 []  ← merge 来晚了
317    //   7. log "merged ... count=0"
318    // - 端到端:stub 在 spawn 内被 query_orders 自己抹掉,外层 merge 是 dead code
319    //   from the stub's perspective.
320    //
321    // 真机证据(claude-5318 v1.4.90 real-money US.F PlaceOrder, 2026-04-25):
322    // - daemon log: place_order.rs:436 "stub upsert (order_id=15250071496077083016)"
323    // - daemon log: place_order.rs:502 "merge_preserving_stubs ... count=0"
324    // - /api/orders 0/5/30/60s 全空
325    //
326    // 修法:把 update_orders(仅 caller = 此处)改为 merge_preserving_stubs。
327    // - 仍保持 query_orders 的"populate cache as side-effect"语义
328    // - 但 backend 的权威列表与 fresh stub 共存(< 30s TTL)
329    // - 外层 callsite 的 merge_preserving_stubs 现在是幂等冗余(无害)
330    //
331    // 对 dispatcher.rs:228 push handler 调用:仅迭代返回的 Vec,不依赖 cache
332    // 状态,行为不变。
333    //
334    // 同坑 #44 "同 bug 跨版本 regression 链": v1.4.73→89 7 版 + v1.4.90 8th 版的
335    // "每版补一层没补根因"。本版(v1.4.93 S3)真补到 inner helper 这一层。
336    trd_cache.merge_preserving_stubs(acc_id, all_orders.clone());
337    Ok(all_orders)
338}