1use 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#[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 ¶ms.code,
41 )),
42 order_id_ex: None,
43 session: None,
44 position_id: None,
45 },
46 }
47}
48
49pub 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
73pub 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
99const RECONFIRM_SERIAL_INIT: u32 = 20_000_000;
104static RECONFIRM_SERIAL: AtomicU32 = AtomicU32::new(RECONFIRM_SERIAL_INIT);
105
106pub 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 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#[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
173pub 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
209pub 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#[cfg(test)]
250mod tests;
251
252#[derive(Debug, Clone)]
254pub struct OrderFeeItem {
255 pub title: String,
256 pub value: f64,
257}
258
259#[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
267pub 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#[derive(Debug, Clone)]
324pub struct MarginRatio {
325 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 pub im_long_ratio: f64,
333 pub im_short_ratio: f64,
335}
336
337pub 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
397fn market_to_prefix(m: i32) -> &'static str {
400 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}