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