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}