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};
6
7use super::super::common::pf;
8use super::super::*;
9
10use crate::crypto_trade::{CryptoAccountContext, lookup_crypto_account_context};
11use crate::proto_internal::cash_change_detail_cmn;
12use crate::trade_cmd::{CryptoTradeOperation, crypto_trade_command};
13
14use super::projections::*;
15use super::types::*;
16
17pub async fn query_crypto_order_fees(
18    backend: &BackendConn,
19    acc_id: u64,
20    trd_cache: &TrdCache,
21    order_ids: &[String],
22) -> Result<Vec<CryptoOrderFeeInfo>> {
23    use prost::Message;
24
25    let _ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
26    let spec = crypto_trade_command(CryptoTradeOperation::BatchOrderFee);
27    let req = inbound_oe::BatchQueryOrderFeeReq {
28        order_id_list: order_ids.to_vec(),
29        long_account_id: Some(acc_id),
30    };
31    let resp = backend
32        .request(spec.cmd, req.encode_to_vec())
33        .await
34        .map_err(|e| {
35            tracing::warn!(
36                acc_id,
37                cmd_id = spec.cmd,
38                error = %e,
39                "crypto order fee query failed"
40            );
41            e
42        })?;
43
44    let parsed: inbound_oe::BatchQueryOrderFeeRsp =
45        Message::decode(resp.body.as_ref()).map_err(|e| {
46            tracing::warn!(
47                acc_id,
48                cmd_id = spec.cmd,
49                body_len = resp.body.len(),
50                error = %e,
51                "crypto order fee query decode failed"
52            );
53            FutuError::Proto(e)
54        })?;
55
56    let fees = parsed
57        .fee_group_list
58        .iter()
59        .filter_map(project_crypto_order_fee)
60        .collect();
61    Ok(fees)
62}
63
64/// Query crypto cash logs through CMD20632 for FTAPI Trd_FlowSummary.
65///
66/// C++ 10.5.6508 `NNProto_Trd_FlowSummaryCrypto.cpp:191-219` sends
67/// `cash_change_detail_cmn::GetCashLogReq` with `MARKET_CRYPTO`, public long
68/// account id, backend intra account id, and broker id.
69pub async fn query_crypto_cash_logs(
70    backend: &BackendConn,
71    acc_id: u64,
72    trd_cache: &TrdCache,
73    begin_time: u64,
74    end_time: u64,
75) -> Result<Vec<CryptoCashLogInfo>> {
76    use prost::Message;
77
78    let ctx = lookup_crypto_account_context(trd_cache, acc_id)?;
79    let spec = crypto_trade_command(CryptoTradeOperation::CashLog);
80    let mut all_logs = Vec::new();
81    let mut log_id: Option<String> = None;
82
83    for _ in 0..MAX_PAGES {
84        let req = cash_change_detail_cmn::GetCashLogReq {
85            market: Some(cash_change_detail_cmn::Market::Crypto as u32),
86            account_id: Some(ctx.require_intra_acc_id("crypto_cash_log")?),
87            broker_id: Some(ctx.require_broker_id("crypto_cash_log")?),
88            long_account_id: Some(acc_id),
89            begin_time: Some(begin_time),
90            end_time: Some(end_time),
91            log_id: log_id.clone(),
92            ..Default::default()
93        };
94        let resp = backend
95            .request(spec.cmd, req.encode_to_vec())
96            .await
97            .map_err(|e| {
98                tracing::warn!(
99                    acc_id,
100                    cmd_id = spec.cmd,
101                    error = %e,
102                    "crypto cash log query failed"
103                );
104                e
105            })?;
106
107        let parsed: cash_change_detail_cmn::GetCashLogRsp = Message::decode(resp.body.as_ref())
108            .map_err(|e| {
109                tracing::warn!(
110                    acc_id,
111                    cmd_id = spec.cmd,
112                    body_len = resp.body.len(),
113                    error = %e,
114                    "crypto cash log decode failed"
115                );
116                FutuError::Proto(e)
117            })?;
118
119        let result = parsed.result.unwrap_or(0);
120        if result != 0 {
121            return Err(FutuError::ServerError {
122                ret_type: result,
123                msg: parsed
124                    .err_msg
125                    .unwrap_or_else(|| "query_crypto_cash_logs: backend error".to_string()),
126            });
127        }
128
129        all_logs.extend(
130            parsed
131                .monthly_log_list
132                .iter()
133                .flat_map(|month| month.cash_log_list.iter())
134                .filter_map(project_crypto_cash_log),
135        );
136
137        if parsed.has_more.unwrap_or(false) {
138            match parsed.next_log_id {
139                Some(ref next) if !next.is_empty() => {
140                    log_id = Some(next.clone());
141                    continue;
142                }
143                _ => {
144                    return Err(FutuError::Codec(
145                        "query_crypto_cash_logs: has_more without next_log_id".to_string(),
146                    ));
147                }
148            }
149        }
150
151        tracing::debug!(acc_id, count = all_logs.len(), "crypto cash logs queried");
152        return Ok(all_logs);
153    }
154
155    Err(FutuError::Codec(format!(
156        "query_crypto_cash_logs: pagination exceeded {MAX_PAGES} pages"
157    )))
158}
159
160/// Query crypto trade configs through CMD20102.
161///
162/// C++ `NNProto_Trd_CryptoTradeConfig.cpp:14-23,63-89` pulls the full trade
163/// config list per broker and stores it in `INNData_Trd_CryptoTradeConfig`.
164/// MaxBuySellQty needs the same config for min quantity and quantity increment.
165pub async fn query_crypto_trade_configs(
166    backend: &BackendConn,
167    broker_id: u32,
168) -> Result<Vec<CryptoTradeConfig>> {
169    use prost::Message;
170
171    let spec = crypto_trade_command(CryptoTradeOperation::FetchTradeConfig);
172    let req = inbound_oe::FetchTradeConfigRequest {
173        currency_pair_list: Vec::new(),
174        data_from: None,
175        data_max_count: None,
176        symbol_list: Vec::new(),
177        status: None,
178    };
179    let resp = backend
180        .request(spec.cmd, req.encode_to_vec())
181        .await
182        .map_err(|e| {
183            tracing::warn!(broker_id, cmd_id = spec.cmd, error = %e, "crypto trade config query failed");
184            e
185        })?;
186
187    let parsed: inbound_oe::FetchTradeConfigResponse = Message::decode(resp.body.as_ref())
188        .map_err(|e| {
189            tracing::warn!(
190                broker_id,
191                cmd_id = spec.cmd,
192                body_len = resp.body.len(),
193                error = %e,
194                "crypto trade config decode failed"
195            );
196            FutuError::Proto(e)
197        })?;
198    crypto_trade_config_response_status(&parsed)?;
199
200    Ok(parsed
201        .full_trade_config_list
202        .iter()
203        .filter_map(project_crypto_trade_config)
204        .collect())
205}
206
207/// Query crypto max buy/sell quantity through CMD20622.
208///
209/// C++ `NNProto_Trd_MaxQtyCrypto.cpp:14-38,49-78` builds
210/// `crypto_risk::GetMaxBuySellReq` from account context, `cc_origin`, and
211/// `cc_destination`.
212//
213// v1.4.109: 协议必填字段, 拆 struct 反而劣化调用点可读性. clippy allow.
214#[allow(clippy::too_many_arguments)]
215pub async fn query_crypto_max_buy_sell_qty(
216    backend: &BackendConn,
217    ctx: &CryptoAccountContext,
218    account_market: u32,
219    order_type: u32,
220    coin: &str,
221    currency: &str,
222    price: Option<String>,
223    order_id: Option<String>,
224) -> Result<CryptoMaxBuySellQtyInfo> {
225    use prost::Message;
226
227    let spec = crypto_trade_command(CryptoTradeOperation::MaxBuySellQty);
228    let req = crypto_risk::GetMaxBuySellReq {
229        account: Some(crypto_risk_comm::Account {
230            cid: Some(ctx.require_customer_id("max_buy_sell_qty")?),
231            acct_id: Some(ctx.require_intra_acc_id("max_buy_sell_qty")?),
232            market: Some(account_market),
233            broker_id: Some(ctx.require_broker_id("max_buy_sell_qty")?),
234            long_acct_id: Some(ctx.acc_id),
235        }),
236        order_type: Some(order_type),
237        currency: Some(currency.to_string()),
238        coin: Some(coin.to_string()),
239        price,
240        order_id,
241    };
242    let resp = backend
243        .request(spec.cmd, req.encode_to_vec())
244        .await
245        .map_err(|e| {
246            tracing::warn!(
247                acc_id = ctx.acc_id,
248                cmd_id = spec.cmd,
249                error = %e,
250                "crypto max buy/sell qty query failed"
251            );
252            e
253        })?;
254    let parsed: crypto_risk::GetMaxBuySellRsp =
255        Message::decode(resp.body.as_ref()).map_err(|e| {
256            tracing::warn!(
257                acc_id = ctx.acc_id,
258                cmd_id = spec.cmd,
259                body_len = resp.body.len(),
260                error = %e,
261                "crypto max buy/sell qty decode failed"
262            );
263            FutuError::Proto(e)
264        })?;
265
266    Ok(CryptoMaxBuySellQtyInfo {
267        max_cash_buy_qty: pf(&parsed.max_cash_buy_qty),
268        max_sell_qty: pf(&parsed.max_sell_qty),
269    })
270}