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;
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    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
36    query_crypto_orders_with_context(backend, acc_id, trd_cache, &ctx).await
37}
38
39/// Startup-only recent-order read. C++ permits this read before trade unlock;
40/// an empty cipher is omitted from `CryptoMsgHeader` while strict/write paths
41/// continue to require the cached cipher.
42pub async fn query_crypto_orders_for_startup(
43    backend: &BackendConn,
44    acc_id: u64,
45    trd_cache: &TrdCache,
46) -> Result<Vec<CachedOrder>> {
47    let ctx = lookup_crypto_read_account_context(trd_cache, acc_id)?;
48    query_crypto_orders_with_context(backend, acc_id, trd_cache, &ctx).await
49}
50
51async fn query_crypto_orders_with_context(
52    backend: &BackendConn,
53    acc_id: u64,
54    trd_cache: &TrdCache,
55    ctx: &CryptoAccountContext,
56) -> Result<Vec<CachedOrder>> {
57    use prost::Message;
58
59    let spec = crypto_trade_command(CryptoTradeOperation::Orders);
60    let mut all_orders = Vec::new();
61    let mut page_flag: Option<String> = None;
62
63    for _ in 0..MAX_PAGES {
64        let plan = plan_crypto_order_list_request_like_cpp(page_flag.as_deref());
65        let req = inbound_read::OrderListReq {
66            msg_header: Some(ctx.build_crypto_msg_header("order_list")),
67            page_size: Some(plan.page_size),
68            page_flag: plan.page_flag,
69            list_type: Some(plan.list_type),
70        };
71        let resp = crate::command_runtime::execute_crypto_trade_command(
72            backend,
73            CryptoTradeOperation::Orders,
74            None,
75            bytes::Bytes::from(req.encode_to_vec()),
76        )
77        .await
78        .map_err(|e| {
79            tracing::warn!(cmd_id = spec.cmd, error = %e, "crypto order query failed");
80            e
81        })?;
82
83        let parsed: inbound_read::OrderListRsp =
84            Message::decode(resp.body.as_ref()).map_err(|e| {
85                tracing::warn!(
86                    cmd_id = spec.cmd,
87                    body_len = resp.body.len(),
88                    error = %e,
89                    "crypto order query decode failed"
90                );
91                FutuError::Proto(e)
92            })?;
93
94        ensure_crypto_order_read_status("CryptoOrderListRsp", parsed.msg_header.as_ref(), acc_id)?;
95        all_orders.extend(parsed.orders.iter().filter_map(project_crypto_order));
96        match decide_crypto_read_order_page_like_cpp(
97            "query_crypto_orders",
98            parsed.completed,
99            parsed.page_flag.as_deref(),
100            all_orders.len(),
101        ) {
102            CryptoReadCursorPageDecision::Complete => {
103                trd_cache.merge_preserving_stubs(acc_id, all_orders.clone());
104                tracing::debug!(count = all_orders.len(), "crypto orders queried");
105                return Ok(all_orders);
106            }
107            CryptoReadCursorPageDecision::Continue { next_page_flag } => {
108                page_flag = Some(next_page_flag);
109            }
110            CryptoReadCursorPageDecision::PartialMissingPageFlag { error_message, .. } => {
111                return Err(FutuError::Codec(error_message));
112            }
113        }
114    }
115
116    Err(FutuError::Codec(
117        crypto_read_order_pagination_exceeded_like_cpp("query_crypto_orders", MAX_PAGES),
118    ))
119}
120
121/// Query crypto order details through CMD20625 and merge them into order cache.
122///
123/// C++ 10.5.6508 `NNProto_Trd_OrderCrypto.cpp:153-177` handles
124/// `NotifyCryptoOrder` by calling `QueryOrderInfo` with the pushed string order
125/// ids. Keep this detail path separate from the full recent-order list refresh
126/// so push updates only refresh the orders named by backend.
127pub async fn query_crypto_order_info(
128    backend: &BackendConn,
129    acc_id: u64,
130    trd_cache: &TrdCache,
131    order_ids: &[String],
132) -> Result<Vec<CachedOrder>> {
133    use prost::Message;
134
135    let detail_plan =
136        plan_crypto_order_detail_request_like_cpp(order_ids).map_err(FutuError::Codec)?;
137    let requested = detail_plan.order_ids;
138
139    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
140    let spec = crypto_trade_command(CryptoTradeOperation::OrderInfo);
141    let req = inbound_read::OrderDetailReq {
142        msg_header: Some(ctx.build_crypto_msg_header("order_detail")),
143        order_ids: requested.clone(),
144    };
145    let resp = crate::command_runtime::execute_crypto_trade_command(
146        backend,
147        CryptoTradeOperation::OrderInfo,
148        None,
149        bytes::Bytes::from(req.encode_to_vec()),
150    )
151    .await
152    .map_err(|e| {
153        tracing::warn!(
154            cmd_id = spec.cmd,
155            order_ids = ?requested,
156            error = %e,
157            "crypto order detail query failed"
158        );
159        e
160    })?;
161
162    let parsed: inbound_read::OrderDetailRsp =
163        Message::decode(resp.body.as_ref()).map_err(|e| {
164            tracing::warn!(
165                cmd_id = spec.cmd,
166                body_len = resp.body.len(),
167                error = %e,
168                "crypto order detail query decode failed"
169            );
170            FutuError::Proto(e)
171        })?;
172    ensure_crypto_order_detail_status(
173        "CryptoOrderDetailRsp",
174        parsed.msg_header.as_ref(),
175        acc_id,
176        requested.len(),
177        parsed.orders.len(),
178    )?;
179    let orders: Vec<CachedOrder> = parsed
180        .orders
181        .iter()
182        .filter_map(project_crypto_order)
183        .collect();
184
185    trd_cache.merge_preserving_stubs(acc_id, orders.clone());
186    tracing::debug!(count = orders.len(), "crypto order details queried");
187    Ok(orders)
188}
189
190/// Query crypto history orders through CMD20623.
191///
192/// C++ 10.5.6508 `NNProto_Trd_OrderCrypto.cpp:373-398` sends
193/// `inbound_read::HistoryOrderListReq` with `start_time` / `end_time` already
194/// in microseconds. Unlike the active order query, this read path does not
195/// update the active order cache.
196pub async fn query_crypto_history_orders(
197    backend: &BackendConn,
198    acc_id: u64,
199    trd_cache: &TrdCache,
200    start_micros: u64,
201    end_micros: u64,
202) -> Result<Vec<CachedOrder>> {
203    use prost::Message;
204
205    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
206    let spec = crypto_trade_command(CryptoTradeOperation::HistoryOrders);
207    let mut all_orders = Vec::new();
208    let mut page_flag: Option<String> = None;
209
210    for _ in 0..MAX_PAGES {
211        let plan = plan_crypto_history_order_list_request_like_cpp(
212            start_micros,
213            end_micros,
214            page_flag.as_deref(),
215        );
216        let req = inbound_read::HistoryOrderListReq {
217            msg_header: Some(ctx.build_crypto_msg_header("history_order_list")),
218            page_size: Some(plan.page_size),
219            page_flag: plan.page_flag,
220            start_time: Some(plan.start_time_micros),
221            end_time: Some(plan.end_time_micros),
222            symbol: plan.symbol,
223            order_status: plan.order_status,
224            currency: plan.currency,
225            side: plan.side,
226            query_word: plan.query_word,
227            ord_type: plan.order_type,
228        };
229        let resp = crate::command_runtime::execute_crypto_trade_command(
230            backend,
231            CryptoTradeOperation::HistoryOrders,
232            None,
233            bytes::Bytes::from(req.encode_to_vec()),
234        )
235        .await
236        .map_err(|e| {
237            tracing::warn!(
238                cmd_id = spec.cmd,
239                error = %e,
240                "crypto history order query failed"
241            );
242            e
243        })?;
244
245        let parsed: inbound_read::HistoryOrderListRsp = Message::decode(resp.body.as_ref())
246            .map_err(|e| {
247                tracing::warn!(
248                    cmd_id = spec.cmd,
249                    body_len = resp.body.len(),
250                    error = %e,
251                    "crypto history order query decode failed"
252                );
253                FutuError::Proto(e)
254            })?;
255
256        ensure_crypto_order_read_status(
257            "CryptoHistoryOrderListRsp",
258            parsed.msg_header.as_ref(),
259            acc_id,
260        )?;
261        all_orders.extend(parsed.orders.iter().filter_map(project_crypto_order));
262        match decide_crypto_read_order_page_like_cpp(
263            "query_crypto_history_orders",
264            parsed.completed,
265            parsed.page_flag.as_deref(),
266            all_orders.len(),
267        ) {
268            CryptoReadCursorPageDecision::Complete => {
269                tracing::debug!(count = all_orders.len(), "crypto history orders queried");
270                return Ok(all_orders);
271            }
272            CryptoReadCursorPageDecision::Continue { next_page_flag } => {
273                page_flag = Some(next_page_flag);
274            }
275            CryptoReadCursorPageDecision::PartialMissingPageFlag { error_message, .. } => {
276                return Err(FutuError::Codec(error_message));
277            }
278        }
279    }
280
281    Err(FutuError::Codec(
282        crypto_read_order_pagination_exceeded_like_cpp("query_crypto_history_orders", MAX_PAGES),
283    ))
284}
285
286fn ensure_crypto_order_read_status(
287    message_name: &str,
288    msg_header: Option<&trade_cmn::CryptoMsgHeader>,
289    acc_id: u64,
290) -> Result<()> {
291    crypto_order_read_response_status_like_cpp(
292        message_name,
293        msg_header.map(crypto_msg_header_facts),
294        acc_id,
295    )
296    .map_err(crypto_order_status_error_to_futu_error)
297}
298
299fn ensure_crypto_order_detail_status(
300    message_name: &str,
301    msg_header: Option<&trade_cmn::CryptoMsgHeader>,
302    acc_id: u64,
303    expected_count: usize,
304    actual_count: usize,
305) -> Result<()> {
306    crypto_order_detail_response_status_like_cpp(
307        message_name,
308        msg_header.map(crypto_msg_header_facts),
309        acc_id,
310        expected_count,
311        actual_count,
312    )
313    .map_err(crypto_order_status_error_to_futu_error)
314}
315
316fn crypto_msg_header_facts(header: &trade_cmn::CryptoMsgHeader) -> BackendHistoryMsgHeaderFacts {
317    BackendHistoryMsgHeaderFacts {
318        account_id: header.account_id,
319    }
320}
321
322fn crypto_order_status_error_to_futu_error(err: BackendHistoryStatusError) -> FutuError {
323    if err.is_backend_error {
324        FutuError::ServerError {
325            ret_type: -1,
326            msg: err.message,
327        }
328    } else {
329        FutuError::Codec(err.message)
330    }
331}