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
73fn 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
91pub 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
111pub 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
135const RECONFIRM_SERIAL_INIT: u32 = 20_000_000;
140static RECONFIRM_SERIAL: AtomicU32 = AtomicU32::new(RECONFIRM_SERIAL_INIT);
141
142pub 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 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#[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
209pub 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
245pub 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#[cfg(test)]
286mod tests;
287
288#[derive(Debug, Clone)]
290pub struct OrderFeeItem {
291 pub title: String,
292 pub value: f64,
293}
294
295#[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
303pub 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#[derive(Debug, Clone)]
360pub struct MarginRatio {
361 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 pub im_long_ratio: f64,
369 pub im_short_ratio: f64,
371}
372
373pub 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
433fn 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}