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_options(client, header, PositionListOptions::default()).await
96}
97
98#[derive(Debug, Clone, Copy, Default)]
99pub struct PositionListOptions {
100 pub filter_market: Option<i32>,
102 pub currency: Option<i32>,
105 pub option_strategy_view: Option<bool>,
107}
108
109pub async fn get_position_list_with_filter_market(
116 client: &FutuClient,
117 header: &TrdHeader,
118 filter_market: Option<i32>,
119) -> Result<Vec<Position>> {
120 get_position_list_with_options(
121 client,
122 header,
123 PositionListOptions {
124 filter_market,
125 ..Default::default()
126 },
127 )
128 .await
129}
130
131pub async fn get_position_list_with_options(
132 client: &FutuClient,
133 header: &TrdHeader,
134 options: PositionListOptions,
135) -> Result<Vec<Position>> {
136 let req = build_get_position_list_request(header, options);
137
138 let body = prost::Message::encode_to_vec(&req);
139 let resp_frame = client
140 .request(proto_id::TRD_GET_POSITION_LIST, body)
141 .await?;
142
143 let resp: futu_proto::trd_get_position_list::Response =
144 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
145
146 if resp.ret_type != 0 {
147 return Err(crate::server_err(
148 resp.ret_type,
149 resp.ret_msg,
150 resp.err_code,
151 ));
152 }
153
154 let s2c = resp
155 .s2c
156 .ok_or(FutuError::Codec("missing s2c in GetPositionList".into()))?;
157
158 Ok(s2c.position_list.iter().map(Position::from_proto).collect())
159}
160
161fn build_get_position_list_request(
162 header: &TrdHeader,
163 options: PositionListOptions,
164) -> futu_proto::trd_get_position_list::Request {
165 futu_proto::trd_get_position_list::Request {
166 c2s: futu_proto::trd_get_position_list::C2s {
167 header: header.to_proto(),
168 filter_conditions: options.filter_market.map(|market| {
169 futu_proto::trd_common::TrdFilterConditions {
170 code_list: vec![],
171 id_list: vec![],
172 begin_time: None,
173 end_time: None,
174 order_id_ex_list: vec![],
175 filter_market: Some(market),
176 }
177 }),
178 filter_pl_ratio_min: None,
179 filter_pl_ratio_max: None,
180 refresh_cache: None,
181 asset_category: None,
182 currency: options.currency,
183 option_strategy_view: options.option_strategy_view,
184 },
185 }
186}
187
188#[derive(Debug, Clone, Default)]
190pub struct UnlockTradeOutcome {
191 pub total_requested: usize,
193 pub total_unlocked: usize,
195 pub need_otp: bool,
197 pub failed_accounts: Vec<u64>,
199 pub message: Option<String>,
201}
202
203pub async fn unlock_trade(
216 client: &FutuClient,
217 pwd_md5: &str,
218 is_unlock: bool,
219 otp: Option<&str>,
220 security_firm: Option<i32>,
221 acc_ids: Vec<u64>,
224) -> Result<UnlockTradeOutcome> {
225 let req = futu_proto::trd_unlock_trade::Request {
226 c2s: futu_proto::trd_unlock_trade::C2s {
227 unlock: is_unlock,
228 pwd_md5: Some(pwd_md5.to_string()),
229 security_firm,
230 sec_otp: otp.map(String::from),
231 acc_ids,
232 },
233 };
234
235 let body = prost::Message::encode_to_vec(&req);
236 let resp_frame = client.request(proto_id::TRD_UNLOCK_TRADE, body).await?;
237
238 let resp: futu_proto::trd_unlock_trade::Response =
239 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
240
241 if resp.ret_type != 0 {
243 let s2c_ref = resp.s2c.as_ref();
244 let need_otp = s2c_ref.and_then(|s| s.need_otp).unwrap_or(false);
245 if need_otp {
246 let failed = s2c_ref
247 .map(|s| s.account_result_list.iter().map(|a| a.acc_id).collect())
248 .unwrap_or_default();
249 return Ok(UnlockTradeOutcome {
250 total_requested: s2c_ref.map(|s| s.account_result_list.len()).unwrap_or(0),
251 total_unlocked: 0,
252 need_otp: true,
253 failed_accounts: failed,
254 message: resp.ret_msg,
255 });
256 }
257 return Err(crate::server_err(
258 resp.ret_type,
259 resp.ret_msg,
260 resp.err_code,
261 ));
262 }
263
264 let s2c_ref = resp.s2c.as_ref();
266 let list = s2c_ref.map(|s| &s.account_result_list[..]).unwrap_or(&[]);
267 let total_requested = list.len();
268 let total_unlocked = list.iter().filter(|a| a.success).count();
269 let failed_accounts: Vec<u64> = list
270 .iter()
271 .filter(|a| !a.success)
272 .map(|a| a.acc_id)
273 .collect();
274 Ok(UnlockTradeOutcome {
275 total_requested,
276 total_unlocked,
277 need_otp: false,
278 failed_accounts,
279 message: resp.ret_msg,
280 })
281}
282
283pub async fn get_acc_list(client: &FutuClient) -> Result<Vec<TrdAcc>> {
290 get_acc_list_with_options(client, None, false).await
291}
292
293pub async fn get_acc_list_for_account_discovery(client: &FutuClient) -> Result<Vec<TrdAcc>> {
302 get_acc_list_with_options(client, None, true).await
303}
304
305pub fn is_app_visible_account(acc: &TrdAcc) -> bool {
314 account_locator::is_app_visible_account(acc)
315}
316
317pub fn app_visible_accounts(accs: Vec<TrdAcc>) -> Vec<TrdAcc> {
318 account_locator::app_visible_accounts(accs)
319}
320
321pub async fn get_acc_list_with_options(
323 client: &FutuClient,
324 trd_category: Option<i32>,
325 need_general_sec_account: bool,
326) -> Result<Vec<TrdAcc>> {
327 let req = build_get_acc_list_request(trd_category, need_general_sec_account);
328
329 let body = prost::Message::encode_to_vec(&req);
330 let resp_frame = client.request(proto_id::TRD_GET_ACC_LIST, body).await?;
331
332 let resp: futu_proto::trd_get_acc_list::Response =
333 prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
334
335 if resp.ret_type != 0 {
336 return Err(crate::server_err(
337 resp.ret_type,
338 resp.ret_msg,
339 resp.err_code,
340 ));
341 }
342
343 let s2c = resp
344 .s2c
345 .ok_or(FutuError::Codec("missing s2c in GetAccList".into()))?;
346
347 Ok(s2c
348 .acc_list
349 .iter()
350 .map(|a| TrdAcc {
351 trd_env: a.trd_env,
352 acc_id: a.acc_id,
353 trd_market_auth_list: a.trd_market_auth_list.clone(),
354 acc_type: a.acc_type,
355 card_num: a.card_num.clone(),
356 security_firm: a.security_firm,
357 sim_acc_type: a.sim_acc_type,
358 uni_card_num: a.uni_card_num.clone(),
359 acc_status: a.acc_status,
360 acc_role: a.acc_role,
361 competition_acc_name: a.competition_acc_name.clone(),
362 acc_label: a.acc_label.clone(),
363 jp_acc_type: a.jp_acc_type.clone(),
364 })
365 .collect())
366}
367
368fn build_get_acc_list_request(
369 trd_category: Option<i32>,
370 need_general_sec_account: bool,
371) -> futu_proto::trd_get_acc_list::Request {
372 futu_proto::trd_get_acc_list::Request {
373 c2s: futu_proto::trd_get_acc_list::C2s {
374 user_id: 0,
375 trd_category,
376 need_general_sec_account: Some(need_general_sec_account),
377 },
378 }
379}
380
381#[derive(Debug, Clone, Default)]
387pub struct TrdAcc {
388 pub trd_env: i32,
390 pub acc_id: u64,
392 pub trd_market_auth_list: Vec<i32>,
394 pub acc_type: Option<i32>,
396 pub card_num: Option<String>,
398 pub security_firm: Option<i32>,
401 pub sim_acc_type: Option<i32>,
403 pub uni_card_num: Option<String>,
405 pub acc_status: Option<i32>,
407 pub acc_role: Option<i32>,
409 pub competition_acc_name: Option<String>,
411 pub acc_label: Option<String>,
415 pub jp_acc_type: Vec<i32>,
417}
418
419impl AccountCardRecord for TrdAcc {
420 fn acc_id(&self) -> u64 {
421 self.acc_id
422 }
423
424 fn card_num(&self) -> Option<&str> {
425 self.card_num.as_deref()
426 }
427
428 fn uni_card_num(&self) -> Option<&str> {
429 self.uni_card_num.as_deref()
430 }
431}
432
433impl AccountVisibilityRecord for TrdAcc {
434 fn trd_market_auth_list(&self) -> &[i32] {
435 &self.trd_market_auth_list
436 }
437
438 fn acc_label(&self) -> Option<&str> {
439 self.acc_label.as_deref()
440 }
441}
442
443#[cfg(test)]
444mod tests;