Skip to main content

futu_mcp/handlers/trade/
misc.rs

1//! mcp/handlers/trade/misc — HistoryDeal + MarginRatio + sub_acc_push + acc_cash_flow
2//! (v1.4.110 CC Batch O: 拆自 trade.rs L787-985)
3
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use futu_net::client::FutuClient;
8use futu_trd::types::TrdHeader;
9use serde::Serialize;
10
11use super::helpers::*;
12use super::orders::HistoryQueryInput;
13
14#[derive(Serialize)]
15struct HistoryDealOut {
16    fill_id: u64,
17    fill_id_ex: String,
18    order_id: u64,
19    trd_side: i32,
20    code: String,
21    name: String,
22    qty: f64,
23    price: f64,
24    create_time: String,
25}
26
27/// 查历史成交。参数语义同 `get_history_orders`。
28pub async fn get_history_deals(
29    client: &Arc<FutuClient>,
30    input: HistoryQueryInput<'_>,
31) -> Result<String> {
32    let header = build_header(input.env, input.acc_id, input.market)?;
33    let filter = futu_trd::misc::HistoryFilterConditions {
34        code_list: input.code_list,
35        id_list: vec![],
36        begin_time: input.begin_time,
37        end_time: input.end_time,
38        filter_market: Some(header.trd_market as i32),
39    };
40    let list = futu_trd::misc::get_history_order_fill_list(client, &header, &filter).await?;
41    let out: Vec<HistoryDealOut> = list
42        .iter()
43        .map(|f| HistoryDealOut {
44            fill_id: f.fill_id,
45            fill_id_ex: f.fill_id_ex.clone(),
46            order_id: f.order_id,
47            trd_side: f.trd_side,
48            code: f.code.clone(),
49            name: f.name.clone(),
50            qty: f.qty,
51            price: f.price,
52            create_time: f.create_time.clone(),
53        })
54        .collect();
55    Ok(serde_json::to_string_pretty(&out)?)
56}
57
58#[derive(Serialize)]
59struct MarginRatioOut {
60    code: String,
61    is_long_permit: bool,
62    is_short_permit: bool,
63    short_pool_remain: f64,
64    short_fee_rate: f64,
65    im_long_ratio: f64,
66    im_short_ratio: f64,
67}
68
69/// 查融资融券比率(按标的列表)。对应 Python SDK
70/// [`OpenTradeContext.get_margin_ratio`](https://github.com/FutunnOpen/py-futu-api/blob/master/futu/trade/open_trade_context.py)。
71///
72/// `codes` 是 `MARKET.CODE` 列表(如 `HK.00700`、`US.AAPL`)。
73pub async fn get_margin_ratio(
74    client: &Arc<FutuClient>,
75    env: &str,
76    acc_id: u64,
77    market: &str,
78    codes: &[String],
79) -> Result<String> {
80    let header = build_header_strict_no_fund(env, acc_id, market)?;
81    let secs: Vec<(i32, String)> = codes.iter().filter_map(|s| parse_market_code(s)).collect();
82    if secs.is_empty() {
83        bail!("no valid MARKET.CODE in codes list");
84    }
85    let list = futu_trd::misc::get_margin_ratio(client, &header, &secs).await?;
86    let out: Vec<MarginRatioOut> = list
87        .into_iter()
88        .map(|m| MarginRatioOut {
89            code: m.code,
90            is_long_permit: m.is_long_permit,
91            is_short_permit: m.is_short_permit,
92            short_pool_remain: m.short_pool_remain,
93            short_fee_rate: m.short_fee_rate,
94            im_long_ratio: m.im_long_ratio,
95            im_short_ratio: m.im_short_ratio,
96        })
97        .collect();
98    Ok(serde_json::to_string_pretty(&out)?)
99}
100
101/// 解析 `HK.00700` / `US.AAPL` 为 `(market_int, code)`。未知 market 返回
102/// None,上层 filter_map 会跳过。
103pub fn parse_market_code(s: &str) -> Option<(i32, String)> {
104    let mut parts = s.splitn(2, '.');
105    let mkt = parts.next()?.to_ascii_uppercase();
106    let code = parts.next()?.to_string();
107    let m = match mkt.as_str() {
108        "HK" => 1,
109        "US" => 11,
110        "SH" => 21,
111        "SZ" => 22,
112        "SG" => 31,
113        "JP" => 41,
114        "AU" => 42,
115        _ => return None,
116    };
117    Some((m, code))
118}
119
120/// 订阅交易账户推送(订单 / 成交更新)。对应 Python SDK
121/// `OpenTradeContext.subscribe_acc_push`(启动推送后通过 WebSocket 转发)。
122///
123/// 注意:MCP 本身是请求-响应模型,推送需要通过 opend 的 WS 通道消费,
124/// MCP 这里只是触发"让 gateway 开始接收 push"。
125pub async fn sub_acc_push(client: &Arc<FutuClient>, acc_ids: &[u64]) -> Result<String> {
126    futu_trd::misc::sub_acc_push(client, acc_ids).await?;
127    Ok(serde_json::json!({ "ok": true, "subscribed_acc_ids": acc_ids }).to_string())
128}
129
130// ============================================================
131// get_acc_cash_flow / Trd_FlowSummary (CMD 2226) — v1.4.30 P2
132// ============================================================
133
134#[derive(Serialize)]
135struct FlowSummaryItemOut {
136    clearing_date: Option<String>,
137    settlement_date: Option<String>,
138    currency: Option<i32>,
139    cash_flow_type: Option<String>,
140    cash_flow_direction: Option<i32>,
141    cash_flow_amount: Option<f64>,
142    cash_flow_remark: Option<String>,
143    cash_flow_id: Option<u64>,
144}
145
146/// 账户资金流水(对齐 py-futu-api `get_acc_cash_flow`)。`clearing_date`
147/// 格式 `yyyy-MM-dd`,每次查一天。`direction`:1=入金 / 2=出金 / None=全部。
148pub async fn get_acc_cash_flow(
149    client: &Arc<FutuClient>,
150    env: &str,
151    acc_id: u64,
152    market: &str,
153    clearing_date: &str,
154    direction: Option<i32>,
155) -> Result<String> {
156    // v1.4.102 codex 41 F2 / 42 F3: typo (e.g. "prod" / "reel") 现 reject,
157    // 不 silent 当 sim.
158    let trd_env = parse_trd_env(env)?;
159    let trd_market = parse_trd_market(market)?;
160    let header = TrdHeader {
161        trd_env,
162        acc_id,
163        trd_market,
164        jp_acc_type: None,
165    };
166    let proto_header = futu_proto::trd_common::TrdHeader {
167        trd_env: header.trd_env as i32,
168        acc_id: header.acc_id,
169        trd_market: header.trd_market as i32,
170        jp_acc_type: None,
171    };
172    let req = futu_proto::trd_flow_summary::Request {
173        c2s: futu_proto::trd_flow_summary::C2s {
174            header: proto_header,
175            clearing_date: clearing_date.to_string(),
176            cash_flow_direction: direction,
177            start_create_date: None,
178            end_create_date: None,
179        },
180    };
181    let body = prost::Message::encode_to_vec(&req);
182    let frame = client
183        .request(futu_core::proto_id::TRD_FLOW_SUMMARY, body)
184        .await?;
185    let resp =
186        <futu_proto::trd_flow_summary::Response as prost::Message>::decode(frame.body.as_ref())
187            .map_err(|e| anyhow::anyhow!("decode flow_summary: {e}"))?;
188    if resp.ret_type != 0 {
189        bail!(
190            "flow_summary ret_type={} msg={:?}",
191            resp.ret_type,
192            resp.ret_msg
193        );
194    }
195    let s2c = resp.s2c.ok_or_else(|| anyhow::anyhow!("missing s2c"))?;
196    let out: Vec<FlowSummaryItemOut> = s2c
197        .flow_summary_info_list
198        .iter()
199        .map(|f| FlowSummaryItemOut {
200            clearing_date: f.clearing_date.clone(),
201            settlement_date: f.settlement_date.clone(),
202            currency: f.currency,
203            cash_flow_type: f.cash_flow_type.clone(),
204            cash_flow_direction: f.cash_flow_direction,
205            cash_flow_amount: f.cash_flow_amount,
206            cash_flow_remark: f.cash_flow_remark.clone(),
207            cash_flow_id: f.cash_flow_id,
208        })
209        .collect();
210    Ok(serde_json::to_string_pretty(&out)?)
211}