Skip to main content

futu_trd/
misc.rs

1// 交易杂项: 最大可买卖、订阅推送、确认订单、历史订单/成交
2
3use std::sync::atomic::{AtomicU32, Ordering};
4
5use futu_core::error::{FutuError, Result};
6use futu_core::proto_id;
7use futu_net::client::FutuClient;
8
9use crate::market::derive_sec_market;
10use crate::query::{Order, OrderFill};
11use crate::types::TrdHeader;
12
13// ===== 最大可买卖数量 =====
14
15/// 最大可买卖数量查询参数
16#[derive(Debug, Clone)]
17pub struct MaxTrdQtysParams {
18    pub header: TrdHeader,
19    pub order_type: i32,
20    pub code: String,
21    pub price: f64,
22    pub order_id: Option<u64>,
23}
24
25fn build_get_max_trd_qtys_request(
26    params: &MaxTrdQtysParams,
27) -> futu_proto::trd_get_max_trd_qtys::Request {
28    futu_proto::trd_get_max_trd_qtys::Request {
29        c2s: futu_proto::trd_get_max_trd_qtys::C2s {
30            header: params.header.to_proto(),
31            order_type: params.order_type,
32            code: params.code.clone(),
33            price: params.price,
34            order_id: params.order_id,
35            adjust_price: None,
36            adjust_side_and_limit: None,
37            sec_market: Some(derive_sec_market(
38                0,
39                params.header.trd_market as i32,
40                &params.code,
41            )),
42            order_id_ex: None,
43            session: None,
44            position_id: None,
45        },
46    }
47}
48
49/// 获取最大可买卖数量(原始 proto 响应)
50pub async fn get_max_trd_qtys(
51    client: &FutuClient,
52    params: &MaxTrdQtysParams,
53) -> Result<futu_proto::trd_get_max_trd_qtys::S2c> {
54    let req = build_get_max_trd_qtys_request(params);
55
56    let body = prost::Message::encode_to_vec(&req);
57    let resp_frame = client.request(proto_id::TRD_GET_MAX_TRD_QTYS, body).await?;
58    let resp: futu_proto::trd_get_max_trd_qtys::Response =
59        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
60
61    if resp.ret_type != 0 {
62        return Err(crate::server_err(
63            resp.ret_type,
64            resp.ret_msg,
65            resp.err_code,
66        ));
67    }
68
69    resp.s2c
70        .ok_or(FutuError::Codec("missing s2c in GetMaxTrdQtys".into()))
71}
72
73// ===== 订阅账户推送 =====
74
75fn build_sub_acc_push_request(acc_ids: &[u64]) -> futu_proto::trd_sub_acc_push::Request {
76    futu_proto::trd_sub_acc_push::Request {
77        c2s: futu_proto::trd_sub_acc_push::C2s {
78            acc_id_list: acc_ids.to_vec(),
79        },
80    }
81}
82
83fn build_unsub_acc_push_request(acc_ids: &[u64]) -> futu_proto::trd_sub_acc_push::Request {
84    build_sub_acc_push_request(acc_ids)
85}
86
87fn unsub_acc_push_proto_id() -> u32 {
88    proto_id::TRD_UNSUB_ACC_PUSH_LOCAL
89}
90
91/// 订阅交易账户的推送(订单/成交更新)
92pub async fn sub_acc_push(client: &FutuClient, acc_ids: &[u64]) -> Result<()> {
93    let req = build_sub_acc_push_request(acc_ids);
94
95    let body = prost::Message::encode_to_vec(&req);
96    let resp_frame = client.request(proto_id::TRD_SUB_ACC_PUSH, body).await?;
97    let resp: futu_proto::trd_sub_acc_push::Response =
98        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
99
100    if resp.ret_type != 0 {
101        return Err(crate::server_err(
102            resp.ret_type,
103            resp.ret_msg,
104            resp.err_code,
105        ));
106    }
107
108    Ok(())
109}
110
111/// 取消订阅交易账户的推送(订单/成交更新)。
112///
113/// The wire body intentionally reuses `Trd_SubAccPush.Request`; the daemon
114/// distinguishes unsubscribe through a local proto id routed to
115/// `UnsubAccPushHandler`.
116pub async fn unsub_acc_push(client: &FutuClient, acc_ids: &[u64]) -> Result<()> {
117    let req = build_unsub_acc_push_request(acc_ids);
118
119    let body = prost::Message::encode_to_vec(&req);
120    let resp_frame = client.request(unsub_acc_push_proto_id(), body).await?;
121    let resp: futu_proto::trd_sub_acc_push::Response =
122        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
123
124    if resp.ret_type != 0 {
125        return Err(crate::server_err(
126            resp.ret_type,
127            resp.ret_msg,
128            resp.err_code,
129        ));
130    }
131
132    Ok(())
133}
134
135// ===== 确认订单 =====
136
137/// v1.4.71: RECONFIRM serial_no 起点(从 20,000,000 开始避免与其他 serial 冲突)。
138/// 命名 const 避免魔法数。
139const RECONFIRM_SERIAL_INIT: u32 = 20_000_000;
140static RECONFIRM_SERIAL: AtomicU32 = AtomicU32::new(RECONFIRM_SERIAL_INIT);
141
142/// 再次确认订单
143pub async fn reconfirm_order(
144    client: &FutuClient,
145    header: &TrdHeader,
146    order_id: u64,
147    reason: i32,
148) -> Result<u64> {
149    let serial = RECONFIRM_SERIAL.fetch_add(1, Ordering::Relaxed);
150    let req = futu_proto::trd_reconfirm_order::Request {
151        c2s: futu_proto::trd_reconfirm_order::C2s {
152            packet_id: futu_proto::common::PacketId {
153                // Echo InitConnect S2C connID like C++ FTAPI clients. The
154                // gateway replay guard compares it with the actual TCP conn_id.
155                conn_id: client.conn_id().ok_or(FutuError::NotInitialized)?,
156                serial_no: serial,
157            },
158            header: header.to_proto(),
159            order_id,
160            reconfirm_reason: reason,
161        },
162    };
163
164    let body = prost::Message::encode_to_vec(&req);
165    let resp_frame = client.request(proto_id::TRD_RECONFIRM_ORDER, body).await?;
166    let resp: futu_proto::trd_reconfirm_order::Response =
167        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
168
169    if resp.ret_type != 0 {
170        return Err(crate::server_err(
171            resp.ret_type,
172            resp.ret_msg,
173            resp.err_code,
174        ));
175    }
176
177    let s2c = resp
178        .s2c
179        .ok_or(FutuError::Codec("missing s2c in ReconfirmOrder".into()))?;
180
181    Ok(s2c.order_id)
182}
183
184// ===== 历史订单 =====
185
186/// 历史订单过滤条件
187#[derive(Debug, Clone)]
188pub struct HistoryFilterConditions {
189    pub code_list: Vec<String>,
190    pub id_list: Vec<u64>,
191    pub begin_time: Option<String>,
192    pub end_time: Option<String>,
193    pub filter_market: Option<i32>,
194}
195
196impl HistoryFilterConditions {
197    pub fn to_proto(&self) -> futu_proto::trd_common::TrdFilterConditions {
198        futu_proto::trd_common::TrdFilterConditions {
199            code_list: self.code_list.clone(),
200            id_list: self.id_list.clone(),
201            begin_time: self.begin_time.clone(),
202            end_time: self.end_time.clone(),
203            order_id_ex_list: vec![],
204            filter_market: self.filter_market,
205        }
206    }
207}
208
209/// 查询历史订单列表
210pub async fn get_history_order_list(
211    client: &FutuClient,
212    header: &TrdHeader,
213    filter: &HistoryFilterConditions,
214) -> Result<Vec<Order>> {
215    let req = futu_proto::trd_get_history_order_list::Request {
216        c2s: futu_proto::trd_get_history_order_list::C2s {
217            header: header.to_proto(),
218            filter_conditions: filter.to_proto(),
219            filter_status_list: vec![],
220        },
221    };
222
223    let body = prost::Message::encode_to_vec(&req);
224    let resp_frame = client
225        .request(proto_id::TRD_GET_HISTORY_ORDER_LIST, body)
226        .await?;
227    let resp: futu_proto::trd_get_history_order_list::Response =
228        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
229
230    if resp.ret_type != 0 {
231        return Err(crate::server_err(
232            resp.ret_type,
233            resp.ret_msg,
234            resp.err_code,
235        ));
236    }
237
238    let s2c = resp.s2c.ok_or(FutuError::Codec(
239        "missing s2c in GetHistoryOrderList".into(),
240    ))?;
241
242    Ok(s2c.order_list.iter().map(Order::from_proto).collect())
243}
244
245/// 查询历史成交列表
246pub async fn get_history_order_fill_list(
247    client: &FutuClient,
248    header: &TrdHeader,
249    filter: &HistoryFilterConditions,
250) -> Result<Vec<OrderFill>> {
251    let req = futu_proto::trd_get_history_order_fill_list::Request {
252        c2s: futu_proto::trd_get_history_order_fill_list::C2s {
253            header: header.to_proto(),
254            filter_conditions: filter.to_proto(),
255        },
256    };
257
258    let body = prost::Message::encode_to_vec(&req);
259    let resp_frame = client
260        .request(proto_id::TRD_GET_HISTORY_ORDER_FILL_LIST, body)
261        .await?;
262    let resp: futu_proto::trd_get_history_order_fill_list::Response =
263        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
264
265    if resp.ret_type != 0 {
266        return Err(crate::server_err(
267            resp.ret_type,
268            resp.ret_msg,
269            resp.err_code,
270        ));
271    }
272
273    let s2c = resp.s2c.ok_or(FutuError::Codec(
274        "missing s2c in GetHistoryOrderFillList".into(),
275    ))?;
276
277    s2c.order_fill_list
278        .iter()
279        .map(OrderFill::from_proto)
280        .collect()
281}
282
283// ===== 订单费用查询(CMD 2225 TRD_GET_ORDER_FEE)=====
284
285#[cfg(test)]
286mod tests;
287
288/// 订单费用明细条目
289#[derive(Debug, Clone)]
290pub struct OrderFeeItem {
291    pub title: String,
292    pub value: f64,
293}
294
295/// 单个订单费用
296#[derive(Debug, Clone)]
297pub struct OrderFee {
298    pub order_id_ex: String,
299    pub fee_amount: f64,
300    pub fee_list: Vec<OrderFeeItem>,
301}
302
303/// 查询订单费用(按 `order_id_ex` 列表)
304///
305/// 对齐 C++ `TRD_GET_ORDER_FEE`(proto_id 2225),接收扩展订单号列表,
306/// 返回每个订单的费用总额 + 明细拆分(佣金、平台费、印花税等)。
307///
308/// 典型调用时机:下单后或撤单前估算费用。
309pub async fn get_order_fee(
310    client: &FutuClient,
311    header: &TrdHeader,
312    order_id_ex_list: &[String],
313) -> Result<Vec<OrderFee>> {
314    let req = futu_proto::trd_get_order_fee::Request {
315        c2s: futu_proto::trd_get_order_fee::C2s {
316            header: header.to_proto(),
317            order_id_ex_list: order_id_ex_list.to_vec(),
318        },
319    };
320
321    let body = prost::Message::encode_to_vec(&req);
322    let resp_frame = client.request(proto_id::TRD_GET_ORDER_FEE, body).await?;
323    let resp: futu_proto::trd_get_order_fee::Response =
324        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
325
326    if resp.ret_type != 0 {
327        return Err(crate::server_err(
328            resp.ret_type,
329            resp.ret_msg,
330            resp.err_code,
331        ));
332    }
333
334    let s2c = resp
335        .s2c
336        .ok_or(FutuError::Codec("missing s2c in GetOrderFee".into()))?;
337
338    Ok(s2c
339        .order_fee_list
340        .iter()
341        .map(|o| OrderFee {
342            order_id_ex: o.order_id_ex.clone(),
343            fee_amount: o.fee_amount.unwrap_or(0.0),
344            fee_list: o
345                .fee_list
346                .iter()
347                .map(|i| OrderFeeItem {
348                    title: i.title.clone().unwrap_or_default(),
349                    value: i.value.unwrap_or(0.0),
350                })
351                .collect(),
352        })
353        .collect())
354}
355
356// ===== 融资融券比率查询(CMD 2223 TRD_GET_MARGIN_RATIO)=====
357
358/// 单个标的的融资融券比率信息(精简版)
359#[derive(Debug, Clone)]
360pub struct MarginRatio {
361    /// 市场 + 代码(用 Python SDK 的 `HK.00700` 格式字符串,方便上层展示)
362    pub code: String,
363    pub is_long_permit: bool,
364    pub is_short_permit: bool,
365    pub short_pool_remain: f64,
366    pub short_fee_rate: f64,
367    /// 融资初始保证金率
368    pub im_long_ratio: f64,
369    /// 融券初始保证金率
370    pub im_short_ratio: f64,
371}
372
373/// 按标的列表查询融资融券比率。对齐 C++ `TRD_GET_MARGIN_RATIO`(2223)
374/// 和 Python SDK `OpenTradeContext.get_margin_ratio`。
375///
376/// `securities` 是 `(market, code)` 元组列表,market 对齐 Qot_Common.QotMarket。
377pub async fn get_margin_ratio(
378    client: &FutuClient,
379    header: &TrdHeader,
380    securities: &[(i32, String)],
381) -> Result<Vec<MarginRatio>> {
382    use futu_proto::qot_common;
383    let sec_list: Vec<qot_common::Security> = securities
384        .iter()
385        .map(|(m, c)| qot_common::Security {
386            market: *m,
387            code: c.clone(),
388        })
389        .collect();
390    let req = futu_proto::trd_get_margin_ratio::Request {
391        c2s: futu_proto::trd_get_margin_ratio::C2s {
392            header: header.to_proto(),
393            security_list: sec_list,
394        },
395    };
396
397    let body = prost::Message::encode_to_vec(&req);
398    let resp_frame = client.request(proto_id::TRD_GET_MARGIN_RATIO, body).await?;
399    let resp: futu_proto::trd_get_margin_ratio::Response =
400        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
401
402    if resp.ret_type != 0 {
403        return Err(crate::server_err(
404            resp.ret_type,
405            resp.ret_msg,
406            resp.err_code,
407        ));
408    }
409
410    let s2c = resp
411        .s2c
412        .ok_or(FutuError::Codec("missing s2c in GetMarginRatio".into()))?;
413
414    Ok(s2c
415        .margin_ratio_info_list
416        .iter()
417        .map(|m| MarginRatio {
418            code: format!(
419                "{}.{}",
420                market_to_prefix(m.security.market),
421                m.security.code
422            ),
423            is_long_permit: m.is_long_permit.unwrap_or(false),
424            is_short_permit: m.is_short_permit.unwrap_or(false),
425            short_pool_remain: m.short_pool_remain.unwrap_or(0.0),
426            short_fee_rate: m.short_fee_rate.unwrap_or(0.0),
427            im_long_ratio: m.im_long_ratio.unwrap_or(0.0),
428            im_short_ratio: m.im_short_ratio.unwrap_or(0.0),
429        })
430        .collect())
431}
432
433/// QotMarket → `HK` / `US` / `SH` / `SZ` 等前缀(对齐 Python SDK 的
434/// `MARKET.CODE` 约定)。未知 market 返回 `"UNK"`。
435fn market_to_prefix(m: i32) -> &'static str {
436    futu_core::market::qot_market_display_prefix(futu_core::market::QotMarketId::new(m))
437        .unwrap_or("UNK")
438}