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
117 Some(CachedOrder {
118 order_id,
119 order_id_ex,
120 code: o.symbol.as_ref().cloned().unwrap_or_default(),
121 name: o.stock_name.as_ref().cloned().unwrap_or_default(),
122 trd_side,
123 order_type,
124 order_status,
125 qty: parse_backend_decimal(&o.qty),
126 price: parse_backend_decimal(&o.price),
127 fill_qty: parse_backend_decimal(&o.cum_qty),
128 fill_avg_price: parse_backend_decimal(&o.avg_fill_price),
129 create_time: String::new(),
130 update_time: String::new(),
131 last_err_msg: o.last_err_msg.clone(),
132 sec_market: None,
133 create_timestamp,
134 update_timestamp,
135 remark: extracted_remark,
136 time_in_force: None,
137 fill_outside_rth: None,
138 aux_price: None,
139 trail_type: None,
140 trail_value: None,
141 trail_spread: None,
142 currency: o.currency.map(|c| c as i32),
143 trd_market,
144 backend_order_id,
145 order_version,
146 exchange_code,
147 exchange,
148 security_type,
149 session: futu_trd::projection::order_trade_time_type_to_session(order_trade_time_type),
150 order_trade_time_type,
151 jp_acc_type,
152 is_stub: false,
153 is_local_order: false,
154 stub_inserted_at_ms: 0,
155 is_pending_broker_confirm: false,
156 })
157}
158
159pub(super) fn sim_order_proto_to_cached_like_cpp(
160 trd_env: i32,
161 o: &sim_odr_sys_cmn::Order,
162) -> Option<futu_cache::trd_cache::CachedOrder> {
163 use futu_cache::trd_cache::CachedOrder;
164
165 o.order_id.as_ref()?;
166 o.side?;
167 let trd_market = if trd_env == 1 {
168 let trd_market = backend_real_market_to_trd_market_like_cpp(o.market?);
169 if !is_valid_real_trd_market_like_cpp(trd_market) {
170 return None;
171 }
172 Some(trd_market)
173 } else {
174 o.market.map(map_backend_market_to_trd_market)
175 };
176 o.symbol.as_ref()?;
177 o.create_time?;
178 o.status?;
179
180 let order_id_ex = o.order_id.clone().unwrap_or_default();
181 let order_id: u64 = if order_id_ex.is_empty() {
182 0
183 } else {
184 hash_str_to_u64(&order_id_ex)
185 };
186 let create_timestamp = o.create_time.map(|t| t as f64 / 1_000_000.0);
187 let update_timestamp = o.update_time.map(|t| t as f64 / 1_000_000.0);
188 let order_status = backend_order_status_to_ftapi(o.status.unwrap_or(0));
189 let trd_side = o.side.unwrap_or(0) as i32;
190 let security_type = o.security_type.map(|v| v as i32);
191 let order_type = backend_order_type_to_ftapi(o.r#type.unwrap_or(0), trd_market, security_type);
192 let backend_order_id = order_id_ex.clone();
193 let order_version = o.version.unwrap_or(0) as i32;
194 let exchange_code = o.exchange_code.unwrap_or(0) as i32;
195 let exchange = o.exchange.clone().unwrap_or_default();
196 let security_type = security_type.unwrap_or(0);
197 let extracted_remark = match parse_open_d_text(o.text.as_deref()) {
198 Some((_local_id, user_remark)) => Some(user_remark),
199 None => o.text.clone(),
200 };
201
202 let order_trade_time_type = o.sup.as_ref().and_then(|s| s.order_trade_time_type);
203
204 Some(CachedOrder {
205 order_id,
206 order_id_ex,
207 code: o.symbol.as_ref().cloned().unwrap_or_default(),
208 name: o.stock_name.as_ref().cloned().unwrap_or_default(),
209 trd_side,
210 order_type,
211 order_status,
212 qty: parse_backend_decimal(&o.qty),
213 price: parse_backend_decimal(&o.price),
214 fill_qty: parse_backend_decimal(&o.cum_qty),
215 fill_avg_price: parse_backend_decimal(&o.avg_fill_price),
216 create_time: String::new(),
217 update_time: String::new(),
218 last_err_msg: o.last_err_msg.clone(),
219 sec_market: None,
220 create_timestamp,
221 update_timestamp,
222 remark: extracted_remark,
223 time_in_force: None,
224 fill_outside_rth: None,
225 aux_price: None,
226 trail_type: None,
227 trail_value: None,
228 trail_spread: None,
229 currency: o.currency.map(|c| c as i32),
230 trd_market,
231 backend_order_id,
232 order_version,
233 exchange_code,
234 exchange,
235 security_type,
236 session: futu_trd::projection::order_trade_time_type_to_session(order_trade_time_type),
237 order_trade_time_type,
238 jp_acc_type: o.sub_account.map(|v| v as i32),
239 is_stub: false,
240 is_local_order: false,
241 stub_inserted_at_ms: 0,
242 is_pending_broker_confirm: false,
243 })
244}
245
246pub async fn query_order_info_strict_for_push_refresh(
254 backend: &BackendConn,
255 acc_id: u64,
256 trd_cache: &TrdCache,
257 order_ids: &[String],
258 security_type: Option<u32>,
259 exchange: Option<&str>,
260) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
261 use prost::Message;
262
263 let filtered_ids: Vec<String> = order_ids
264 .iter()
265 .filter(|id| !id.is_empty())
266 .cloned()
267 .collect();
268 if filtered_ids.is_empty() {
269 return Ok(vec![]);
270 }
271
272 let (trd_env, _enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
273 let cmd_id = order_info_cmd_for_env(trd_env);
274 let real_cipher = if trd_env == 1 {
275 require_real_cipher_for_strict_trade_query(acc_id, trd_cache, "QueryOrderInfo")?
276 } else {
277 Vec::new()
278 };
279 let req =
280 build_order_info_req_base(acc_id, security_type, exchange, &filtered_ids, real_cipher);
281 let resp = backend.request(cmd_id, req.encode_to_vec()).await?;
282 let (orders, received): (Vec<futu_cache::trd_cache::CachedOrder>, usize) = if trd_env == 1 {
283 let parsed: order_sys_interface::OrderDetailRsp = Message::decode(resp.body.as_ref())?;
284 if let Err(status_err) = backend_order_info_status_like_cpp(
285 parsed.result,
286 parsed.msg_header.as_ref(),
287 parsed.err_msg.as_deref(),
288 acc_id,
289 filtered_ids.len(),
290 parsed.orders.len(),
291 ) {
292 return Err(futu_core::error::FutuError::ServerError {
293 ret_type: status_err.result,
294 msg: format!("{} (acc_id={acc_id})", status_err.message),
295 });
296 }
297 let received = parsed.orders.len();
298 let orders = parsed
299 .orders
300 .iter()
301 .filter_map(|order| {
302 let jp_acc_type = order
303 .sub_account_id
304 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
305 order_proto_to_cached_with_jp_acc_type_like_cpp(trd_env, order, jp_acc_type)
306 })
307 .collect();
308 (orders, received)
309 } else {
310 let parsed: sim_order_sys_interface::OrderDetailRsp = Message::decode(resp.body.as_ref())?;
311 if let Err(status_err) = backend_order_info_status_by_account_like_cpp(
312 parsed.result,
313 parsed
314 .msg_header
315 .as_ref()
316 .and_then(|header| header.account_id),
317 parsed.err_msg.as_deref(),
318 acc_id,
319 filtered_ids.len(),
320 parsed.orders.len(),
321 ) {
322 return Err(futu_core::error::FutuError::ServerError {
323 ret_type: status_err.result,
324 msg: format!("{} (acc_id={acc_id})", status_err.message),
325 });
326 }
327 let received = parsed.orders.len();
328 let orders = parsed
329 .orders
330 .iter()
331 .filter_map(|order| sim_order_proto_to_cached_like_cpp(trd_env, order))
332 .collect();
333 (orders, received)
334 };
335 let mut applied_orders = Vec::with_capacity(orders.len());
336 for order in orders {
337 if trd_cache.upsert_order(acc_id, order.clone()) {
338 applied_orders.push(order);
339 }
340 }
341 tracing::debug!(
342 acc_id,
343 cmd_id,
344 requested = filtered_ids.len(),
345 received,
346 applied = applied_orders.len(),
347 "order details queried"
348 );
349 Ok(applied_orders)
350}
351
352pub async fn query_orders_inner(
353 backend: &BackendConn,
354 acc_id: u64,
355 trd_cache: &TrdCache,
356 error_mode: QueryErrorMode,
357) -> Result<Vec<futu_cache::trd_cache::CachedOrder>> {
358 use prost::Message;
359
360 let (trd_env, enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
364 let real_cipher = if trd_env == 1 && error_mode.is_strict() {
370 require_real_cipher_for_strict_trade_query(acc_id, trd_cache, "QueryOrderList")?
371 } else {
372 trd_cache.get_cipher(acc_id).unwrap_or_default()
373 };
374 let real_req = build_order_list_req_base(acc_id, trd_env, &enabled_markets, real_cipher);
375 let sim_req = build_sim_order_list_req_base(acc_id, &enabled_markets);
376
377 let mut all_orders = Vec::new();
378 let mut page_flag: Option<String> = Some(String::new());
381 const MAX_PAGES: usize = 100;
386 let mut completed = false;
387
388 for _page in 0..MAX_PAGES {
389 let spec = trade_query_command(TradeQueryOperation::Orders);
390 let (cmd_id, req_body) = if trd_env == 1 {
391 let mut paged_req = real_req.clone();
392 paged_req.page_flag = page_flag.clone();
393 (spec.real_cmd, paged_req.encode_to_vec())
394 } else {
395 let mut paged_req = sim_req.clone();
396 paged_req.page_flag = page_flag.clone();
397 (spec.sim_cmd, paged_req.encode_to_vec())
398 };
399
400 let resp = match backend.request(cmd_id, req_body).await {
401 Ok(r) => r,
402 Err(e) => {
403 tracing::debug!(acc_id, cmd_id, error = %e, "order query failed");
404 return Err(e);
405 }
406 };
407
408 let parsed: order_sys_interface::OrderListRsp = match Message::decode(resp.body.as_ref()) {
409 Ok(p) => p,
410 Err(e) => {
411 if error_mode.is_strict() {
414 tracing::warn!(
415 acc_id,
416 error = %e,
417 "v1.4.106 F3 strict: order response decode failed → \
418 returning Err to trigger retry (push-refresh path)"
419 );
420 return Err(futu_core::error::FutuError::from(e));
421 }
422 tracing::debug!(acc_id, error = %e, "order response decode failed (lenient)");
423 return Ok(vec![]);
424 }
425 };
426
427 if let Err(status_err) = backend_order_list_status_like_cpp(
428 parsed.result,
429 parsed.msg_header.as_ref(),
430 parsed.err_msg.as_deref(),
431 acc_id,
432 ) {
433 if error_mode.is_strict() || !status_err.is_backend_error {
438 tracing::warn!(
439 acc_id,
440 result = status_err.result,
441 error = %status_err.message,
442 "v1.4.110: order query status/header invalid -> returning Err \
443 to trigger retry (push-refresh path)"
444 );
445 return Err(futu_core::error::FutuError::ServerError {
446 ret_type: status_err.result,
447 msg: format!("{} (acc_id={acc_id})", status_err.message),
448 });
449 }
450 return Ok(vec![]);
451 }
452
453 for o in &parsed.orders {
454 let jp_acc_type = o
455 .sub_account_id
456 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
457 if let Some(order) =
458 order_proto_to_cached_with_jp_acc_type_like_cpp(trd_env, o, jp_acc_type)
459 {
460 all_orders.push(order);
461 }
462 }
463
464 let parsed_completed = parsed.completed.unwrap_or(false);
469 if parsed_completed {
470 completed = true;
471 break;
472 }
473 match parsed.page_flag {
474 Some(ref pf) if !pf.is_empty() => {
475 page_flag = Some(pf.clone());
476 }
477 _ => {
478 tracing::warn!(
480 acc_id,
481 partial_count = all_orders.len(),
482 "v1.4.106 audit 0955 F5: backend completed=false 但 page_flag \
483 缺失/空 — partial response, 不写 cache 防 stub 被抹"
484 );
485 return Err(futu_core::error::FutuError::Codec(
486 "query_orders: partial response (completed=false + missing page_flag)"
487 .to_string(),
488 ));
489 }
490 }
491 }
492
493 if !completed {
495 tracing::warn!(
496 acc_id,
497 max_pages = MAX_PAGES,
498 partial_count = all_orders.len(),
499 "v1.4.106 audit 0955 F5: pagination 超 MAX_PAGES 仍未 completed — \
500 partial response, 不写 cache 防 stub 被抹"
501 );
502 return Err(futu_core::error::FutuError::Codec(format!(
503 "query_orders: pagination exceeded {MAX_PAGES} pages without completed=true"
504 )));
505 }
506
507 tracing::debug!(acc_id, count = all_orders.len(), "orders queried");
508 trd_cache.merge_preserving_stubs(acc_id, all_orders.clone());
542 Ok(all_orders)
543}