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 competition_acc_name: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub sim_acc_type: Option<i32>,
39 #[serde(skip_serializing_if = "Vec::is_empty")]
40 pub jp_acc_type: Vec<i32>,
41}
42
43pub fn visible_card_num_for_account(a: &futu_trd::account::TrdAcc) -> Option<String> {
44 futu_core::account_locator::visible_card_num(a).map(ToOwned::to_owned)
45}
46
47pub fn unique_acc_ids_from_allowed_card_nums(
48 accs: &[futu_trd::account::TrdAcc],
49 allowed_card_nums: Option<&[String]>,
50) -> HashSet<u64> {
51 let mut ids = HashSet::new();
52 let Some(allowed_card_nums) = allowed_card_nums else {
53 return ids;
54 };
55 for card_num in allowed_card_nums
56 .iter()
57 .map(|s| s.trim())
58 .filter(|s| !s.is_empty())
59 {
60 if let Ok(futu_core::account_locator::CardNumResolution::Resolved(acc_id)) =
61 futu_core::account_locator::resolve_card_num_in_records(accs, card_num, None)
62 {
63 ids.insert(acc_id);
64 }
65 }
66 ids
67}
68
69pub fn caller_visible_accounts<'a>(
70 accs: &'a [futu_trd::account::TrdAcc],
71 caller_allowed: Option<&HashSet<u64>>,
72 allowed_card_nums: Option<&[String]>,
73) -> Vec<&'a futu_trd::account::TrdAcc> {
74 let allowed_ids_active = caller_allowed.is_some_and(|s| !s.is_empty());
75 let allowed_cards_active = allowed_card_nums.is_some_and(|v| !v.is_empty());
76 if !allowed_ids_active && !allowed_cards_active {
77 return accs.iter().collect();
78 }
79
80 let allowed_by_card = unique_acc_ids_from_allowed_card_nums(accs, allowed_card_nums);
81 accs.iter()
82 .filter(|a| {
83 let allowed_by_id =
84 caller_allowed.is_some_and(|allowed| a.acc_id != 0 && allowed.contains(&a.acc_id));
85 allowed_by_id || allowed_by_card.contains(&a.acc_id)
86 })
87 .collect()
88}
89
90pub async fn list_accounts_filtered(
99 client: &Arc<FutuClient>,
100 caller_allowed: Option<&std::collections::HashSet<u64>>,
101 allowed_card_nums: Option<&[String]>,
102) -> Result<String> {
103 let accs = futu_trd::account::app_visible_accounts(
104 futu_trd::account::get_acc_list_for_account_discovery(client).await?,
105 );
106 let visible = caller_visible_accounts(&accs, caller_allowed, allowed_card_nums);
107 let out: Vec<AccountOut> = visible
108 .into_iter()
109 .map(|a| AccountOut {
110 acc_id: a.acc_id.to_string(),
111 trd_env: a.trd_env,
112 env_label: match a.trd_env {
113 0 => "simulate",
114 1 => "real",
115 _ => "unknown",
116 },
117 trd_market_auth_list: a.trd_market_auth_list.clone(),
118 card_num: visible_card_num_for_account(a),
119 security_firm: a.security_firm,
120 acc_type: a.acc_type,
121 acc_status: a.acc_status,
122 acc_role: a.acc_role,
123 acc_label: a.acc_label.clone(),
124 competition_acc_name: a.competition_acc_name.clone(),
125 sim_acc_type: a.sim_acc_type,
126 jp_acc_type: a.jp_acc_type.clone(),
127 })
128 .collect();
129 Ok(serde_json::to_string_pretty(&out)?)
130}