1use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use futu_net::client::FutuClient;
8use futu_trd::types::TrdHeader;
9use serde::Serialize;
10
11use super::helpers::*;
12use super::orders::HistoryQueryInput;
13
14#[derive(Serialize)]
15struct HistoryDealOut {
16 fill_id: u64,
17 fill_id_ex: String,
18 order_id: u64,
19 trd_side: i32,
20 code: String,
21 name: String,
22 qty: f64,
23 price: f64,
24 create_time: String,
25}
26
27pub async fn get_history_deals(
29 client: &Arc<FutuClient>,
30 input: HistoryQueryInput<'_>,
31) -> Result<String> {
32 let header = build_header(input.env, input.acc_id, input.market)?;
33 let filter = futu_trd::misc::HistoryFilterConditions {
34 code_list: input.code_list,
35 id_list: vec![],
36 begin_time: input.begin_time,
37 end_time: input.end_time,
38 filter_market: Some(header.trd_market as i32),
39 };
40 let list = futu_trd::misc::get_history_order_fill_list(client, &header, &filter).await?;
41 let out: Vec<HistoryDealOut> = list
42 .iter()
43 .map(|f| HistoryDealOut {
44 fill_id: f.fill_id,
45 fill_id_ex: f.fill_id_ex.clone(),
46 order_id: f.order_id,
47 trd_side: f.trd_side,
48 code: f.code.clone(),
49 name: f.name.clone(),
50 qty: f.qty,
51 price: f.price,
52 create_time: f.create_time.clone(),
53 })
54 .collect();
55 Ok(serde_json::to_string_pretty(&out)?)
56}
57
58#[derive(Serialize)]
59struct MarginRatioOut {
60 code: String,
61 is_long_permit: bool,
62 is_short_permit: bool,
63 short_pool_remain: f64,
64 short_fee_rate: f64,
65 im_long_ratio: f64,
66 im_short_ratio: f64,
67}
68
69pub async fn get_margin_ratio(
74 client: &Arc<FutuClient>,
75 env: &str,
76 acc_id: u64,
77 market: &str,
78 codes: &[String],
79) -> Result<String> {
80 let header = build_header_strict_no_fund(env, acc_id, market)?;
81 let secs: Vec<(i32, String)> = codes.iter().filter_map(|s| parse_market_code(s)).collect();
82 if secs.is_empty() {
83 bail!("no valid MARKET.CODE in codes list");
84 }
85 let list = futu_trd::misc::get_margin_ratio(client, &header, &secs).await?;
86 let out: Vec<MarginRatioOut> = list
87 .into_iter()
88 .map(|m| MarginRatioOut {
89 code: m.code,
90 is_long_permit: m.is_long_permit,
91 is_short_permit: m.is_short_permit,
92 short_pool_remain: m.short_pool_remain,
93 short_fee_rate: m.short_fee_rate,
94 im_long_ratio: m.im_long_ratio,
95 im_short_ratio: m.im_short_ratio,
96 })
97 .collect();
98 Ok(serde_json::to_string_pretty(&out)?)
99}
100
101pub fn parse_market_code(s: &str) -> Option<(i32, String)> {
104 let mut parts = s.splitn(2, '.');
105 let mkt = parts.next()?.to_ascii_uppercase();
106 let code = parts.next()?.to_string();
107 let m = match mkt.as_str() {
108 "HK" => 1,
109 "US" => 11,
110 "SH" => 21,
111 "SZ" => 22,
112 "SG" => 31,
113 "JP" => 41,
114 "AU" => 42,
115 _ => return None,
116 };
117 Some((m, code))
118}
119
120pub async fn sub_acc_push(client: &Arc<FutuClient>, acc_ids: &[u64]) -> Result<String> {
126 futu_trd::misc::sub_acc_push(client, acc_ids).await?;
127 Ok(serde_json::json!({ "ok": true, "subscribed_acc_ids": acc_ids }).to_string())
128}
129
130#[derive(Serialize)]
135struct FlowSummaryItemOut {
136 clearing_date: Option<String>,
137 settlement_date: Option<String>,
138 currency: Option<i32>,
139 cash_flow_type: Option<String>,
140 cash_flow_direction: Option<i32>,
141 cash_flow_amount: Option<f64>,
142 cash_flow_remark: Option<String>,
143 cash_flow_id: Option<u64>,
144}
145
146pub async fn get_acc_cash_flow(
149 client: &Arc<FutuClient>,
150 env: &str,
151 acc_id: u64,
152 market: &str,
153 clearing_date: &str,
154 direction: Option<i32>,
155) -> Result<String> {
156 let trd_env = parse_trd_env(env)?;
159 let trd_market = parse_trd_market(market)?;
160 let header = TrdHeader {
161 trd_env,
162 acc_id,
163 trd_market,
164 jp_acc_type: None,
165 };
166 let proto_header = futu_proto::trd_common::TrdHeader {
167 trd_env: header.trd_env as i32,
168 acc_id: header.acc_id,
169 trd_market: header.trd_market as i32,
170 jp_acc_type: None,
171 };
172 let req = futu_proto::trd_flow_summary::Request {
173 c2s: futu_proto::trd_flow_summary::C2s {
174 header: proto_header,
175 clearing_date: clearing_date.to_string(),
176 cash_flow_direction: direction,
177 start_create_date: None,
178 end_create_date: None,
179 },
180 };
181 let body = prost::Message::encode_to_vec(&req);
182 let frame = client
183 .request(futu_core::proto_id::TRD_FLOW_SUMMARY, body)
184 .await?;
185 let resp =
186 <futu_proto::trd_flow_summary::Response as prost::Message>::decode(frame.body.as_ref())
187 .map_err(|e| anyhow::anyhow!("decode flow_summary: {e}"))?;
188 if resp.ret_type != 0 {
189 bail!(
190 "flow_summary ret_type={} msg={:?}",
191 resp.ret_type,
192 resp.ret_msg
193 );
194 }
195 let s2c = resp.s2c.ok_or_else(|| anyhow::anyhow!("missing s2c"))?;
196 let out: Vec<FlowSummaryItemOut> = s2c
197 .flow_summary_info_list
198 .iter()
199 .map(|f| FlowSummaryItemOut {
200 clearing_date: f.clearing_date.clone(),
201 settlement_date: f.settlement_date.clone(),
202 currency: f.currency,
203 cash_flow_type: f.cash_flow_type.clone(),
204 cash_flow_direction: f.cash_flow_direction,
205 cash_flow_amount: f.cash_flow_amount,
206 cash_flow_remark: f.cash_flow_remark.clone(),
207 cash_flow_id: f.cash_flow_id,
208 })
209 .collect();
210 Ok(serde_json::to_string_pretty(&out)?)
211}