futu_backend/trade_query/orders/
query_fills.rs1use 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
25pub 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_and_jp_acc_type(
45 f: &odr_sys_cmn::OrderFill,
46 trd_market: Option<i32>,
47 jp_acc_type: Option<i32>,
48) -> OrderFillInfo {
49 let (qty, price) = backend_order_fill_qty_price_for_ftapi(f);
50 let fill_id_ex = f.id.clone().unwrap_or_default();
51 let fill_id: u64 = if fill_id_ex.is_empty() {
55 0
56 } else {
57 hash_str_to_u64(&fill_id_ex)
58 };
59 let order_id_ex = f.order_id.clone().unwrap_or_default();
60 let order_id: u64 = if order_id_ex.is_empty() {
61 0
62 } else {
63 hash_str_to_u64(&order_id_ex)
64 };
65 let create_timestamp = f.create_time.map(|t| t as f64 / 1_000_000.0);
66 let update_timestamp = f.update_time.map(|t| t as f64 / 1_000_000.0);
67 let counter_broker_id = f
68 .counter_broker_id
69 .as_ref()
70 .and_then(|s| s.parse::<i32>().ok());
71 let status = Some(backend_deal_status_to_ftapi(
74 f.is_cancelled.unwrap_or(false),
75 f.is_corrected.unwrap_or(false),
76 ));
77
78 OrderFillInfo {
79 fill_id,
80 fill_id_ex,
81 order_id,
82 order_id_ex,
83 code: f.symbol.as_ref().cloned().unwrap_or_default(),
84 name: f.stock_name.as_ref().cloned().unwrap_or_default(),
85 trd_side: f.side.unwrap_or(0) as i32,
86 qty,
87 price,
88 create_timestamp,
89 update_timestamp,
90 counter_broker_id,
91 status,
92 trd_market,
93 jp_acc_type,
94 }
95}
96
97pub fn try_order_fill_proto_to_info_like_cpp(f: &odr_sys_cmn::OrderFill) -> Option<OrderFillInfo> {
108 try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(f, None)
109}
110
111pub fn try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(
112 f: &odr_sys_cmn::OrderFill,
113 jp_acc_type: Option<i32>,
114) -> Option<OrderFillInfo> {
115 f.id.as_ref()?;
116 f.side?;
117 let trd_market = backend_real_market_to_trd_market_like_cpp(f.market?);
118 if !is_valid_real_trd_market_like_cpp(trd_market) {
119 return None;
120 }
121 f.symbol.as_ref()?;
122 f.exchange.as_ref()?;
123 f.price.as_deref()?.parse::<f64>().ok()?;
124 f.qty.as_deref()?.parse::<f64>().ok()?;
125 f.create_time?;
126
127 Some(order_fill_proto_to_info_with_market_and_jp_acc_type(
128 f,
129 Some(trd_market),
130 jp_acc_type,
131 ))
132}
133
134pub async fn query_order_fill_info_strict_for_push_refresh(
138 backend: &BackendConn,
139 acc_id: u64,
140 trd_cache: &TrdCache,
141 order_fill_ids: &[String],
142 security_type: Option<u32>,
143 exchange: Option<&str>,
144) -> Result<Vec<OrderFillInfo>> {
145 use prost::Message;
146
147 let filtered_ids: Vec<String> = order_fill_ids
148 .iter()
149 .filter(|id| !id.is_empty())
150 .cloned()
151 .collect();
152 if filtered_ids.is_empty() {
153 return Ok(vec![]);
154 }
155
156 let (trd_env, _account_trd_market, _enabled_markets) =
157 derive_acc_query_context(trd_cache, acc_id);
158 let cmd_id = order_fill_info_cmd_for_env(trd_env);
159 let req = build_order_fill_info_req_base(acc_id, security_type, exchange, &filtered_ids);
160 let resp = backend.request(cmd_id, req.encode_to_vec()).await?;
161 let parsed: order_sys_interface::OrderFillInfoRsp = Message::decode(resp.body.as_ref())?;
162
163 if let Err(status_err) = backend_order_fill_info_status_like_cpp(
164 parsed.result,
165 parsed.msg_header.as_ref(),
166 parsed.err_msg.as_deref(),
167 acc_id,
168 filtered_ids.len(),
169 parsed.order_fills.len(),
170 ) {
171 return Err(futu_core::error::FutuError::ServerError {
172 ret_type: status_err.result,
173 msg: format!("{} (acc_id={acc_id})", status_err.message),
174 });
175 }
176
177 Ok(parsed
178 .order_fills
179 .iter()
180 .filter_map(|fill| {
181 let jp_acc_type = fill
182 .sub_account_id
183 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
184 try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
185 })
186 .collect())
187}
188
189pub async fn query_order_fills_inner(
190 backend: &BackendConn,
191 acc_id: u64,
192 trd_cache: &TrdCache,
193 error_mode: QueryErrorMode,
194) -> Result<Vec<OrderFillInfo>> {
195 use prost::Message;
196
197 let (trd_env, _account_trd_market, enabled_markets) =
199 derive_acc_query_context(trd_cache, acc_id);
200 let req = build_order_fill_list_req_base(acc_id, trd_env, &enabled_markets);
201 let cmd_id = order_fill_list_cmd_for_env(trd_env);
202
203 let mut all_fills = Vec::new();
204 let mut page_flag: Option<String> = None;
205 const MAX_PAGES_FILLS: usize = 100;
207 let mut fills_completed = false;
208
209 for _page in 0..MAX_PAGES_FILLS {
210 let mut paged_req = req.clone();
211 paged_req.page_flag = page_flag.clone();
212
213 let resp = match backend.request(cmd_id, paged_req.encode_to_vec()).await {
214 Ok(r) => r,
215 Err(e) => {
216 tracing::debug!(acc_id, cmd_id, error = %e, "fill query failed");
217 return Err(e);
218 }
219 };
220
221 let parsed: order_sys_interface::OrderFillListRsp =
222 match Message::decode(resp.body.as_ref()) {
223 Ok(p) => p,
224 Err(e) => {
225 if error_mode.is_strict() {
227 tracing::warn!(
228 acc_id,
229 error = %e,
230 "v1.4.106 F3 strict: fill response decode failed → \
231 returning Err to trigger retry (push-refresh path)"
232 );
233 return Err(futu_core::error::FutuError::from(e));
234 }
235 tracing::debug!(acc_id, error = %e, "fill response decode failed (lenient)");
236 return Ok(vec![]);
237 }
238 };
239
240 if let Err(status_err) = backend_order_fill_list_status_like_cpp(
241 parsed.result,
242 parsed.msg_header.as_ref(),
243 parsed.err_msg.as_deref(),
244 acc_id,
245 ) {
246 if error_mode.is_strict() || !status_err.is_backend_error {
251 tracing::warn!(
252 acc_id,
253 result = status_err.result,
254 error = %status_err.message,
255 "v1.4.110: fill query status/header invalid -> returning Err \
256 to trigger retry (push-refresh path)"
257 );
258 return Err(futu_core::error::FutuError::ServerError {
259 ret_type: status_err.result,
260 msg: format!("{} (acc_id={acc_id})", status_err.message),
261 });
262 }
263 return Ok(vec![]);
264 }
265
266 all_fills.extend(parsed.order_fills.iter().filter_map(|fill| {
267 let jp_acc_type = fill
268 .sub_account_id
269 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
270 try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
271 }));
272
273 let parsed_completed = parsed.completed.unwrap_or(false);
275 if parsed_completed {
276 fills_completed = true;
277 break;
278 }
279 match parsed.page_flag {
280 Some(ref pf) if !pf.is_empty() => {
281 page_flag = Some(pf.clone());
282 }
283 _ => {
284 tracing::warn!(
285 acc_id,
286 partial_count = all_fills.len(),
287 "v1.4.106 audit 0955 F5: backend completed=false 但 page_flag \
288 缺失/空 — partial response, 不返 partial 列表防 caller 误用"
289 );
290 return Err(futu_core::error::FutuError::Codec(
291 "query_order_fills: partial response (completed=false + missing page_flag)"
292 .to_string(),
293 ));
294 }
295 }
296 }
297
298 if !fills_completed {
299 tracing::warn!(
300 acc_id,
301 max_pages = MAX_PAGES_FILLS,
302 partial_count = all_fills.len(),
303 "v1.4.106 audit 0955 F5: pagination 超 MAX_PAGES 仍未 completed — partial"
304 );
305 return Err(futu_core::error::FutuError::Codec(format!(
306 "query_order_fills: pagination exceeded {MAX_PAGES_FILLS} pages without completed=true"
307 )));
308 }
309
310 tracing::debug!(acc_id, count = all_fills.len(), "order fills queried");
311 Ok(all_fills)
312}