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        // C++ NNProto_Trd_FlowSummaryCrypto::OnReply_PullFlowSummary initializes nSvrRet=0
120        // and only overwrites it when GetCashLogRsp.has_result().
121        // Ref: FutuOpenD/Src/NNProtoCenter/Trade/CashFlow/NNProto_Trd_FlowSummaryCrypto.cpp:126-146.
122        let result = parsed.result.unwrap_or(0);
123        if result != 0 {
124            return Err(FutuError::ServerError {
125                ret_type: result,
126                msg: parsed
127                    .err_msg
128                    .unwrap_or_else(|| "query_crypto_cash_logs: backend error".to_string()),
129            });
130        }
131
132        all_logs.extend(
133            parsed
134                .monthly_log_list
135                .iter()
136                .flat_map(|month| month.cash_log_list.iter())
137                .filter_map(project_crypto_cash_log),
138        );
139
140        if parsed.has_more.unwrap_or(false) {
141            match parsed.next_log_id {
142                Some(ref next) if !next.is_empty() => {
143                    log_id = Some(next.clone());
144                    continue;
145                }
146                _ => {
147                    return Err(FutuError::Codec(
148                        "query_crypto_cash_logs: has_more without next_log_id".to_string(),
149                    ));
150                }
151            }
152        }
153
154        tracing::debug!(acc_id, count = all_logs.len(), "crypto cash logs queried");
155        return Ok(all_logs);
156    }
157
158    Err(FutuError::Codec(format!(
159        "query_crypto_cash_logs: pagination exceeded {MAX_PAGES} pages"
160    )))
161}
162
163/// Query crypto trade configs through CMD20102.
164///
165/// C++ `NNProto_Trd_CryptoTradeConfig.cpp:14-23,63-89` pulls the full trade
166/// config list per broker and stores it in `INNData_Trd_CryptoTradeConfig`.
167/// MaxBuySellQty needs the same config for min quantity and quantity increment.
168pub async fn query_crypto_trade_configs(
169    backend: &BackendConn,
170    broker_id: u32,
171) -> Result<Vec<CryptoTradeConfig>> {
172    use prost::Message;
173
174    let spec = crypto_trade_command(CryptoTradeOperation::FetchTradeConfig);
175    let req = inbound_oe::FetchTradeConfigRequest {
176        currency_pair_list: Vec::new(),
177        data_from: None,
178        data_max_count: None,
179        symbol_list: Vec::new(),
180        status: None,
181    };
182    let resp = backend
183        .request(spec.cmd, req.encode_to_vec())
184        .await
185        .map_err(|e| {
186            tracing::warn!(broker_id, cmd_id = spec.cmd, error = %e, "crypto trade config query failed");
187            e
188        })?;
189
190    let parsed: inbound_oe::FetchTradeConfigResponse = Message::decode(resp.body.as_ref())
191        .map_err(|e| {
192            tracing::warn!(
193                broker_id,
194                cmd_id = spec.cmd,
195                body_len = resp.body.len(),
196                error = %e,
197                "crypto trade config decode failed"
198            );
199            FutuError::Proto(e)
200        })?;
201    crypto_trade_config_response_status(&parsed)?;
202
203    Ok(parsed
204        .full_trade_config_list
205        .iter()
206        .filter_map(project_crypto_trade_config)
207        .collect())
208}
209
210/// Query crypto max buy/sell quantity through CMD20622.
211///
212/// C++ `NNProto_Trd_MaxQtyCrypto.cpp:14-38,49-78` builds
213/// `crypto_risk::GetMaxBuySellReq` from account context, `cc_origin`, and
214/// `cc_destination`.
215#[derive(Debug, Clone)]
216pub struct CryptoMaxBuySellQtyRequest {
217    pub account_market: u32,
218    pub order_type: u32,
219    pub coin: String,
220    pub currency: String,
221    pub price: Option<String>,
222    pub order_id: Option<String>,
223}
224
225pub async fn query_crypto_max_buy_sell_qty(
226    backend: &BackendConn,
227    ctx: &CryptoAccountContext,
228    input: CryptoMaxBuySellQtyRequest,
229) -> Result<CryptoMaxBuySellQtyInfo> {
230    use prost::Message;
231
232    let spec = crypto_trade_command(CryptoTradeOperation::MaxBuySellQty);
233    let req = crypto_risk::GetMaxBuySellReq {
234        account: Some(crypto_risk_comm::Account {
235            cid: Some(ctx.require_customer_id("max_buy_sell_qty")?),
236            acct_id: Some(ctx.require_intra_acc_id("max_buy_sell_qty")?),
237            market: Some(input.account_market),
238            broker_id: Some(ctx.require_broker_id("max_buy_sell_qty")?),
239            long_acct_id: Some(ctx.acc_id),
240        }),
241        order_type: Some(input.order_type),
242        currency: Some(input.currency),
243        coin: Some(input.coin),
244        price: input.price,
245        order_id: input.order_id,
246    };
247    let resp = backend
248        .request(spec.cmd, req.encode_to_vec())
249        .await
250        .map_err(|e| {
251            tracing::warn!(
252                acc_id = ctx.acc_id,
253                cmd_id = spec.cmd,
254                error = %e,
255                "crypto max buy/sell qty query failed"
256            );
257            e
258        })?;
259    let parsed: crypto_risk::GetMaxBuySellRsp =
260        Message::decode(resp.body.as_ref()).map_err(|e| {
261            tracing::warn!(
262                acc_id = ctx.acc_id,
263                cmd_id = spec.cmd,
264                body_len = resp.body.len(),
265                error = %e,
266                "crypto max buy/sell qty decode failed"
267            );
268            FutuError::Proto(e)
269        })?;
270
271    Ok(CryptoMaxBuySellQtyInfo {
272        max_cash_buy_qty: pf(&parsed.max_cash_buy_qty),
273        max_sell_qty: pf(&parsed.max_sell_qty),
274    })
275}