Skip to main content

futu_mcp/handlers/trade/
margin_info.rs

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// ============================================================================
12// v1.4.95 U2-D Tier M (mobile-driven extension): margin account info per market
13//
14// 来源: ftcnnproto/.../risk_user_account_info.proto + FLCltProtocol.h
15// (clt_cmd_hk_margin_info=3101 / us=3102 / cn_ah=3107).
16// ============================================================================
17
18#[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    // HK-specific (US/CN_AH 通常返空字符串)
53    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
112/// v1.4.95 U2-D: MCP tool `futu_get_margin_info` — per-account margin info.
113///
114/// 与 `futu_get_margin_ratio` (per-security ratio) 互补: 本 tool 给账户全景.
115pub 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    // 校验 market 在支持范围内 (HK / US / CN_AH); 其他市场 daemon 也会拒,
125    // 但 MCP 提前 reject 给清晰 error.
126    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    // v1.4.102 codex 38 F4 / 41 F2: 严格 env, typo reject.
137    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, // daemon handler 自动派生 account 字段
147        },
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;