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_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    // HK-specific (US/CN_AH 通常返空字符串)
55    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
118/// v1.4.95 U2-D: MCP tool `futu_get_margin_info` — per-account margin info.
119///
120/// 与 `futu_get_margin_ratio` (per-security ratio) 互补: 本 tool 给账户全景.
121pub 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    // 校验 market 在支持范围内 (HK / US / CN_AH); 其他市场 daemon 也会拒,
131    // 但 MCP 提前 reject 给清晰 error.
132    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    // v1.4.102 codex 38 F4 / 41 F2: 严格 env, typo reject.
143    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, // daemon handler 自动派生 account 字段
153        },
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;