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