1use futu_core::account_locator::{self, AccountCardRecord, AccountVisibilityRecord};
2use futu_core::error::{FutuError, Result};
3use futu_core::proto_id;
4use futu_net::client::FutuClient;
5
6use crate::read_plan;
7use crate::types::{Funds, Position, TrdHeader};
8
9pub async fn get_funds(client: &FutuClient, header: &TrdHeader) -> Result<Funds> {
15 get_funds_with_currency(client, header, None).await
16}
17
18pub async fn get_funds_with_currency(
28 client: &FutuClient,
29 header: &TrdHeader,
30 currency: Option<i32>,
31) -> Result<Funds> {
32 let req = futu_proto::trd_get_funds::Request {
33 c2s: futu_proto::trd_get_funds::C2s {
34 header: header.to_proto(),
35 refresh_cache: None,
36 currency,
37 asset_category: None,
38 },
39 };
40
41 let body = prost::Message::encode_to_vec(&req);
42 let resp_frame = client.request(proto_id::TRD_GET_FUNDS, body).await?;
43
44 let resp: futu_proto::trd_get_funds::Response =
45 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
46
47 if resp.ret_type != 0 {
48 return Err(crate::server_err(
49 resp.ret_type,
50 resp.ret_msg,
51 resp.err_code,
52 ));
53 }
54
55 let s2c = resp
56 .s2c
57 .ok_or(FutuError::Codec("missing s2c in GetFunds".into()))?;
58
59 let funds = s2c.funds.unwrap_or_else(|| {
67 tracing::warn!(
68 trd_env = ?header.trd_env,
69 acc_id = header.acc_id,
70 trd_market = ?header.trd_market,
71 "get_funds: s2c.funds is None (sim account or no data); returning empty Funds"
72 );
73 Default::default()
74 });
75
76 if let Some(warn_msg) = read_plan::funds_currency_mismatch_warning(currency, funds.currency) {
80 tracing::warn!(
81 acc_id = header.acc_id,
82 trd_market = ?header.trd_market,
83 requested_currency = ?currency,
84 returned_currency = ?funds.currency,
85 warning = %warn_msg,
86 "GetFunds: backend ignored requested currency and returned account base currency"
87 );
88 }
89
90 Ok(Funds::from_proto(&funds))
91}
92
93pub async fn get_position_list(client: &FutuClient, header: &TrdHeader) -> Result<Vec<Position>> {
95 get_position_list_with_filter_market(client, header, None).await
96}
97
98pub async fn get_position_list_with_filter_market(
105 client: &FutuClient,
106 header: &TrdHeader,
107 filter_market: Option<i32>,
108) -> Result<Vec<Position>> {
109 let req = build_get_position_list_request(header, filter_market);
110
111 let body = prost::Message::encode_to_vec(&req);
112 let resp_frame = client
113 .request(proto_id::TRD_GET_POSITION_LIST, body)
114 .await?;
115
116 let resp: futu_proto::trd_get_position_list::Response =
117 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
118
119 if resp.ret_type != 0 {
120 return Err(crate::server_err(
121 resp.ret_type,
122 resp.ret_msg,
123 resp.err_code,
124 ));
125 }
126
127 let s2c = resp
128 .s2c
129 .ok_or(FutuError::Codec("missing s2c in GetPositionList".into()))?;
130
131 Ok(s2c.position_list.iter().map(Position::from_proto).collect())
132}
133
134fn build_get_position_list_request(
135 header: &TrdHeader,
136 filter_market: Option<i32>,
137) -> futu_proto::trd_get_position_list::Request {
138 futu_proto::trd_get_position_list::Request {
139 c2s: futu_proto::trd_get_position_list::C2s {
140 header: header.to_proto(),
141 filter_conditions: filter_market.map(|market| {
142 futu_proto::trd_common::TrdFilterConditions {
143 code_list: vec![],
144 id_list: vec![],
145 begin_time: None,
146 end_time: None,
147 order_id_ex_list: vec![],
148 filter_market: Some(market),
149 }
150 }),
151 filter_pl_ratio_min: None,
152 filter_pl_ratio_max: None,
153 refresh_cache: None,
154 asset_category: None,
155 },
156 }
157}
158
159#[derive(Debug, Clone, Default)]
161pub struct UnlockTradeOutcome {
162 pub total_requested: usize,
164 pub total_unlocked: usize,
166 pub need_otp: bool,
168 pub failed_accounts: Vec<u64>,
170 pub message: Option<String>,
172}
173
174pub async fn unlock_trade(
187 client: &FutuClient,
188 pwd_md5: &str,
189 is_unlock: bool,
190 otp: Option<&str>,
191 security_firm: Option<i32>,
192 acc_ids: Vec<u64>,
195) -> Result<UnlockTradeOutcome> {
196 let req = futu_proto::trd_unlock_trade::Request {
197 c2s: futu_proto::trd_unlock_trade::C2s {
198 unlock: is_unlock,
199 pwd_md5: Some(pwd_md5.to_string()),
200 security_firm,
201 sec_otp: otp.map(String::from),
202 acc_ids,
203 },
204 };
205
206 let body = prost::Message::encode_to_vec(&req);
207 let resp_frame = client.request(proto_id::TRD_UNLOCK_TRADE, body).await?;
208
209 let resp: futu_proto::trd_unlock_trade::Response =
210 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
211
212 if resp.ret_type != 0 {
214 let s2c_ref = resp.s2c.as_ref();
215 let need_otp = s2c_ref.and_then(|s| s.need_otp).unwrap_or(false);
216 if need_otp {
217 let failed = s2c_ref
218 .map(|s| s.account_result_list.iter().map(|a| a.acc_id).collect())
219 .unwrap_or_default();
220 return Ok(UnlockTradeOutcome {
221 total_requested: s2c_ref.map(|s| s.account_result_list.len()).unwrap_or(0),
222 total_unlocked: 0,
223 need_otp: true,
224 failed_accounts: failed,
225 message: resp.ret_msg,
226 });
227 }
228 return Err(crate::server_err(
229 resp.ret_type,
230 resp.ret_msg,
231 resp.err_code,
232 ));
233 }
234
235 let s2c_ref = resp.s2c.as_ref();
237 let list = s2c_ref.map(|s| &s.account_result_list[..]).unwrap_or(&[]);
238 let total_requested = list.len();
239 let total_unlocked = list.iter().filter(|a| a.success).count();
240 let failed_accounts: Vec<u64> = list
241 .iter()
242 .filter(|a| !a.success)
243 .map(|a| a.acc_id)
244 .collect();
245 Ok(UnlockTradeOutcome {
246 total_requested,
247 total_unlocked,
248 need_otp: false,
249 failed_accounts,
250 message: resp.ret_msg,
251 })
252}
253
254pub async fn get_acc_list(client: &FutuClient) -> Result<Vec<TrdAcc>> {
261 get_acc_list_with_options(client, None, false).await
262}
263
264pub async fn get_acc_list_for_account_discovery(client: &FutuClient) -> Result<Vec<TrdAcc>> {
273 get_acc_list_with_options(client, None, true).await
274}
275
276pub fn is_app_visible_account(acc: &TrdAcc) -> bool {
285 account_locator::is_app_visible_account(acc)
286}
287
288pub fn app_visible_accounts(accs: Vec<TrdAcc>) -> Vec<TrdAcc> {
289 account_locator::app_visible_accounts(accs)
290}
291
292pub async fn get_acc_list_with_options(
294 client: &FutuClient,
295 trd_category: Option<i32>,
296 need_general_sec_account: bool,
297) -> Result<Vec<TrdAcc>> {
298 let req = build_get_acc_list_request(trd_category, need_general_sec_account);
299
300 let body = prost::Message::encode_to_vec(&req);
301 let resp_frame = client.request(proto_id::TRD_GET_ACC_LIST, body).await?;
302
303 let resp: futu_proto::trd_get_acc_list::Response =
304 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
305
306 if resp.ret_type != 0 {
307 return Err(crate::server_err(
308 resp.ret_type,
309 resp.ret_msg,
310 resp.err_code,
311 ));
312 }
313
314 let s2c = resp
315 .s2c
316 .ok_or(FutuError::Codec("missing s2c in GetAccList".into()))?;
317
318 Ok(s2c
319 .acc_list
320 .iter()
321 .map(|a| TrdAcc {
322 trd_env: a.trd_env,
323 acc_id: a.acc_id,
324 trd_market_auth_list: a.trd_market_auth_list.clone(),
325 acc_type: a.acc_type,
326 card_num: a.card_num.clone(),
327 security_firm: a.security_firm,
328 sim_acc_type: a.sim_acc_type,
329 uni_card_num: a.uni_card_num.clone(),
330 acc_status: a.acc_status,
331 acc_role: a.acc_role,
332 acc_label: a.acc_label.clone(),
333 jp_acc_type: a.jp_acc_type.clone(),
334 })
335 .collect())
336}
337
338fn build_get_acc_list_request(
339 trd_category: Option<i32>,
340 need_general_sec_account: bool,
341) -> futu_proto::trd_get_acc_list::Request {
342 futu_proto::trd_get_acc_list::Request {
343 c2s: futu_proto::trd_get_acc_list::C2s {
344 user_id: 0,
345 trd_category,
346 need_general_sec_account: Some(need_general_sec_account),
347 },
348 }
349}
350
351#[derive(Debug, Clone, Default)]
357pub struct TrdAcc {
358 pub trd_env: i32,
360 pub acc_id: u64,
362 pub trd_market_auth_list: Vec<i32>,
364 pub acc_type: Option<i32>,
366 pub card_num: Option<String>,
368 pub security_firm: Option<i32>,
371 pub sim_acc_type: Option<i32>,
373 pub uni_card_num: Option<String>,
375 pub acc_status: Option<i32>,
377 pub acc_role: Option<i32>,
379 pub acc_label: Option<String>,
383 pub jp_acc_type: Vec<i32>,
385}
386
387impl AccountCardRecord for TrdAcc {
388 fn acc_id(&self) -> u64 {
389 self.acc_id
390 }
391
392 fn card_num(&self) -> Option<&str> {
393 self.card_num.as_deref()
394 }
395
396 fn uni_card_num(&self) -> Option<&str> {
397 self.uni_card_num.as_deref()
398 }
399}
400
401impl AccountVisibilityRecord for TrdAcc {
402 fn trd_market_auth_list(&self) -> &[i32] {
403 &self.trd_market_auth_list
404 }
405
406 fn acc_label(&self) -> Option<&str> {
407 self.acc_label.as_deref()
408 }
409}
410
411#[cfg(test)]
412mod tests;