1use futu_cache::trd_cache::TrdCache;
5use futu_core::error::{FutuError, 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, sim_odr_sys_cmn, sim_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
26pub 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
47pub 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
59fn require_real_cipher_for_strict_trade_query(
60 acc_id: u64,
61 trd_cache: &TrdCache,
62 op_name: &str,
63) -> Result<Vec<u8>> {
64 let cipher = trd_cache.get_cipher(acc_id).unwrap_or_default();
65 if cipher.is_empty() {
66 return Err(FutuError::ServerError {
67 ret_type: -401,
68 msg: format!(
69 "{op_name}: real account cipher missing; trade is not unlocked (acc_id={acc_id})"
70 ),
71 });
72 }
73 Ok(cipher)
74}
75
76pub fn order_proto_to_cached_like_cpp(
77 trd_env: i32,
78 o: &crate::proto_internal::odr_sys_cmn::Order,
79) -> Option<futu_cache::trd_cache::CachedOrder> {
80 order_proto_to_cached_with_jp_acc_type_like_cpp(trd_env, o, None)
81}
82
83pub fn order_proto_to_cached_with_jp_acc_type_like_cpp(
84 trd_env: i32,
85 o: &crate::proto_internal::odr_sys_cmn::Order,
86 jp_acc_type: Option<i32>,
87) -> Option<futu_cache::trd_cache::CachedOrder> {
88 use futu_cache::trd_cache::CachedOrder;
89
90 let trd_market = backend_order_unpacked_trd_market_like_cpp(trd_env, o)?;
91 let order_id_ex = o.order_id.clone().unwrap_or_default();
92 let order_id: u64 = if order_id_ex.is_empty() {
95 0
96 } else {
97 hash_str_to_u64(&order_id_ex)
98 };
99 let create_timestamp = o.create_time.map(|t| t as f64 / 1_000_000.0);
100 let update_timestamp = o.update_time.map(|t| t as f64 / 1_000_000.0);
101 let order_status = backend_order_status_to_ftapi(o.status.unwrap_or(0));
102 let trd_side = o.side.unwrap_or(0) as i32;
103 let security_type = o.security_type.map(|v| v as i32);
104 let order_type = backend_order_type_to_ftapi(o.r#type.unwrap_or(0), trd_market, security_type);
105 let backend_order_id = order_id_ex.clone();
106 let order_version = o.version.unwrap_or(0) as i32;
107 let exchange_code = o.exchange_code.unwrap_or(0) as i32;
108 let exchange = o.exchange.clone().unwrap_or_default();
109 let security_type = security_type.unwrap_or(0);
110 let extracted_remark = match parse_open_d_text(o.text.as_deref()) {
111 Some((_local_id, user_remark)) => Some(user_remark),
112 None => o.text.clone(),
113 };
114
115 let order_trade_time_type = o.sup.as_ref().and_then(|s| s.order_trade_time_type);
116 let (strategy_type, combo_legs) = o
117 .multi_leg_info
118 .as_ref()
119 .map(project_order_multi_leg_info_to_cached_like_cpp)
120 .unwrap_or_default();
121
122 Some(CachedOrder {
123 order_id,
124 order_id_ex,
125 code: o.symbol.as_ref().cloned().unwrap_or_default(),
126 name: o.stock_name.as_ref().cloned().unwrap_or_default(),
127 trd_side,
128 order_type,
129 order_status,
130 qty: parse_backend_decimal(&o.qty),
131 price: parse_backend_decimal(&o.price),
132 fill_qty: parse_backend_decimal(&o.cum_qty),
133 fill_avg_price: parse_backend_decimal(&o.avg_fill_price),
134 create_time: String::new(),
135 update_time: String::new(),
136 last_err_msg: o.last_err_msg.clone(),
137 sec_market: None,
138 create_timestamp,
139 update_timestamp,
140 remark: extracted_remark,
141 time_in_force: None,
142 fill_outside_rth: None,
143 aux_price: None,
144 trail_type: None,
145 trail_value: None,
146 trail_spread: None,
147 currency: o.currency.map(|c| c as i32),
148 trd_market,
149 strategy_type,
150 combo_legs,
151 backend_order_id,
152 order_version,
153 exchange_code,
154 exchange,
155 security_type,
156 session: futu_trd::projection::order_trade_time_type_to_session(order_trade_time_type),
157 order_trade_time_type,
158 jp_acc_type,
159 is_stub: false,
160 is_local_order: false,
161 stub_inserted_at_ms: 0,
162 is_pending_broker_confirm: false,
163 })
164}
165
166fn project_order_multi_leg_info_to_cached_like_cpp(
167 multi_leg: &crate::proto_internal::odr_sys_cmn::MultiLegInfo,
168) -> (Option<i32>, Vec<futu_proto::qot_common::ComboLeg>) {
169 let strategy_type = multi_leg
177 .strategy_type
178 .and_then(combo_strategy_type_s2c_to_api_like_cpp);
179 let combo_legs = multi_leg
180 .leg_infos
181 .iter()
182 .map(combo_leg_data_s2c_to_api_like_cpp)
183 .collect();
184 (strategy_type, combo_legs)
185}
186
187fn combo_strategy_type_s2c_to_api_like_cpp(strategy_type: u32) -> Option<i32> {
188 let api = match strategy_type {
189 1 => futu_proto::qot_common::OptionStrategyType::SingleOption,
190 2 | 3 => futu_proto::qot_common::OptionStrategyType::Covered,
191 4 => futu_proto::qot_common::OptionStrategyType::Spread,
192 6 => futu_proto::qot_common::OptionStrategyType::Straddle,
193 7 => futu_proto::qot_common::OptionStrategyType::Strangle,
194 8 => futu_proto::qot_common::OptionStrategyType::Collar,
195 9 => futu_proto::qot_common::OptionStrategyType::Butterfly,
196 11 => futu_proto::qot_common::OptionStrategyType::Condor,
197 13 => futu_proto::qot_common::OptionStrategyType::IronButterfly,
198 14 => futu_proto::qot_common::OptionStrategyType::IronCondor,
199 15 => futu_proto::qot_common::OptionStrategyType::CalendarSpread,
200 16 => futu_proto::qot_common::OptionStrategyType::DiagonalSpread,
201 17..=19 => futu_proto::qot_common::OptionStrategyType::Customize,
202 _ => futu_proto::qot_common::OptionStrategyType::Unknown,
203 };
204 (api != futu_proto::qot_common::OptionStrategyType::Unknown).then_some(api as i32)
205}
206
207fn combo_leg_data_s2c_to_api_like_cpp(
208 leg: &crate::proto_internal::odr_sys_cmn::OrderLegInfo,
209) -> futu_proto::qot_common::ComboLeg {
210 futu_proto::qot_common::ComboLeg {
211 security: futu_proto::qot_common::Security {
212 market: exchange_to_qot_market_like_cpp(
213 leg.leg_exchange.as_deref().unwrap_or_default(),
214 ),
215 code: leg.leg_symbol.clone().unwrap_or_default(),
216 },
217 side: leg.leg_side.map(|v| v as i32),
218 qty_ratio: leg
219 .leg_ratio_qty
220 .as_deref()
221 .and_then(|v| v.parse::<f64>().ok()),
222 position_id: leg.position_id.as_deref().map(hash_str_to_u64),
223 }
224}
225
226fn exchange_to_qot_market_like_cpp(exchange: &str) -> i32 {
227 match exchange {
228 "SEHK" | "HKFE" => futu_proto::qot_common::QotMarket::HkSecurity as i32,
230 "US" | "NYMEX" | "COMEX" | "CBOT" | "CME" | "CBOE" => {
231 futu_proto::qot_common::QotMarket::UsSecurity as i32
232 }
233 "SSE" => futu_proto::qot_common::QotMarket::CnshSecurity as i32,
234 "SZSE" => futu_proto::qot_common::QotMarket::CnszSecurity as i32,
235 "SGX" => futu_proto::qot_common::QotMarket::SgSecurity as i32,
236 "OSE" | "JP" => futu_proto::qot_common::QotMarket::JpSecurity as i32,
237 "CRYPTO" => futu_proto::qot_common::QotMarket::CcSecurity as i32,
238 "BMS" => futu_proto::qot_common::QotMarket::MySecurity as i32,
239 _ => futu_proto::qot_common::QotMarket::Unknown as i32,
240 }
241}
242
243pub(super) fn sim_order_proto_to_cached_like_cpp(
244 trd_env: i32,
245 o: &sim_odr_sys_cmn::Order,
246) -> Option<futu_cache::trd_cache::CachedOrder> {
247 use futu_cache::trd_cache::CachedOrder;
248
249 o.order_id.as_ref()?;
250 o.side?;
251 let trd_market = if trd_env == 1 {
252 let trd_market = backend_real_market_to_trd_market_like_cpp(o.market?);
253 if !is_valid_real_trd_market_like_cpp(trd_market) {
254 return None;
255 }
256 Some(trd_market)
257 } else {
258 o.market.map(map_backend_market_to_trd_market)
259 };
260 o.symbol.as_ref()?;
261 o.create_time?;
262 o.expire_time?;
263 o.status?;
264
265 let order_id_ex = o.order_id.clone().unwrap_or_default();
266 let order_id: u64 = if order_id_ex.is_empty() {
267 0
268 } else {
269 hash_str_to_u64(&order_id_ex)
270 };
271 let create_timestamp = o.create_time.map(|t| t as f64 / 1_000_000.0);
272 let update_timestamp = o.update_time.map(|t| t as f64 / 1_000_000.0);
273 let order_status = backend_order_status_to_ftapi(o.status.unwrap_or(0));
274 let trd_side = o.side.unwrap_or(0) as i32;
275 let security_type = o.security_type.map(|v| v as i32);
276 let order_type = backend_order_type_to_ftapi(o.r#type.unwrap_or(0), trd_market, security_type);
277 let backend_order_id = order_id_ex.clone();
278 let order_version = o.version.unwrap_or(0) as i32;
279 let exchange_code = o.exchange_code.unwrap_or(0) as i32;
280 let exchange = o.exchange.clone().unwrap_or_default();
281 let security_type = security_type.unwrap_or(0);
282 let extracted_remark = match parse_open_d_text(o.text.as_deref()) {
283 Some((_local_id, user_remark)) => Some(user_remark),
284 None => o.text.clone(),
285 };
286
287 let order_trade_time_type = o.sup.as_ref().and_then(|s| s.order_trade_time_type);
288
289 Some(CachedOrder {
290 order_id,
291 order_id_ex,
292 code: o.symbol.as_ref().cloned().unwrap_or_default(),
293 name: o.stock_name.as_ref().cloned().unwrap_or_default(),
294 trd_side,
295 order_type,
296 order_status,
297 qty: parse_backend_decimal(&o.qty),
298 price: parse_backend_decimal(&o.price),
299 fill_qty: parse_backend_decimal(&o.cum_qty),
300 fill_avg_price: parse_backend_decimal(&o.avg_fill_price),
301 create_time: String::new(),
302 update_time: String::new(),
303 last_err_msg: o.last_err_msg.clone(),
304 sec_market: None,
305 create_timestamp,
306 update_timestamp,
307 remark: extracted_remark,
308 time_in_force: None,
309 fill_outside_rth: None,
310 aux_price: None,
311 trail_type: None,
312 trail_value: None,
313 trail_spread: None,
314 currency: o.currency.map(|c| c as i32),
315 trd_market,
316 strategy_type: None,
317 combo_legs: Vec::new(),
318 backend_order_id,
319 order_version,
320 exchange_code,
321 exchange,
322 security_type,
323 session: futu_trd::projection::order_trade_time_type_to_session(order_trade_time_type),
324 order_trade_time_type,
325 jp_acc_type: o.sub_account.map(|v| v as i32),
326 is_stub: false,
327 is_local_order: false,
328 stub_inserted_at_ms: 0,
329 is_pending_broker_confirm: false,
330 })
331}
332
333pub async fn query_order_info_strict_for_push_refresh(
341 backend: &BackendConn,
342 acc_id: u64,
343 trd_cache: &TrdCache,
344 order_ids: &[String],
345 security_type: Option<u32>,
346 exchange: Option<&str>,
347) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
348 use prost::Message;
349
350 let filtered_ids: Vec<String> = order_ids
351 .iter()
352 .filter(|id| !id.is_empty())
353 .cloned()
354 .collect();
355 if filtered_ids.is_empty() {
356 return Ok(vec![]);
357 }
358
359 let (trd_env, _account_trd_market, _enabled_markets) =
360 derive_acc_query_context(trd_cache, acc_id);
361 let cmd_id = order_info_cmd_for_env(trd_env);
362 let real_cipher = if trd_env == 1 {
363 require_real_cipher_for_strict_trade_query(acc_id, trd_cache, "QueryOrderInfo")?
364 } else {
365 Vec::new()
366 };
367 let req =
368 build_order_info_req_base(acc_id, security_type, exchange, &filtered_ids, real_cipher);
369 let resp = backend.request(cmd_id, req.encode_to_vec()).await?;
370 let (orders, received): (Vec<futu_cache::trd_cache::CachedOrder>, usize) = if trd_env == 1 {
371 let parsed: order_sys_interface::OrderDetailRsp = Message::decode(resp.body.as_ref())?;
372 if let Err(status_err) = backend_order_info_status_like_cpp(
373 parsed.result,
374 parsed.msg_header.as_ref(),
375 parsed.err_msg.as_deref(),
376 acc_id,
377 filtered_ids.len(),
378 parsed.orders.len(),
379 ) {
380 return Err(futu_core::error::FutuError::ServerError {
381 ret_type: status_err.result,
382 msg: format!("{} (acc_id={acc_id})", status_err.message),
383 });
384 }
385 let received = parsed.orders.len();
386 let orders = parsed
387 .orders
388 .iter()
389 .filter_map(|order| {
390 let jp_acc_type = order
391 .sub_account_id
392 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
393 order_proto_to_cached_with_jp_acc_type_like_cpp(trd_env, order, jp_acc_type)
394 })
395 .collect();
396 (orders, received)
397 } else {
398 let parsed: sim_order_sys_interface::OrderDetailRsp = Message::decode(resp.body.as_ref())?;
399 if let Err(status_err) = backend_order_info_status_by_account_like_cpp(
400 parsed.result,
401 parsed
402 .msg_header
403 .as_ref()
404 .and_then(|header| header.account_id),
405 parsed.err_msg.as_deref(),
406 acc_id,
407 filtered_ids.len(),
408 parsed.orders.len(),
409 ) {
410 return Err(futu_core::error::FutuError::ServerError {
411 ret_type: status_err.result,
412 msg: format!("{} (acc_id={acc_id})", status_err.message),
413 });
414 }
415 let received = parsed.orders.len();
416 let orders = parsed
417 .orders
418 .iter()
419 .filter_map(|order| sim_order_proto_to_cached_like_cpp(trd_env, order))
420 .collect();
421 (orders, received)
422 };
423 let mut applied_orders = Vec::with_capacity(orders.len());
424 for order in orders {
425 if trd_cache.upsert_order(acc_id, order.clone()) {
426 applied_orders.push(order);
427 }
428 }
429 tracing::debug!(
430 acc_id,
431 cmd_id,
432 requested = filtered_ids.len(),
433 received,
434 applied = applied_orders.len(),
435 "order details queried"
436 );
437 Ok(applied_orders)
438}
439
440pub async fn query_orders_inner(
441 backend: &BackendConn,
442 acc_id: u64,
443 trd_cache: &TrdCache,
444 error_mode: QueryErrorMode,
445) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
446 use prost::Message;
447
448 let (trd_env, account_trd_market, enabled_markets) =
452 derive_acc_query_context(trd_cache, acc_id);
453 let real_cipher = if trd_env == 1 && error_mode.is_strict() {
459 require_real_cipher_for_strict_trade_query(acc_id, trd_cache, "QueryOrderList")?
460 } else {
461 trd_cache.get_cipher(acc_id).unwrap_or_default()
462 };
463 let real_req = build_order_list_req_base(
464 acc_id,
465 trd_env,
466 account_trd_market,
467 &enabled_markets,
468 real_cipher,
469 );
470 let sim_req = build_sim_order_list_req_base(acc_id, &enabled_markets);
471
472 let mut all_orders = Vec::new();
473 let mut page_flag: Option<String> = Some(String::new());
476 const MAX_PAGES: usize = 100;
481 let mut completed = false;
482
483 for _page in 0..MAX_PAGES {
484 let spec = trade_query_command(TradeQueryOperation::Orders);
485 let (cmd_id, req_body) = if trd_env == 1 {
486 let mut paged_req = real_req.clone();
487 paged_req.page_flag = page_flag.clone();
488 (spec.real_cmd, paged_req.encode_to_vec())
489 } else {
490 let mut paged_req = sim_req.clone();
491 paged_req.page_flag = page_flag.clone();
492 (spec.sim_cmd, paged_req.encode_to_vec())
493 };
494
495 let resp = match backend.request(cmd_id, req_body).await {
496 Ok(r) => r,
497 Err(e) => {
498 tracing::debug!(acc_id, cmd_id, error = %e, "order query failed");
499 return Err(e);
500 }
501 };
502
503 let parsed: order_sys_interface::OrderListRsp = match Message::decode(resp.body.as_ref()) {
504 Ok(p) => p,
505 Err(e) => {
506 if error_mode.is_strict() {
509 tracing::warn!(
510 acc_id,
511 error = %e,
512 "v1.4.106 F3 strict: order response decode failed → \
513 returning Err to trigger retry (push-refresh path)"
514 );
515 return Err(futu_core::error::FutuError::from(e));
516 }
517 tracing::debug!(acc_id, error = %e, "order response decode failed (lenient)");
518 return Ok(vec![]);
519 }
520 };
521
522 if let Err(status_err) = backend_order_list_status_like_cpp(
523 parsed.result,
524 parsed.msg_header.as_ref(),
525 parsed.err_msg.as_deref(),
526 acc_id,
527 ) {
528 if error_mode.is_strict() || !status_err.is_backend_error {
533 tracing::warn!(
534 acc_id,
535 result = status_err.result,
536 error = %status_err.message,
537 "v1.4.110: order query status/header invalid -> returning Err \
538 to trigger retry (push-refresh path)"
539 );
540 return Err(futu_core::error::FutuError::ServerError {
541 ret_type: status_err.result,
542 msg: format!("{} (acc_id={acc_id})", status_err.message),
543 });
544 }
545 return Ok(vec![]);
546 }
547
548 for o in &parsed.orders {
549 let jp_acc_type = o
550 .sub_account_id
551 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
552 if let Some(order) =
553 order_proto_to_cached_with_jp_acc_type_like_cpp(trd_env, o, jp_acc_type)
554 {
555 all_orders.push(order);
556 }
557 }
558
559 let parsed_completed = parsed.completed.unwrap_or(false);
564 if parsed_completed {
565 completed = true;
566 break;
567 }
568 match parsed.page_flag {
569 Some(ref pf) if !pf.is_empty() => {
570 page_flag = Some(pf.clone());
571 }
572 _ => {
573 tracing::warn!(
575 acc_id,
576 partial_count = all_orders.len(),
577 "v1.4.106 audit 0955 F5: backend completed=false 但 page_flag \
578 缺失/空 — partial response, 不写 cache 防 stub 被抹"
579 );
580 return Err(futu_core::error::FutuError::Codec(
581 "query_orders: partial response (completed=false + missing page_flag)"
582 .to_string(),
583 ));
584 }
585 }
586 }
587
588 if !completed {
590 tracing::warn!(
591 acc_id,
592 max_pages = MAX_PAGES,
593 partial_count = all_orders.len(),
594 "v1.4.106 audit 0955 F5: pagination 超 MAX_PAGES 仍未 completed — \
595 partial response, 不写 cache 防 stub 被抹"
596 );
597 return Err(futu_core::error::FutuError::Codec(format!(
598 "query_orders: pagination exceeded {MAX_PAGES} pages without completed=true"
599 )));
600 }
601
602 tracing::debug!(acc_id, count = all_orders.len(), "orders queried");
603 trd_cache.merge_preserving_stubs(acc_id, all_orders.clone());
637 Ok(all_orders)
638}