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, _enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
157 let cmd_id = order_fill_info_cmd_for_env(trd_env);
158 let req = build_order_fill_info_req_base(acc_id, security_type, exchange, &filtered_ids);
159 let resp = backend.request(cmd_id, req.encode_to_vec()).await?;
160 let parsed: order_sys_interface::OrderFillInfoRsp = Message::decode(resp.body.as_ref())?;
161
162 if let Err(status_err) = backend_order_fill_info_status_like_cpp(
163 parsed.result,
164 parsed.msg_header.as_ref(),
165 parsed.err_msg.as_deref(),
166 acc_id,
167 filtered_ids.len(),
168 parsed.order_fills.len(),
169 ) {
170 return Err(futu_core::error::FutuError::ServerError {
171 ret_type: status_err.result,
172 msg: format!("{} (acc_id={acc_id})", status_err.message),
173 });
174 }
175
176 Ok(parsed
177 .order_fills
178 .iter()
179 .filter_map(|fill| {
180 let jp_acc_type = fill
181 .sub_account_id
182 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
183 try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
184 })
185 .collect())
186}
187
188pub async fn query_order_fills_inner(
189 backend: &BackendConn,
190 acc_id: u64,
191 trd_cache: &TrdCache,
192 error_mode: QueryErrorMode,
193) -> Result<Vec<OrderFillInfo>> {
194 use prost::Message;
195
196 let (trd_env, enabled_markets) = derive_acc_query_context(trd_cache, acc_id);
198 let req = build_order_fill_list_req_base(acc_id, trd_env, &enabled_markets);
199 let cmd_id = order_fill_list_cmd_for_env(trd_env);
200
201 let mut all_fills = Vec::new();
202 let mut page_flag: Option<String> = None;
203 const MAX_PAGES_FILLS: usize = 100;
205 let mut fills_completed = false;
206
207 for _page in 0..MAX_PAGES_FILLS {
208 let mut paged_req = req.clone();
209 paged_req.page_flag = page_flag.clone();
210
211 let resp = match backend.request(cmd_id, paged_req.encode_to_vec()).await {
212 Ok(r) => r,
213 Err(e) => {
214 tracing::debug!(acc_id, cmd_id, error = %e, "fill query failed");
215 return Err(e);
216 }
217 };
218
219 let parsed: order_sys_interface::OrderFillListRsp =
220 match Message::decode(resp.body.as_ref()) {
221 Ok(p) => p,
222 Err(e) => {
223 if error_mode.is_strict() {
225 tracing::warn!(
226 acc_id,
227 error = %e,
228 "v1.4.106 F3 strict: fill response decode failed → \
229 returning Err to trigger retry (push-refresh path)"
230 );
231 return Err(futu_core::error::FutuError::from(e));
232 }
233 tracing::debug!(acc_id, error = %e, "fill response decode failed (lenient)");
234 return Ok(vec![]);
235 }
236 };
237
238 if let Err(status_err) = backend_order_fill_list_status_like_cpp(
239 parsed.result,
240 parsed.msg_header.as_ref(),
241 parsed.err_msg.as_deref(),
242 acc_id,
243 ) {
244 if error_mode.is_strict() || !status_err.is_backend_error {
249 tracing::warn!(
250 acc_id,
251 result = status_err.result,
252 error = %status_err.message,
253 "v1.4.110: fill query status/header invalid -> returning Err \
254 to trigger retry (push-refresh path)"
255 );
256 return Err(futu_core::error::FutuError::ServerError {
257 ret_type: status_err.result,
258 msg: format!("{} (acc_id={acc_id})", status_err.message),
259 });
260 }
261 return Ok(vec![]);
262 }
263
264 all_fills.extend(parsed.order_fills.iter().filter_map(|fill| {
265 let jp_acc_type = fill
266 .sub_account_id
267 .and_then(|id| trd_cache.jp_acc_type_for_sub_account(acc_id, id));
268 try_order_fill_proto_to_info_with_jp_acc_type_like_cpp(fill, jp_acc_type)
269 }));
270
271 let parsed_completed = parsed.completed.unwrap_or(false);
273 if parsed_completed {
274 fills_completed = true;
275 break;
276 }
277 match parsed.page_flag {
278 Some(ref pf) if !pf.is_empty() => {
279 page_flag = Some(pf.clone());
280 }
281 _ => {
282 tracing::warn!(
283 acc_id,
284 partial_count = all_fills.len(),
285 "v1.4.106 audit 0955 F5: backend completed=false 但 page_flag \
286 缺失/空 — partial response, 不返 partial 列表防 caller 误用"
287 );
288 return Err(futu_core::error::FutuError::Codec(
289 "query_order_fills: partial response (completed=false + missing page_flag)"
290 .to_string(),
291 ));
292 }
293 }
294 }
295
296 if !fills_completed {
297 tracing::warn!(
298 acc_id,
299 max_pages = MAX_PAGES_FILLS,
300 partial_count = all_fills.len(),
301 "v1.4.106 audit 0955 F5: pagination 超 MAX_PAGES 仍未 completed — partial"
302 );
303 return Err(futu_core::error::FutuError::Codec(format!(
304 "query_order_fills: pagination exceeded {MAX_PAGES_FILLS} pages without completed=true"
305 )));
306 }
307
308 tracing::debug!(acc_id, count = all_fills.len(), "order fills queried");
309 Ok(all_fills)
310}