Skip to main content

futu_mcp/handlers/trade/
funds.rs

1//! mcp/handlers/trade/funds — FundsOut + CashInfoOut + MarketInfoOut + get_funds_with_currency
2//! (v1.4.110 CC Batch O: 拆自 trade.rs L239-449)
3
4use std::sync::Arc;
5
6use anyhow::Result;
7use futu_net::client::FutuClient;
8use futu_trd::{currency, read_plan};
9use serde::Serialize;
10
11use super::helpers::*;
12
13#[derive(Serialize)]
14struct FundsOut {
15    // 旧 7 字段(100% 向后兼容)
16    power: f64,
17    total_assets: f64,
18    /// **v1.4.106 codex 1612 Candidate A**: top-level summary cash, in
19    /// response `currency` field. **不**等于 `cash_info_list` 跨币种相加
20    /// (跨币种不能无汇率相加, 由 backend `Ndt_Trd_AccFund.fTotalCash` 直传,
21    /// 对齐 C++ `pFunds->set_cash(nnFunds.fTotalCash)`). 综合账户用 `currency`
22    /// 字段标识此 cash 的口径 (`union_currency` for futures/universal,
23    /// 主市场 currency for legacy 普通账户). 分币种现金 breakdown 看
24    /// `cash_info_list`.
25    cash: f64,
26    market_val: f64,
27    frozen_cash: f64,
28    debt_cash: f64,
29    avl_withdrawal_cash: f64,
30
31    // v1.4.73 BUG-004 新增 18 字段
32    /// 账户主货币(1=HKD / 2=USD / 3=CNY / 4=JPY / 5=SGD / 6=AUD / 7=CAD / 8=MYR)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    currency: Option<i32>,
35    /// 可用资金(margin 账户与 `cash` 不同)
36    #[serde(skip_serializing_if = "Option::is_none")]
37    available_funds: Option<f64>,
38    /// 浮动盈亏
39    #[serde(skip_serializing_if = "Option::is_none")]
40    unrealized_pl: Option<f64>,
41    /// 已实现盈亏
42    #[serde(skip_serializing_if = "Option::is_none")]
43    realized_pl: Option<f64>,
44    /// 风控级别
45    #[serde(skip_serializing_if = "Option::is_none")]
46    risk_level: Option<i32>,
47    /// 风控状态
48    #[serde(skip_serializing_if = "Option::is_none")]
49    risk_status: Option<i32>,
50    /// 初始保证金
51    #[serde(skip_serializing_if = "Option::is_none")]
52    initial_margin: Option<f64>,
53    /// 维持保证金
54    #[serde(skip_serializing_if = "Option::is_none")]
55    maintenance_margin: Option<f64>,
56    /// 保证金追加金额
57    #[serde(skip_serializing_if = "Option::is_none")]
58    margin_call_margin: Option<f64>,
59    /// 做空购买力
60    #[serde(skip_serializing_if = "Option::is_none")]
61    max_power_short: Option<f64>,
62    /// 剩余现金购买力
63    /// v1.4.104 eli P1-002 (P1) fix: 移除 `skip_serializing_if`, 让字段
64    /// 始终出现 (即使 None → null) 以与 REST 33-key shape 对齐. agent
65    /// 按 "observed fields" 推断 schema 不再误判.
66    net_cash_power: Option<f64>,
67    /// 多头持仓市值
68    #[serde(skip_serializing_if = "Option::is_none")]
69    long_mv: Option<f64>,
70    /// 空头持仓市值
71    #[serde(skip_serializing_if = "Option::is_none")]
72    short_mv: Option<f64>,
73    /// 挂单占用资产
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pending_asset: Option<f64>,
76    /// 可取现上限
77    #[serde(skip_serializing_if = "Option::is_none")]
78    max_withdrawal: Option<f64>,
79    // v1.4.104 eli P1-002 (P1) fix: 9 字段 (is_pdt, pdt_seq, beginning_dtbp,
80    // remaining_dtbp, dt_call_amount, dt_status, fund_assets, bond_assets,
81    // net_cash_power 见 above) 移除 skip_serializing_if. REST 33 key shape
82    // 含 null, MCP 24 key 省略 → agent 按 "observed fields" 推断 schema 误判
83    // 字段不存在. 现在 MCP 与 REST 同 33 key shape, null 显式输出.
84    /// 是否 Pattern Day Trader(US 账户)
85    is_pdt: Option<bool>,
86    /// PDT 标识序列号
87    pdt_seq: Option<String>,
88    /// v1.4.102 BUG-010 fix: 初始日内交易购买力 (DTBP, US PDT 账户).
89    /// REST 已有但 MCP 漏暴露 — leaf v1.4.100 报告矩阵 9 缺字段之一.
90    beginning_dtbp: Option<f64>,
91    /// 剩余日内交易购买力(US PDT 账户)
92    remaining_dtbp: Option<f64>,
93    /// 日内交易追加金额
94    dt_call_amount: Option<f64>,
95    /// 日内交易风险状态
96    dt_status: Option<i32>,
97    /// 证券资产
98    #[serde(skip_serializing_if = "Option::is_none")]
99    securities_assets: Option<f64>,
100    /// 基金资产
101    fund_assets: Option<f64>,
102    /// 债券资产
103    bond_assets: Option<f64>,
104
105    // v1.4.74 C1 BUG-004 Phase 2: 分币种现金 + 分市场资产 breakdown
106    /// 分币种现金信息(多币种账户 per-currency breakdown)
107    #[serde(skip_serializing_if = "Vec::is_empty")]
108    cash_info_list: Vec<CashInfoOut>,
109    /// 分市场资产信息(综合账户 / 跨市场 per-market breakdown)
110    #[serde(skip_serializing_if = "Vec::is_empty")]
111    market_info_list: Vec<MarketInfoOut>,
112
113    /// 用户显式传 currency, 但 backend 按账户基准币种返回时的提示。
114    /// 例如 requested USD, returned HKD;未显式传 currency 或返回币种一致时为 None。
115    #[serde(skip_serializing_if = "Option::is_none")]
116    currency_warning: Option<String>,
117}
118
119/// v1.4.74 C1 BUG-004 Phase 2: 分币种现金(对齐 `futu_trd::types::FundsCashInfo`)
120#[derive(Serialize)]
121struct CashInfoOut {
122    /// 货币 (1=HKD / 2=USD / 3=CNY / 4=JPY / 5=SGD / 6=AUD / 7=CAD / 8=MYR)
123    #[serde(skip_serializing_if = "Option::is_none")]
124    currency: Option<i32>,
125    /// 该币种现金结余
126    #[serde(skip_serializing_if = "Option::is_none")]
127    cash: Option<f64>,
128    /// 该币种可提现金额
129    #[serde(skip_serializing_if = "Option::is_none")]
130    available_balance: Option<f64>,
131    /// 该币种现金购买力
132    #[serde(skip_serializing_if = "Option::is_none")]
133    net_cash_power: Option<f64>,
134}
135
136/// v1.4.74 C1 BUG-004 Phase 2: 分市场资产(对齐 `futu_trd::types::FundsMarketInfo`)
137#[derive(Serialize)]
138struct MarketInfoOut {
139    /// 交易市场 (TrdMarket: 1=HK / 2=US / 3=CN / 4=HKCC / 5=Futures / 6=SG / 8=AU / 15=JP / 111=MY / 112=CA per Trd_Common.proto, v1.4.93 BUG-001 fix 纠正)
140    #[serde(skip_serializing_if = "Option::is_none")]
141    trd_market: Option<i32>,
142    /// 该市场资产总额
143    #[serde(skip_serializing_if = "Option::is_none")]
144    assets: Option<f64>,
145}
146
147/// 查询账户资金,可显式传入 currency 字符串
148/// (HKD|USD|CNH|JPY|SGD|AUD|CAD|MYR|USDT).
149///
150/// 留空时 daemon 会按账户所属券商派生默认资金视图币种;显式传入时由
151/// gateway 校验账户是否支持该币种。普通单市场账户可能由 backend 忽略
152/// 显式币种并返回账户基准币种。
153pub async fn get_funds_with_currency(
154    client: &Arc<FutuClient>,
155    env: &str,
156    acc_id: u64,
157    market: &str,
158    currency: Option<&str>,
159) -> Result<String> {
160    let header = build_header(env, acc_id, market)?;
161    let currency_int: Option<i32> = match currency {
162        Some(s) => Some(currency::parse_currency_label(s)?),
163        None => None,
164    };
165    let f = futu_trd::account::get_funds_with_currency(client, &header, currency_int).await?;
166    let currency_warning = read_plan::funds_currency_mismatch_warning(currency_int, f.currency);
167    let out = FundsOut {
168        power: f.power,
169        total_assets: f.total_assets,
170        cash: f.cash,
171        market_val: f.market_val,
172        frozen_cash: f.frozen_cash,
173        debt_cash: f.debt_cash,
174        avl_withdrawal_cash: f.avl_withdrawal_cash,
175        // v1.4.73 BUG-004
176        currency: f.currency,
177        available_funds: f.available_funds,
178        unrealized_pl: f.unrealized_pl,
179        realized_pl: f.realized_pl,
180        risk_level: f.risk_level,
181        risk_status: f.risk_status,
182        initial_margin: f.initial_margin,
183        maintenance_margin: f.maintenance_margin,
184        margin_call_margin: f.margin_call_margin,
185        max_power_short: f.max_power_short,
186        net_cash_power: f.net_cash_power,
187        long_mv: f.long_mv,
188        short_mv: f.short_mv,
189        pending_asset: f.pending_asset,
190        max_withdrawal: f.max_withdrawal,
191        is_pdt: f.is_pdt,
192        pdt_seq: f.pdt_seq.clone(),
193        // v1.4.102 BUG-010: MCP funds parity with REST — beginning_dtbp wire
194        beginning_dtbp: f.beginning_dtbp,
195        remaining_dtbp: f.remaining_dtbp,
196        dt_call_amount: f.dt_call_amount,
197        dt_status: f.dt_status,
198        securities_assets: f.securities_assets,
199        fund_assets: f.fund_assets,
200        bond_assets: f.bond_assets,
201        // v1.4.74 C1 BUG-004 Phase 2
202        cash_info_list: f
203            .cash_info_list
204            .iter()
205            .map(|c| CashInfoOut {
206                currency: c.currency,
207                cash: c.cash,
208                available_balance: c.available_balance,
209                net_cash_power: c.net_cash_power,
210            })
211            .collect(),
212        market_info_list: f
213            .market_info_list
214            .iter()
215            .map(|m| MarketInfoOut {
216                trd_market: m.trd_market,
217                assets: m.assets,
218            })
219            .collect(),
220        currency_warning,
221    };
222    Ok(serde_json::to_string_pretty(&out)?)
223}