Skip to main content

futu_backend/trade_query/crypto_orders/
queries_orders.rs

1//! trade_query/crypto_orders/queries_orders — query_crypto_orders / order_info / history_orders
2//! (v1.4.110 CC Batch K: 拆自 crypto_orders.rs L57-292)
3
4use futu_cache::trd_cache::{CachedOrder, OrderRelationSourceToken};
5use futu_core::error::{FutuError, Result};
6use futu_domain_trade_history::pagination::{
7    CryptoReadCursorPageDecision, crypto_read_order_pagination_exceeded_like_cpp,
8    decide_crypto_read_order_page_like_cpp,
9};
10use futu_domain_trade_history::status::{
11    BackendHistoryMsgHeaderFacts, BackendHistoryStatusError,
12    crypto_order_detail_response_status_like_cpp, crypto_order_read_response_status_like_cpp,
13};
14use futu_domain_trade_history::{
15    plan_crypto_history_order_list_request_like_cpp, plan_crypto_order_detail_request_like_cpp,
16    plan_crypto_order_list_request_like_cpp,
17};
18
19use super::super::*;
20
21use crate::crypto_trade::{
22    CryptoAccountContext, lookup_crypto_account_context, lookup_crypto_read_account_context,
23};
24use crate::proto_internal::trade_cmn;
25use crate::trade_cmd::{CryptoTradeOperation, crypto_trade_command};
26
27use super::projections::*;
28use super::types::*;
29
30pub async fn query_crypto_orders(
31    backend: &BackendConn,
32    acc_id: u64,
33    trd_cache: &TrdCache,
34) -> Result<Vec<CachedOrder>> {
35    query_crypto_orders_with_source(
36        backend,
37        acc_id,
38        trd_cache,
39        OrderRelationSourceToken::unknown(backend.connection_generation()),
40    )
41    .await
42}
43
44pub async fn query_crypto_orders_with_source(
45    backend: &BackendConn,
46    acc_id: u64,
47    trd_cache: &TrdCache,
48    source: OrderRelationSourceToken,
49) -> Result<Vec<CachedOrder>> {
50    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
51    query_crypto_orders_with_context(backend, acc_id, trd_cache, &ctx, source).await
52}
53
54/// Startup-only recent-order read. C++ permits this read before trade unlock;
55/// an empty cipher is omitted from `CryptoMsgHeader` while strict/write paths
56/// continue to require the cached cipher.
57pub async fn query_crypto_orders_for_startup(
58    backend: &BackendConn,
59    acc_id: u64,
60    trd_cache: &TrdCache,
61) -> Result<Vec<CachedOrder>> {
62    query_crypto_orders_for_startup_with_source(
63        backend,
64        acc_id,
65        trd_cache,
66        OrderRelationSourceToken::unknown(backend.connection_generation()),
67    )
68    .await
69}
70
71pub async fn query_crypto_orders_for_startup_with_source(
72    backend: &BackendConn,
73    acc_id: u64,
74    trd_cache: &TrdCache,
75    source: OrderRelationSourceToken,
76) -> Result<Vec<CachedOrder>> {
77    let ctx = lookup_crypto_read_account_context(trd_cache, acc_id)?;
78    query_crypto_orders_with_context(backend, acc_id, trd_cache, &ctx, source).await
79}
80
81async fn query_crypto_orders_with_context(
82    backend: &BackendConn,
83    acc_id: u64,
84    trd_cache: &TrdCache,
85    ctx: &CryptoAccountContext,
86    source: OrderRelationSourceToken,
87) -> Result<Vec<CachedOrder>> {
88    use prost::Message;
89
90    let spec = crypto_trade_command(CryptoTradeOperation::Orders);
91    let mut all_orders = Vec::new();
92    let mut dropped_order_count = 0usize;
93    let mut page_flag: Option<String> = None;
94
95    for _ in 0..MAX_PAGES {
96        let plan = plan_crypto_order_list_request_like_cpp(page_flag.as_deref());
97        let req = inbound_read::OrderListReq {
98            msg_header: Some(ctx.build_crypto_msg_header("order_list")),
99            page_size: Some(plan.page_size),
100            page_flag: plan.page_flag,
101            list_type: Some(plan.list_type),
102        };
103        let resp = crate::command_runtime::execute_crypto_trade_command(
104            backend,
105            CryptoTradeOperation::Orders,
106            None,
107            bytes::Bytes::from(req.encode_to_vec()),
108        )
109        .await
110        .inspect_err(|e| {
111            tracing::warn!(
112                cmd_id = spec.cmd,
113                error = ?futu_core::log_redact::RedactedFutuError::new(e),
114                "crypto order query failed"
115            );
116        })?;
117
118        let parsed: inbound_read::OrderListRsp =
119            Message::decode(resp.body.as_ref()).map_err(|e| {
120                tracing::warn!(
121                    cmd_id = spec.cmd,
122                    body_len = resp.body.len(),
123                    error_class = "protobuf_decode",
124                    "crypto order query decode failed"
125                );
126                FutuError::Proto(e)
127            })?;
128
129        ensure_crypto_order_read_status("CryptoOrderListRsp", parsed.msg_header.as_ref(), acc_id)?;
130        let raw_order_count = parsed.orders.len();
131        let projected = parsed
132            .orders
133            .iter()
134            .filter_map(project_crypto_order)
135            .collect::<Vec<_>>();
136        dropped_order_count =
137            dropped_order_count.saturating_add(raw_order_count.saturating_sub(projected.len()));
138        all_orders.extend(projected);
139        match decide_crypto_read_order_page_like_cpp(
140            "query_crypto_orders",
141            parsed.completed,
142            parsed.page_flag.as_deref(),
143            all_orders.len(),
144        ) {
145            CryptoReadCursorPageDecision::Complete => {
146                trd_cache.replace_order_snapshot_like_cpp_from_source(
147                    acc_id,
148                    &all_orders,
149                    source,
150                    dropped_order_count == 0,
151                );
152                tracing::debug!(count = all_orders.len(), "crypto orders queried");
153                return Ok(all_orders);
154            }
155            CryptoReadCursorPageDecision::Continue { next_page_flag } => {
156                page_flag = Some(next_page_flag);
157            }
158            CryptoReadCursorPageDecision::PartialMissingPageFlag { error_message, .. } => {
159                return Err(FutuError::Codec(error_message));
160            }
161        }
162    }
163
164    Err(FutuError::Codec(
165        crypto_read_order_pagination_exceeded_like_cpp("query_crypto_orders", MAX_PAGES),
166    ))
167}
168
169/// Query crypto order details through CMD20625 and merge them into order cache.
170///
171/// C++ 10.5.6508 `NNProto_Trd_OrderCrypto.cpp:153-177` handles
172/// `NotifyCryptoOrder` by calling `QueryOrderInfo` with the pushed string order
173/// ids. Keep this detail path separate from the full recent-order list refresh
174/// so push updates only refresh the orders named by backend.
175pub async fn query_crypto_order_info(
176    backend: &BackendConn,
177    acc_id: u64,
178    trd_cache: &TrdCache,
179    order_ids: &[String],
180) -> Result<Vec<CachedOrder>> {
181    query_crypto_order_info_with_source(
182        backend,
183        acc_id,
184        trd_cache,
185        order_ids,
186        OrderRelationSourceToken::unknown(backend.connection_generation()),
187    )
188    .await
189}
190
191pub async fn query_crypto_order_info_with_source(
192    backend: &BackendConn,
193    acc_id: u64,
194    trd_cache: &TrdCache,
195    order_ids: &[String],
196    source: OrderRelationSourceToken,
197) -> Result<Vec<CachedOrder>> {
198    use prost::Message;
199
200    let detail_plan =
201        plan_crypto_order_detail_request_like_cpp(order_ids).map_err(FutuError::Codec)?;
202    let requested = detail_plan.order_ids;
203
204    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
205    let spec = crypto_trade_command(CryptoTradeOperation::OrderInfo);
206    let req = inbound_read::OrderDetailReq {
207        msg_header: Some(ctx.build_crypto_msg_header("order_detail")),
208        order_ids: requested.clone(),
209    };
210    let resp = crate::command_runtime::execute_crypto_trade_command(
211        backend,
212        CryptoTradeOperation::OrderInfo,
213        None,
214        bytes::Bytes::from(req.encode_to_vec()),
215    )
216    .await
217    .inspect_err(|e| {
218        tracing::warn!(
219            cmd_id = spec.cmd,
220            order_ids = ?requested,
221            error = ?futu_core::log_redact::RedactedFutuError::new(e),
222            "crypto order detail query failed"
223        );
224    })?;
225
226    let parsed: inbound_read::OrderDetailRsp =
227        Message::decode(resp.body.as_ref()).map_err(|e| {
228            tracing::warn!(
229                cmd_id = spec.cmd,
230                body_len = resp.body.len(),
231                error_class = "protobuf_decode",
232                "crypto order detail query decode failed"
233            );
234            FutuError::Proto(e)
235        })?;
236    ensure_crypto_order_detail_status(
237        "CryptoOrderDetailRsp",
238        parsed.msg_header.as_ref(),
239        acc_id,
240        requested.len(),
241        parsed.orders.len(),
242    )?;
243    let orders: Vec<CachedOrder> = parsed
244        .orders
245        .iter()
246        .filter_map(project_crypto_order)
247        .collect();
248    if orders.len() != parsed.orders.len() {
249        trd_cache.mark_order_relation_snapshot_incomplete_from_source(acc_id, source);
250    }
251    for order in &orders {
252        trd_cache.upsert_order_from_source(acc_id, order.clone(), source);
253    }
254    tracing::debug!(count = orders.len(), "crypto order details queried");
255    Ok(orders)
256}
257
258/// Query crypto history orders through CMD20623.
259///
260/// C++ 10.5.6508 `NNProto_Trd_OrderCrypto.cpp:373-398` sends
261/// `inbound_read::HistoryOrderListReq` with `start_time` / `end_time` already
262/// in microseconds. Unlike the active order query, this read path does not
263/// update the active order cache.
264pub async fn query_crypto_history_orders(
265    backend: &BackendConn,
266    acc_id: u64,
267    trd_cache: &TrdCache,
268    start_micros: u64,
269    end_micros: u64,
270) -> Result<Vec<CachedOrder>> {
271    use prost::Message;
272
273    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
274    let spec = crypto_trade_command(CryptoTradeOperation::HistoryOrders);
275    let mut all_orders = Vec::new();
276    let mut page_flag: Option<String> = None;
277
278    for _ in 0..MAX_PAGES {
279        let plan = plan_crypto_history_order_list_request_like_cpp(
280            start_micros,
281            end_micros,
282            page_flag.as_deref(),
283        );
284        let req = inbound_read::HistoryOrderListReq {
285            msg_header: Some(ctx.build_crypto_msg_header("history_order_list")),
286            page_size: Some(plan.page_size),
287            page_flag: plan.page_flag,
288            start_time: Some(plan.start_time_micros),
289            end_time: Some(plan.end_time_micros),
290            symbol: plan.symbol,
291            order_status: plan.order_status,
292            currency: plan.currency,
293            side: plan.side,
294            query_word: plan.query_word,
295            ord_type: plan.order_type,
296        };
297        let resp = crate::command_runtime::execute_crypto_trade_command(
298            backend,
299            CryptoTradeOperation::HistoryOrders,
300            None,
301            bytes::Bytes::from(req.encode_to_vec()),
302        )
303        .await
304        .inspect_err(|e| {
305            tracing::warn!(
306                cmd_id = spec.cmd,
307                error = ?futu_core::log_redact::RedactedFutuError::new(e),
308                "crypto history order query failed"
309            );
310        })?;
311
312        let parsed: inbound_read::HistoryOrderListRsp = Message::decode(resp.body.as_ref())
313            .map_err(|e| {
314                tracing::warn!(
315                    cmd_id = spec.cmd,
316                    body_len = resp.body.len(),
317                    error_class = "protobuf_decode",
318                    "crypto history order query decode failed"
319                );
320                FutuError::Proto(e)
321            })?;
322
323        ensure_crypto_order_read_status(
324            "CryptoHistoryOrderListRsp",
325            parsed.msg_header.as_ref(),
326            acc_id,
327        )?;
328        all_orders.extend(parsed.orders.iter().filter_map(project_crypto_order));
329        match decide_crypto_read_order_page_like_cpp(
330            "query_crypto_history_orders",
331            parsed.completed,
332            parsed.page_flag.as_deref(),
333            all_orders.len(),
334        ) {
335            CryptoReadCursorPageDecision::Complete => {
336                tracing::debug!(count = all_orders.len(), "crypto history orders queried");
337                return Ok(all_orders);
338            }
339            CryptoReadCursorPageDecision::Continue { next_page_flag } => {
340                page_flag = Some(next_page_flag);
341            }
342            CryptoReadCursorPageDecision::PartialMissingPageFlag { error_message, .. } => {
343                return Err(FutuError::Codec(error_message));
344            }
345        }
346    }
347
348    Err(FutuError::Codec(
349        crypto_read_order_pagination_exceeded_like_cpp("query_crypto_history_orders", MAX_PAGES),
350    ))
351}
352
353fn ensure_crypto_order_read_status(
354    message_name: &str,
355    msg_header: Option<&trade_cmn::CryptoMsgHeader>,
356    acc_id: u64,
357) -> Result<()> {
358    crypto_order_read_response_status_like_cpp(
359        message_name,
360        msg_header.map(crypto_msg_header_facts),
361        acc_id,
362    )
363    .map_err(crypto_order_status_error_to_futu_error)
364}
365
366fn ensure_crypto_order_detail_status(
367    message_name: &str,
368    msg_header: Option<&trade_cmn::CryptoMsgHeader>,
369    acc_id: u64,
370    expected_count: usize,
371    actual_count: usize,
372) -> Result<()> {
373    crypto_order_detail_response_status_like_cpp(
374        message_name,
375        msg_header.map(crypto_msg_header_facts),
376        acc_id,
377        expected_count,
378        actual_count,
379    )
380    .map_err(crypto_order_status_error_to_futu_error)
381}
382
383fn crypto_msg_header_facts(header: &trade_cmn::CryptoMsgHeader) -> BackendHistoryMsgHeaderFacts {
384    BackendHistoryMsgHeaderFacts {
385        account_id: header.account_id,
386    }
387}
388
389fn crypto_order_status_error_to_futu_error(err: BackendHistoryStatusError) -> FutuError {
390    if err.is_backend_error {
391        FutuError::ServerError {
392            ret_type: -1,
393            msg: err.message,
394        }
395    } else {
396        FutuError::Codec(err.message)
397    }
398}