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