Skip to main content

futucli/cmd/
account_view.rs

1use futu_core::account_locator;
2use futu_trd::account::TrdAcc;
3use futu_trd::types::TrdMarket;
4
5pub(super) fn env_label(v: i32) -> &'static str {
6    match v {
7        0 => "simulate",
8        1 => "real",
9        2 => "fund",
10        _ => "?",
11    }
12}
13
14/// `Trd_Common.SecurityFirm` enum to human-facing broker label.
15pub(super) fn security_firm_label(v: i32) -> &'static str {
16    match v {
17        1 => "FutuHK",
18        2 => "FutuInc(US)",
19        3 => "FutuSG",
20        4 => "FutuAU",
21        5 => "FutuCA",
22        6 => "FutuMY",
23        7 => "FutuJP",
24        _ => "?",
25    }
26}
27
28pub(super) fn market_label(v: i32) -> &'static str {
29    match v {
30        10 => "SIM_HK_FUTURES",
31        11 => "SIM_US_FUTURES",
32        12 => "SIM_SG_FUTURES",
33        13 => "SIM_JP_FUTURES",
34        0 => "UNKNOWN",
35        _ => futu_trd::market::trd_market_label(v).unwrap_or("?"),
36    }
37}
38
39pub(super) fn market_list_label(markets: &[i32]) -> String {
40    markets
41        .iter()
42        .map(|m| market_label(*m).to_string())
43        .collect::<Vec<_>>()
44        .join(", ")
45}
46
47pub(super) fn account_special_label(label: &str) -> String {
48    match label {
49        "crypto" => "Crypto".to_string(),
50        "equity_incentive" => "Equity Incentive".to_string(),
51        "ipo_route" => "IPO Route".to_string(),
52        "opened_business" => "Business".to_string(),
53        "paper_trade" => "Paper Trade".to_string(),
54        other => other.replace('_', " "),
55    }
56}
57
58pub(super) fn account_market_list_label(a: &TrdAcc) -> String {
59    if a.trd_market_auth_list.is_empty()
60        && let Some(label) = a.acc_label.as_deref()
61    {
62        return account_special_label(label);
63    }
64    market_list_label(&a.trd_market_auth_list)
65}
66
67/// `Trd_Common.TrdAccType` enum to human-facing account type label.
68pub(super) fn acc_type_label(v: i32) -> &'static str {
69    match v {
70        1 => "Cash",
71        2 => "Margin",
72        3 => "Futures",
73        4 => "Option",
74        _ => "?",
75    }
76}
77
78/// `Trd_Common.TrdAccStatus` enum to human-facing account status label.
79pub(super) fn acc_status_label(v: i32) -> &'static str {
80    match v {
81        0 => "active",
82        1 => "disabled",
83        _ => "?",
84    }
85}
86
87/// `Trd_Common.TrdAccRole` enum to human-facing account role label.
88pub(super) fn acc_role_label(v: i32) -> &'static str {
89    match v {
90        0 => "Unknown",
91        1 => "Normal",
92        2 => "Master",
93        3 => "IPO Route",
94        _ => "?",
95    }
96}
97
98pub(super) fn account_display_label(a: &TrdAcc) -> String {
99    if let Some(label) = a.acc_label.as_deref() {
100        return account_special_label(label);
101    }
102    a.acc_role
103        .map(|v| acc_role_label(v).to_string())
104        .unwrap_or_else(|| "-".into())
105}
106
107pub(super) fn visible_card_num(a: &TrdAcc) -> Option<String> {
108    // C++ `AddRealAccount` returns both `cardNum` (business sub-account card)
109    // and `uniCardNum` (parent comprehensive account card) for universal
110    // sub-accounts. The mobile app labels comprehensive accounts with
111    // `uniCardNum` suffix such as "(7680)", so user-facing CLI output exposes
112    // one unified `card_num`: prefer `uni_card_num`, then fall back to raw
113    // `card_num`. Backend/internal code still keeps both fields where protocol
114    // semantics require them.
115    account_locator::visible_card_num(a).map(ToOwned::to_owned)
116}
117
118fn inferred_security_firm_for_markets(markets: &[i32]) -> Option<i32> {
119    if markets.contains(&(TrdMarket::HK as i32)) {
120        Some(1)
121    } else if markets.contains(&(TrdMarket::US as i32)) {
122        Some(2)
123    } else if markets.contains(&(TrdMarket::SG as i32)) {
124        Some(3)
125    } else if markets.contains(&(TrdMarket::AU as i32)) {
126        Some(4)
127    } else if markets.contains(&(TrdMarket::CA as i32)) {
128        Some(5)
129    } else if markets.contains(&(TrdMarket::MY as i32)) {
130        Some(6)
131    } else if markets.contains(&(TrdMarket::JP as i32)) {
132        Some(7)
133    } else {
134        None
135    }
136}
137
138pub(super) fn display_security_firm_label(a: &TrdAcc) -> String {
139    let firm = match a.security_firm {
140        Some(v) if v != 0 => Some(v),
141        _ => inferred_security_firm_for_markets(&a.trd_market_auth_list),
142    };
143    firm.map(security_firm_label).unwrap_or("-").to_string()
144}