Skip to main content

futu_backend/trade_query/crypto_orders/
queries_misc.rs

1//! trade_query/crypto_orders/queries_misc — query_crypto_order_fees / cash_logs / trade_configs / max_buy_sell_qty
2//! (v1.4.110 CC Batch K: 拆自 crypto_orders.rs L539-793)
3
4use futu_cache::static_data::CryptoTradeConfig;
5use futu_core::error::{FutuError, Result};
6use futu_domain_trade_history::projection::crypto::{
7    CryptoCashLogPageDecision, CryptoCashLogResponseStatusDecision,
8    CryptoCashLogResponseStatusFacts, crypto_cash_log_pagination_exceeded_like_cpp,
9    decide_crypto_cash_log_page_like_cpp, decide_crypto_cash_log_response_status_like_cpp,
10    plan_crypto_cash_log_request_like_cpp, plan_crypto_order_fee_batch_ranges_like_cpp,
11};
12use futu_domain_trade_write::{
13    CryptoMaxQtyBackendRequestPlan, CryptoTradeConfigRequestPlan,
14    plan_crypto_trade_config_full_request_like_cpp, project_crypto_max_qty_backend_values_like_cpp,
15};
16
17use super::super::*;
18
19use crate::crypto_trade::{CryptoAccountContext, lookup_crypto_account_context};
20use crate::proto_internal::cash_change_detail_cmn;
21use crate::trade_cmd::{CryptoTradeOperation, crypto_trade_command};
22
23use super::projections::*;
24use super::types::*;
25
26pub async fn query_crypto_order_fees(
27    backend: &BackendConn,
28    acc_id: u64,
29    trd_cache: &TrdCache,
30    order_ids: &[String],
31) -> Result<Vec<CryptoOrderFeeInfo>> {
32    use prost::Message;
33
34    let _ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
35    let spec = crypto_trade_command(CryptoTradeOperation::BatchOrderFee);
36    let mut fees = Vec::new();
37
38    for batch in plan_crypto_order_fee_batch_ranges_like_cpp(order_ids.len()) {
39        let req = inbound_oe::BatchQueryOrderFeeReq {
40            order_id_list: order_ids[batch].to_vec(),
41            long_account_id: Some(acc_id),
42        };
43        let resp = crate::command_runtime::execute_crypto_trade_command(
44            backend,
45            CryptoTradeOperation::BatchOrderFee,
46            None,
47            bytes::Bytes::from(req.encode_to_vec()),
48        )
49        .await
50        .map_err(|e| {
51            tracing::warn!(
52                cmd_id = spec.cmd,
53                error = %e,
54                "crypto order fee query failed"
55            );
56            e
57        })?;
58
59        let parsed: inbound_oe::BatchQueryOrderFeeRsp = Message::decode(resp.body.as_ref())
60            .map_err(|e| {
61                tracing::warn!(
62                    cmd_id = spec.cmd,
63                    body_len = resp.body.len(),
64                    error = %e,
65                    "crypto order fee query decode failed"
66                );
67                FutuError::Proto(e)
68            })?;
69
70        fees.extend(
71            parsed
72                .fee_group_list
73                .iter()
74                .filter_map(project_crypto_order_fee),
75        );
76    }
77
78    Ok(fees)
79}
80
81/// Query crypto cash logs through CMD20632 for FTAPI Trd_FlowSummary.
82///
83/// C++ 10.5.6508 `NNProto_Trd_FlowSummaryCrypto.cpp:191-219` sends
84/// `cash_change_detail_cmn::GetCashLogReq` with `MARKET_CRYPTO`, public long
85/// account id, backend intra account id, and broker id.
86pub async fn query_crypto_cash_logs(
87    backend: &BackendConn,
88    acc_id: u64,
89    trd_cache: &TrdCache,
90    begin_time: u64,
91    end_time: u64,
92) -> Result<Vec<CryptoCashLogInfo>> {
93    use prost::Message;
94
95    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
96    let spec = crypto_trade_command(CryptoTradeOperation::CashLog);
97    let mut all_logs = Vec::new();
98    let mut log_id: Option<String> = None;
99
100    for _ in 0..MAX_PAGES {
101        let plan = plan_crypto_cash_log_request_like_cpp(
102            ctx.require_intra_acc_id("crypto_cash_log")?,
103            ctx.require_broker_id("crypto_cash_log")?,
104            acc_id,
105            begin_time,
106            end_time,
107            log_id.as_deref(),
108        );
109        let req = cash_change_detail_cmn::GetCashLogReq {
110            market: Some(plan.market),
111            account_id: Some(plan.account_id),
112            broker_id: Some(plan.broker_id),
113            long_account_id: Some(plan.long_account_id),
114            begin_time: Some(plan.begin_time),
115            end_time: Some(plan.end_time),
116            log_id: plan.log_id,
117            // C++ `NNProto_Trd_FlowSummaryCrypto.cpp:200-211` only sets the
118            // seven fields above. biz_group/in_out/max_cnt/keyword/
119            // biz_sub_group/currency/stock_id/time_location intentionally stay
120            // absent; this prost default is construction shorthand, not a
121            // fallback for unknown protocol semantics.
122            ..Default::default()
123        };
124        let resp = crate::command_runtime::execute_crypto_trade_command(
125            backend,
126            CryptoTradeOperation::CashLog,
127            None,
128            bytes::Bytes::from(req.encode_to_vec()),
129        )
130        .await
131        .map_err(|e| {
132            tracing::warn!(
133                cmd_id = spec.cmd,
134                error = %e,
135                "crypto cash log query failed"
136            );
137            e
138        })?;
139
140        let parsed: cash_change_detail_cmn::GetCashLogRsp = Message::decode(resp.body.as_ref())
141            .map_err(|e| {
142                tracing::warn!(
143                    cmd_id = spec.cmd,
144                    body_len = resp.body.len(),
145                    error = %e,
146                    "crypto cash log decode failed"
147                );
148                FutuError::Proto(e)
149            })?;
150
151        match decide_crypto_cash_log_response_status_like_cpp(CryptoCashLogResponseStatusFacts {
152            result: parsed.result,
153            err_msg: parsed.err_msg.as_deref(),
154        }) {
155            CryptoCashLogResponseStatusDecision::Accepted => {}
156            CryptoCashLogResponseStatusDecision::BusinessError { ret_type, message } => {
157                return Err(FutuError::ServerError {
158                    ret_type,
159                    msg: message,
160                });
161            }
162        }
163
164        all_logs.extend(
165            parsed
166                .monthly_log_list
167                .iter()
168                .flat_map(|month| month.cash_log_list.iter())
169                .filter_map(project_crypto_cash_log),
170        );
171
172        match decide_crypto_cash_log_page_like_cpp(parsed.has_more, parsed.next_log_id.as_deref()) {
173            CryptoCashLogPageDecision::Continue { next_log_id } => {
174                log_id = Some(next_log_id);
175                continue;
176            }
177            CryptoCashLogPageDecision::Complete => {}
178        }
179
180        tracing::debug!(count = all_logs.len(), "crypto cash logs queried");
181        return Ok(all_logs);
182    }
183
184    Err(FutuError::Codec(
185        crypto_cash_log_pagination_exceeded_like_cpp("query_crypto_cash_logs", MAX_PAGES),
186    ))
187}
188
189/// Query crypto trade configs through CMD20102.
190///
191/// C++ `NNProto_Trd_CryptoTradeConfig.cpp:14-23,63-89` pulls the full trade
192/// config list per broker and stores it in `INNData_Trd_CryptoTradeConfig`.
193/// MaxBuySellQty needs the same config for min quantity and quantity increment.
194pub async fn query_crypto_trade_configs(
195    backend: &BackendConn,
196    broker_id: u32,
197) -> Result<Vec<CryptoTradeConfig>> {
198    use prost::Message;
199
200    let spec = crypto_trade_command(CryptoTradeOperation::FetchTradeConfig);
201    let req =
202        fetch_trade_config_request_from_plan(plan_crypto_trade_config_full_request_like_cpp());
203    let resp = crate::command_runtime::execute_crypto_trade_command(
204        backend,
205        CryptoTradeOperation::FetchTradeConfig,
206        None,
207        bytes::Bytes::from(req.encode_to_vec()),
208    )
209        .await
210        .map_err(|e| {
211            tracing::warn!(broker_id, cmd_id = spec.cmd, error = %e, "crypto trade config query failed");
212            e
213        })?;
214
215    let parsed: inbound_oe::FetchTradeConfigResponse = Message::decode(resp.body.as_ref())
216        .map_err(|e| {
217            tracing::warn!(
218                broker_id,
219                cmd_id = spec.cmd,
220                body_len = resp.body.len(),
221                error = %e,
222                "crypto trade config decode failed"
223            );
224            FutuError::Proto(e)
225        })?;
226    crypto_trade_config_response_status(&parsed)?;
227
228    Ok(parsed
229        .full_trade_config_list
230        .iter()
231        .filter_map(project_crypto_trade_config)
232        .collect())
233}
234
235fn fetch_trade_config_request_from_plan(
236    plan: CryptoTradeConfigRequestPlan,
237) -> inbound_oe::FetchTradeConfigRequest {
238    inbound_oe::FetchTradeConfigRequest {
239        currency_pair_list: plan
240            .currency_pairs
241            .into_iter()
242            .map(|pair| config_base::CurrencyPair {
243                base_currency: Some(pair.base_currency),
244                quote_currency: Some(pair.quote_currency),
245            })
246            .collect(),
247        data_from: plan.data_from,
248        data_max_count: plan.data_max_count,
249        symbol_list: plan.symbols,
250        status: plan.status,
251    }
252}
253
254/// Query crypto max buy/sell quantity through CMD20622.
255///
256/// C++ `NNProto_Trd_MaxQtyCrypto.cpp:14-38,49-78` builds
257/// `crypto_risk::GetMaxBuySellReq` from account context, `cc_origin`, and
258/// `cc_destination`.
259pub async fn query_crypto_max_buy_sell_qty(
260    backend: &BackendConn,
261    ctx: &CryptoAccountContext,
262    input: CryptoMaxQtyBackendRequestPlan,
263) -> Result<CryptoMaxBuySellQtyInfo> {
264    use prost::Message;
265
266    let spec = crypto_trade_command(CryptoTradeOperation::MaxBuySellQty);
267    let req = crypto_risk::GetMaxBuySellReq {
268        account: Some(crypto_risk_comm::Account {
269            cid: Some(ctx.require_customer_id("max_buy_sell_qty")?),
270            acct_id: Some(ctx.require_intra_acc_id("max_buy_sell_qty")?),
271            market: Some(input.account_market),
272            broker_id: Some(ctx.require_broker_id("max_buy_sell_qty")?),
273            long_acct_id: Some(ctx.acc_id),
274        }),
275        order_type: Some(input.order_type),
276        currency: Some(input.currency),
277        coin: Some(input.coin),
278        price: input.price,
279        order_id: input.order_id,
280    };
281    let resp = crate::command_runtime::execute_crypto_trade_command(
282        backend,
283        CryptoTradeOperation::MaxBuySellQty,
284        None,
285        bytes::Bytes::from(req.encode_to_vec()),
286    )
287    .await
288    .map_err(|e| {
289        tracing::warn!(
290            cmd_id = spec.cmd,
291            error = %e,
292            "crypto max buy/sell qty query failed"
293        );
294        e
295    })?;
296    let parsed: crypto_risk::GetMaxBuySellRsp =
297        Message::decode(resp.body.as_ref()).map_err(|e| {
298            tracing::warn!(
299                cmd_id = spec.cmd,
300                body_len = resp.body.len(),
301                error = %e,
302                "crypto max buy/sell qty decode failed"
303            );
304            FutuError::Proto(e)
305        })?;
306
307    let values = project_crypto_max_qty_backend_values_like_cpp(
308        parsed.max_cash_buy_qty.as_deref(),
309        parsed.max_sell_qty.as_deref(),
310    );
311
312    Ok(CryptoMaxBuySellQtyInfo {
313        max_cash_buy_qty: values.max_cash_buy_qty,
314        max_sell_qty: values.max_sell_qty,
315    })
316}