futu_mcp/handlers/trade/
accounts.rs1use std::collections::HashSet;
5use std::sync::Arc;
6
7use anyhow::Result;
8use futu_net::client::FutuClient;
9use serde::Serialize;
10
11#[derive(Serialize)]
12pub struct AccountOut {
13 pub acc_id: String,
14 pub trd_env: i32,
15 pub env_label: &'static str,
16 pub trd_market_auth_list: Vec<i32>,
17 #[serde(skip_serializing_if = "Option::is_none")]
24 pub card_num: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub security_firm: Option<i32>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub acc_type: Option<i32>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub acc_status: Option<i32>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub acc_role: Option<i32>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub acc_label: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub sim_acc_type: Option<i32>,
37 #[serde(skip_serializing_if = "Vec::is_empty")]
38 pub jp_acc_type: Vec<i32>,
39}
40
41pub fn visible_card_num_for_account(a: &futu_trd::account::TrdAcc) -> Option<String> {
42 futu_core::account_locator::visible_card_num(a).map(ToOwned::to_owned)
43}
44
45pub fn unique_acc_ids_from_allowed_card_nums(
46 accs: &[futu_trd::account::TrdAcc],
47 allowed_card_nums: Option<&[String]>,
48) -> HashSet<u64> {
49 let mut ids = HashSet::new();
50 let Some(allowed_card_nums) = allowed_card_nums else {
51 return ids;
52 };
53 for card_num in allowed_card_nums
54 .iter()
55 .map(|s| s.trim())
56 .filter(|s| !s.is_empty())
57 {
58 if let Ok(futu_core::account_locator::CardNumResolution::Resolved(acc_id)) =
59 futu_core::account_locator::resolve_card_num_in_records(accs, card_num, None)
60 {
61 ids.insert(acc_id);
62 }
63 }
64 ids
65}
66
67pub fn caller_visible_accounts<'a>(
68 accs: &'a [futu_trd::account::TrdAcc],
69 caller_allowed: Option<&HashSet<u64>>,
70 allowed_card_nums: Option<&[String]>,
71) -> Vec<&'a futu_trd::account::TrdAcc> {
72 let allowed_ids_active = caller_allowed.is_some_and(|s| !s.is_empty());
73 let allowed_cards_active = allowed_card_nums.is_some_and(|v| !v.is_empty());
74 if !allowed_ids_active && !allowed_cards_active {
75 return accs.iter().collect();
76 }
77
78 let allowed_by_card = unique_acc_ids_from_allowed_card_nums(accs, allowed_card_nums);
79 accs.iter()
80 .filter(|a| {
81 let allowed_by_id =
82 caller_allowed.is_some_and(|allowed| a.acc_id != 0 && allowed.contains(&a.acc_id));
83 allowed_by_id || allowed_by_card.contains(&a.acc_id)
84 })
85 .collect()
86}
87
88pub async fn list_accounts_filtered(
97 client: &Arc<FutuClient>,
98 caller_allowed: Option<&std::collections::HashSet<u64>>,
99 allowed_card_nums: Option<&[String]>,
100) -> Result<String> {
101 let accs = futu_trd::account::app_visible_accounts(
102 futu_trd::account::get_acc_list_for_account_discovery(client).await?,
103 );
104 let visible = caller_visible_accounts(&accs, caller_allowed, allowed_card_nums);
105 let out: Vec<AccountOut> = visible
106 .into_iter()
107 .map(|a| AccountOut {
108 acc_id: a.acc_id.to_string(),
109 trd_env: a.trd_env,
110 env_label: match a.trd_env {
111 0 => "simulate",
112 1 => "real",
113 _ => "unknown",
114 },
115 trd_market_auth_list: a.trd_market_auth_list.clone(),
116 card_num: visible_card_num_for_account(a),
117 security_firm: a.security_firm,
118 acc_type: a.acc_type,
119 acc_status: a.acc_status,
120 acc_role: a.acc_role,
121 acc_label: a.acc_label.clone(),
122 sim_acc_type: a.sim_acc_type,
123 jp_acc_type: a.jp_acc_type.clone(),
124 })
125 .collect();
126 Ok(serde_json::to_string_pretty(&out)?)
127}