1use std::sync::Arc;
2
3use anyhow::{Result, bail};
4use futu_backend::proto_internal::risk_user_account_info;
5use futu_net::client::FutuClient;
6use prost::Message as _;
7use serde::Serialize;
8
9use super::parse_trd_env_int;
10
11#[derive(Serialize)]
19struct MarginInfoOut {
20 account_id: u64,
21 market: u32,
22 long_power: String,
23 short_power: String,
24 balance: String,
25 market_value: String,
26 elv: String,
27 im: String,
28 mcm: String,
29 mm: String,
30 overnight_im: String,
31 overnight_mm: String,
32 im_balance: String,
33 mcm_balance: String,
34 mm_balance: String,
35 overnight_mm_balance: String,
36 im_recover: String,
37 alerter_margin: String,
38 alerter_margin_balance: String,
39 elv_mv_ratio: f64,
40 risk_level_type: u32,
41 margin_call_days: i32,
42 risk_status: i32,
43 risk_status_client: i32,
44 pstn_ratio: String,
45 lever_multi: String,
46 mm_balance_ratio: String,
47 ibp: String,
48 original_client_level: u32,
49 original_risk_factor_client: u32,
50 original_risk_level: u32,
51 original_risk_status: i32,
52 real_loan: String,
54 loan_ratio: String,
55 margin_value: String,
56 margin_ratio: String,
57 margin_init_ratio: String,
58 margin_warn_ratio: String,
59 margin_cover_ratio: String,
60 regt_call_amount: String,
61 is_high_leverage_user: bool,
62}
63
64fn margin_info_out_from_proto(umi: risk_user_account_info::UserMarginInfo) -> MarginInfoOut {
65 let acc = umi.account.unwrap_or_default();
66 let mi = umi.margin_info.unwrap_or_default();
67 MarginInfoOut {
68 account_id: acc.account_id.unwrap_or(0),
69 market: acc.market.unwrap_or(0),
70 long_power: mi.long_power.unwrap_or_default(),
71 short_power: mi.short_power.unwrap_or_default(),
72 balance: mi.balance.unwrap_or_default(),
73 market_value: mi.market_value.unwrap_or_default(),
74 elv: mi.elv.unwrap_or_default(),
75 im: mi.im.unwrap_or_default(),
76 mcm: mi.mcm.unwrap_or_default(),
77 mm: mi.mm.unwrap_or_default(),
78 overnight_im: mi.overnight_im.unwrap_or_default(),
79 overnight_mm: mi.overnight_mm.unwrap_or_default(),
80 im_balance: mi.im_balance.unwrap_or_default(),
81 mcm_balance: mi.mcm_balance.unwrap_or_default(),
82 mm_balance: mi.mm_balance.unwrap_or_default(),
83 overnight_mm_balance: mi.overnight_mm_balance.unwrap_or_default(),
84 im_recover: mi.im_recover.unwrap_or_default(),
85 alerter_margin: mi.alerter_margin.unwrap_or_default(),
86 alerter_margin_balance: mi.alerter_margin_balance.unwrap_or_default(),
87 elv_mv_ratio: mi.elv_mv_ratio.unwrap_or(0.0),
88 risk_level_type: mi.risk_level_type.unwrap_or(0),
89 margin_call_days: mi.margin_call_days.unwrap_or(0),
90 risk_status: mi.risk_status.unwrap_or(0),
91 risk_status_client: mi.risk_status_client.unwrap_or(0),
92 pstn_ratio: mi.pstn_ratio.unwrap_or_default(),
93 lever_multi: mi.lever_multi.unwrap_or_default(),
94 mm_balance_ratio: mi.mm_balance_ratio.unwrap_or_default(),
95 ibp: mi.ibp.unwrap_or_default(),
96 original_client_level: mi.original_client_level.unwrap_or(0),
97 original_risk_factor_client: mi.original_risk_factor_client.unwrap_or(0),
98 original_risk_level: mi.original_risk_level.unwrap_or(0),
99 original_risk_status: mi.original_risk_status.unwrap_or(0),
100 real_loan: mi.real_loan.unwrap_or_default(),
101 loan_ratio: mi.loan_ratio.unwrap_or_default(),
102 margin_value: mi.margin_value.unwrap_or_default(),
103 margin_ratio: mi.margin_ratio.unwrap_or_default(),
104 margin_init_ratio: mi.margin_init_ratio.unwrap_or_default(),
105 margin_warn_ratio: mi.margin_warn_ratio.unwrap_or_default(),
106 margin_cover_ratio: mi.margin_cover_ratio.unwrap_or_default(),
107 regt_call_amount: mi.regt_call_amount.unwrap_or_default(),
108 is_high_leverage_user: mi.is_high_leverage_user.unwrap_or(false),
109 }
110}
111
112pub async fn get_margin_info(
116 client: &Arc<FutuClient>,
117 env: &str,
118 acc_id: u64,
119 market: &str,
120) -> Result<String> {
121 if acc_id == 0 {
122 bail!("acc_id 必填 (call futu_list_accounts to discover)");
123 }
124 let market_upper = market.trim().to_ascii_uppercase();
127 if !matches!(
128 market_upper.as_str(),
129 "HK" | "US" | "USA" | "CN_AH" | "AH" | "A_H" | "CN-AH"
130 ) {
131 bail!(
132 "market {market:?} 不支持 (only HK / US / CN_AH; mobile cmd 3101/3102/3107). \
133 Other markets: use futu_get_margin_ratio for per-security ratio"
134 );
135 }
136 let trd_env_int: i32 = parse_trd_env_int(env)?;
138
139 let req = risk_user_account_info::DaemonGetMarginInfoReq {
140 c2s: risk_user_account_info::daemon_get_margin_info_req::C2s {
141 header: risk_user_account_info::DaemonMarginInfoHeader {
142 acc_id,
143 trd_env: Some(trd_env_int),
144 market: market_upper,
145 },
146 inner: None, },
148 };
149
150 let body = req.encode_to_vec();
151 let frame = client
152 .request(futu_core::proto_id::TRD_GET_MARGIN_INFO, body)
153 .await?;
154 let resp = <risk_user_account_info::DaemonGetMarginInfoRsp as prost::Message>::decode(
155 frame.body.as_ref(),
156 )
157 .map_err(|e| anyhow::anyhow!("decode DaemonGetMarginInfoRsp: {e}"))?;
158 if resp.ret_type != 0 {
159 bail!(
160 "GetMarginInfo ret_type={} msg={:?} (related per-security tool: futu_get_margin_ratio)",
161 resp.ret_type,
162 resp.ret_msg
163 );
164 }
165
166 let inner_rsp = resp
167 .s2c
168 .and_then(|s| s.inner)
169 .ok_or_else(|| anyhow::anyhow!("empty s2c.inner in GetMarginInfoRsp"))?;
170
171 let out: Vec<MarginInfoOut> = inner_rsp
172 .user_margin_info
173 .into_iter()
174 .map(margin_info_out_from_proto)
175 .collect();
176
177 Ok(serde_json::to_string_pretty(&out)?)
178}
179
180#[cfg(test)]
181mod tests;