Skip to main content

futu_mcp/tools/
trade_read.rs

1//! MCP trade-read/account tools (accounts, funds, positions, orders, Tier M read APIs).
2
3use crate::handlers;
4use crate::tool_args::*;
5use rmcp::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
6
7use super::FutuServer;
8
9impl FutuServer {
10    // ------- 账户(只读) -------
11
12    async fn futu_list_accounts_impl(
13        &self,
14        Parameters(_req): Parameters<NoArgs>,
15        req_ctx: RequestContext<RoleServer>,
16    ) -> std::result::Result<String, String> {
17        // v1.4.103 B5: per-request Bearer 解析 (HTTP) → caller-specific scope check.
18        // v1.4.104 codex F3 (P2) fix: 用 pipeline 返的 snapshot 做 list filter,
19        // 不再 re-resolve from Bearer/startup (避免 SIGHUP race / drift —
20        // pipeline 授权时的 KeyRecord 与 filter 用的同一实例).
21        let snap = self.require_acc_read_with_acc_id("futu_list_accounts", &req_ctx, None, None)?;
22        tracing::info!(tool = "futu_list_accounts");
23        let client = self.client_or_err().await?;
24        // v1.4.103 codex F2.5 (P2) + v1.4.104 codex F3 (P2): 按 caller 的
25        // allowed_acc_ids snapshot (来自 pipeline 授权决策, 同一 KeyRecord)
26        // filter list, 防受限 key 跨账户 enumerate. 与 REST `/api/accounts`
27        // filter (codex F6) 对齐.
28        let allowed_card_nums = snap
29            .rec
30            .as_ref()
31            .and_then(|r| r.allowed_card_nums.as_deref());
32        Self::wrap_result(
33            handlers::trade::list_accounts_filtered(
34                &client,
35                snap.allowed_acc_ids.as_ref(),
36                allowed_card_nums,
37            )
38            .await,
39        )
40    }
41
42    async fn futu_get_funds_impl(
43        &self,
44        Parameters(req): Parameters<TrdAccReq>,
45        req_ctx: RequestContext<RoleServer>,
46    ) -> std::result::Result<String, String> {
47        let resolved = self
48            .resolve_read_trd_account("futu_get_funds", &req, &req_ctx)
49            .await?;
50        tracing::info!(
51            tool = "futu_get_funds",
52            market = %req.market,
53            env = %req.env,
54            currency = ?req.currency,
55        );
56        // v1.4.103 (external reviewer 反馈 P1): 综合账户币种不一致 fix — 透传 currency.
57        Self::wrap_result(
58            handlers::trade::get_funds_with_currency(
59                &resolved.client,
60                &req.env,
61                resolved.acc_id,
62                &req.market,
63                req.currency.as_deref(),
64            )
65            .await,
66        )
67    }
68
69    async fn futu_get_positions_impl(
70        &self,
71        Parameters(req): Parameters<PositionReq>,
72        req_ctx: RequestContext<RoleServer>,
73    ) -> std::result::Result<String, String> {
74        let acc_req = req.as_trd_acc_req();
75        let resolved = self
76            .resolve_read_trd_account("futu_get_positions", &acc_req, &req_ctx)
77            .await?;
78        tracing::info!(
79            tool = "futu_get_positions",
80            market = %req.market,
81            currency = ?req.currency,
82            option_strategy_view = req.option_strategy_view,
83        );
84        Self::wrap_result(
85            handlers::trade::get_positions(
86                &resolved.client,
87                &req.env,
88                resolved.acc_id,
89                &req.market,
90                req.currency.as_deref(),
91                req.option_strategy_view,
92            )
93            .await,
94        )
95    }
96
97    async fn futu_get_orders_impl(
98        &self,
99        Parameters(req): Parameters<TrdAccReq>,
100        req_ctx: RequestContext<RoleServer>,
101    ) -> std::result::Result<String, String> {
102        let resolved = self
103            .resolve_read_trd_account("futu_get_orders", &req, &req_ctx)
104            .await?;
105        tracing::info!(tool = "futu_get_orders", market = %req.market);
106        Self::wrap_result(
107            handlers::trade::get_orders(&resolved.client, &req.env, resolved.acc_id, &req.market)
108                .await,
109        )
110    }
111
112    async fn futu_get_deals_impl(
113        &self,
114        Parameters(req): Parameters<TrdAccReq>,
115        req_ctx: RequestContext<RoleServer>,
116    ) -> std::result::Result<String, String> {
117        let resolved = self
118            .resolve_read_trd_account("futu_get_deals", &req, &req_ctx)
119            .await?;
120        tracing::info!(tool = "futu_get_deals", market = %req.market);
121        Self::wrap_result(
122            handlers::trade::get_deals(&resolved.client, &req.env, resolved.acc_id, &req.market)
123                .await,
124        )
125    }
126
127    // ===== v1.4.25: 交易扩展查询 (py-futu-api 对齐) =====
128
129    async fn futu_get_max_trd_qtys_impl(
130        &self,
131        Parameters(req): Parameters<MaxTrdQtysReq>,
132        req_ctx: RequestContext<RoleServer>,
133    ) -> std::result::Result<String, String> {
134        tracing::info!(tool = "futu_get_max_trd_qtys", market = %req.market, code = %req.code);
135        let client = self
136            .read_client_or_err("futu_get_max_trd_qtys", &req_ctx, None, Some(req.acc_id))
137            .await?;
138        Self::wrap_result(
139            handlers::trade::get_max_trd_qtys(
140                &client,
141                handlers::trade::MaxTrdQtysInput {
142                    env: &req.env,
143                    acc_id: req.acc_id,
144                    market: &req.market,
145                    order_type: req.order_type,
146                    code: &req.code,
147                    price: req.price,
148                    jp_acc_type: req.jp_acc_type,
149                    order_id: req.order_id,
150                },
151            )
152            .await,
153        )
154    }
155
156    async fn futu_get_order_fee_impl(
157        &self,
158        Parameters(req): Parameters<OrderFeeReq>,
159        req_ctx: RequestContext<RoleServer>,
160    ) -> std::result::Result<String, String> {
161        tracing::info!(tool = "futu_get_order_fee", market = %req.market, count = req.order_id_ex_list.len());
162        let client = self
163            .read_client_or_err("futu_get_order_fee", &req_ctx, None, Some(req.acc_id))
164            .await?;
165        Self::wrap_result(
166            handlers::trade::get_order_fee(
167                &client,
168                &req.env,
169                req.acc_id,
170                &req.market,
171                &req.order_id_ex_list,
172            )
173            .await,
174        )
175    }
176
177    async fn futu_get_margin_ratio_impl(
178        &self,
179        Parameters(req): Parameters<MarginRatioReq>,
180        req_ctx: RequestContext<RoleServer>,
181    ) -> std::result::Result<String, String> {
182        tracing::info!(tool = "futu_get_margin_ratio", market = %req.market, count = req.codes.len());
183        let client = self
184            .read_client_or_err("futu_get_margin_ratio", &req_ctx, None, Some(req.acc_id))
185            .await?;
186        Self::wrap_result(
187            handlers::trade::get_margin_ratio(
188                &client,
189                &req.env,
190                req.acc_id,
191                &req.market,
192                &req.codes,
193            )
194            .await,
195        )
196    }
197
198    async fn futu_get_history_orders_impl(
199        &self,
200        Parameters(req): Parameters<HistoryQueryReq>,
201        req_ctx: RequestContext<RoleServer>,
202    ) -> std::result::Result<String, String> {
203        tracing::info!(tool = "futu_get_history_orders", market = %req.market);
204        let client = self
205            .read_client_or_err("futu_get_history_orders", &req_ctx, None, Some(req.acc_id))
206            .await?;
207        Self::wrap_result(
208            handlers::trade::get_history_orders(
209                &client,
210                handlers::trade::HistoryQueryInput {
211                    env: &req.env,
212                    acc_id: req.acc_id,
213                    market: &req.market,
214                    code_list: req.code_list,
215                    begin_time: req.begin_time,
216                    end_time: req.end_time,
217                },
218            )
219            .await,
220        )
221    }
222
223    async fn futu_get_history_deals_impl(
224        &self,
225        Parameters(req): Parameters<HistoryQueryReq>,
226        req_ctx: RequestContext<RoleServer>,
227    ) -> std::result::Result<String, String> {
228        tracing::info!(tool = "futu_get_history_deals", market = %req.market);
229        let client = self
230            .read_client_or_err("futu_get_history_deals", &req_ctx, None, Some(req.acc_id))
231            .await?;
232        Self::wrap_result(
233            handlers::trade::get_history_deals(
234                &client,
235                handlers::trade::HistoryQueryInput {
236                    env: &req.env,
237                    acc_id: req.acc_id,
238                    market: &req.market,
239                    code_list: req.code_list,
240                    begin_time: req.begin_time,
241                    end_time: req.end_time,
242                },
243            )
244            .await,
245        )
246    }
247
248    async fn futu_get_acc_cash_flow_impl(
249        &self,
250        Parameters(req): Parameters<AccCashFlowReq>,
251        req_ctx: RequestContext<RoleServer>,
252    ) -> std::result::Result<String, String> {
253        tracing::info!(
254            tool = "futu_get_acc_cash_flow",
255            env = %req.env,
256            market = %req.market,
257            date = %req.clearing_date
258        );
259        let client = self
260            .read_client_or_err("futu_get_acc_cash_flow", &req_ctx, None, Some(req.acc_id))
261            .await?;
262        Self::wrap_result(
263            handlers::trade::get_acc_cash_flow(
264                &client,
265                &req.env,
266                req.acc_id,
267                &req.market,
268                &req.clearing_date,
269                req.direction,
270            )
271            .await,
272        )
273    }
274
275    /// v1.4.73 BUG-002 fix: MCP alias `futu_get_flow_summary` 指向同 handler。
276    ///
277    /// external reviewer v1.4.71 验收报告的 AI tester 列此为 P0("MCP 58 工具无对应 `futu_get_flow_summary`,
278    /// 有 `margin_ratio` / `order_fee` 同类查询")。审查后发现:REST 侧两个
279    /// endpoint 并存 `/api/flow-summary` (原) + `/api/acc-cash-flow` (v1.4.51 alias),
280    /// MCP 只有 `futu_get_acc_cash_flow` (v1.4.30 P2 实装)。
281    ///
282    /// 用户期待 MCP 与 REST `/api/flow-summary` 同名的 tool,这是 UX 对称问题。
283    /// 本 alias 保持与主 tool `futu_get_acc_cash_flow` **语义完全一致**,
284    /// 参数结构 + handler 调用 + scope 登记全部共享。
285    async fn futu_get_flow_summary_impl(
286        &self,
287        Parameters(req): Parameters<AccCashFlowReq>,
288        req_ctx: RequestContext<RoleServer>,
289    ) -> std::result::Result<String, String> {
290        // v1.4.73: alias 下面所有行为等同 futu_get_acc_cash_flow
291        tracing::info!(
292            tool = "futu_get_flow_summary",
293            alias_of = "futu_get_acc_cash_flow",
294            env = %req.env,
295            market = %req.market,
296            date = %req.clearing_date
297        );
298        let client = self
299            .read_client_or_err("futu_get_flow_summary", &req_ctx, None, Some(req.acc_id))
300            .await?;
301        Self::wrap_result(
302            handlers::trade::get_acc_cash_flow(
303                &client,
304                &req.env,
305                req.acc_id,
306                &req.market,
307                &req.clearing_date,
308                req.direction,
309            )
310            .await,
311        )
312    }
313
314    // ========================================================================
315    // v1.4.95 U1 (Tier M MCP): cash log mobile-driven extension tools
316    //
317    // 来源: v1.4.94 M1 ship 了 REST + gRPC FTAPI, MCP 推迟到 v1.4.95.
318    // 比 futu_get_acc_cash_flow 字段更全 (10+ vs 3): 时间范围 / 业务分组 /
319    // 货币 / 关键词 / 股票 / 方向 多维过滤 + cursor 分页.
320    //
321    // gateway handler 与 REST `/api/cash-log` 共用同一实现:从已鉴权账户派生
322    // native account/market,并补齐移动端默认分页字段。
323    // ========================================================================
324
325    async fn futu_get_cash_log_impl(
326        &self,
327        Parameters(req): Parameters<CashLogReq>,
328        req_ctx: RequestContext<RoleServer>,
329    ) -> std::result::Result<String, String> {
330        tracing::info!(
331            tool = "futu_get_cash_log",
332            env = %req.env,
333            market = ?req.market,
334            has_keyword = req.keyword.is_some(),
335            has_symbol = req.symbol.is_some()
336        );
337        let client = self
338            .read_client_or_err("futu_get_cash_log", &req_ctx, None, Some(req.acc_id))
339            .await?;
340        Self::wrap_result(
341            handlers::trade::get_cash_log(
342                &client,
343                handlers::trade::CashLogInput {
344                    env: &req.env,
345                    acc_id: req.acc_id,
346                    begin_time: req.begin_time,
347                    end_time: req.end_time,
348                    biz_group_id: req.biz_group_id,
349                    biz_sub_group_id: req.biz_sub_group_id,
350                    in_out: req.in_out,
351                    keyword: req.keyword,
352                    symbol: req.symbol,
353                    stock_id: req.stock_id,
354                    log_id: req.log_id,
355                    max_cnt: req.max_cnt,
356                    currency: req.currency,
357                },
358            )
359            .await,
360        )
361    }
362
363    async fn futu_get_cash_detail_impl(
364        &self,
365        Parameters(req): Parameters<CashDetailReq>,
366        req_ctx: RequestContext<RoleServer>,
367    ) -> std::result::Result<String, String> {
368        tracing::info!(
369            tool = "futu_get_cash_detail",
370            env = %req.env,
371            market = ?req.market,
372            log_id_len = req.log_id.len()
373        );
374        let client = self
375            .read_client_or_err("futu_get_cash_detail", &req_ctx, None, Some(req.acc_id))
376            .await?;
377        Self::wrap_result(
378            handlers::trade::get_cash_detail(&client, &req.env, req.acc_id, req.log_id.clone())
379                .await,
380        )
381    }
382
383    async fn futu_get_biz_group_impl(
384        &self,
385        Parameters(req): Parameters<BizGroupReq>,
386        req_ctx: RequestContext<RoleServer>,
387    ) -> std::result::Result<String, String> {
388        tracing::info!(
389            tool = "futu_get_biz_group",
390            env = %req.env,
391            market = ?req.market
392        );
393        let client = self
394            .read_client_or_err("futu_get_biz_group", &req_ctx, None, Some(req.acc_id))
395            .await?;
396        Self::wrap_result(handlers::trade::get_biz_group(&client, &req.env, req.acc_id).await)
397    }
398
399    // ========================================================================
400    // v1.4.95 U2-D Tier M (mobile-driven extension): per-account margin info
401    //
402    // 与 futu_get_margin_ratio (per-security ratio) 互补: 本 tool 给账户全景.
403    // 仅 HK / US / CN_AH 3 市场 (mobile cmd 3101/3102/3107).
404    //
405    // v1.4.107: risk_user_account_info::MarginInfo 字段号对齐 mobile proto,
406    // MCP 投影不再裁剪 backend 已返回字段。
407    // ========================================================================
408
409    async fn futu_get_margin_info_impl(
410        &self,
411        Parameters(req): Parameters<MarginInfoReq>,
412        req_ctx: RequestContext<RoleServer>,
413    ) -> std::result::Result<String, String> {
414        tracing::info!(
415            tool = "futu_get_margin_info",
416            env = %req.env,
417            market = %req.market
418        );
419        let client = self
420            .read_client_or_err("futu_get_margin_info", &req_ctx, None, Some(req.acc_id))
421            .await?;
422        Self::wrap_result(
423            handlers::trade::get_margin_info(&client, &req.env, req.acc_id, &req.market).await,
424        )
425    }
426
427    // ========================================================================
428    // v1.4.95 U2-A Tier M (mobile-driven extension): account compliance flag
429    //
430    // 用户高级交易准入 (期权 / 衍生品 / OTC / CFD 等) 强制要求 flag=1.
431    // LLM agent 用此 tool 检查用户合规状态.
432    //
433    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
434    // ========================================================================
435
436    async fn futu_get_account_flag_impl(
437        &self,
438        Parameters(req): Parameters<AccountFlagReq>,
439        req_ctx: RequestContext<RoleServer>,
440    ) -> std::result::Result<String, String> {
441        tracing::info!(
442            tool = "futu_get_account_flag",
443            env = %req.env,
444            flag_id = req.flag_id
445        );
446        let client = self
447            .read_client_or_err("futu_get_account_flag", &req_ctx, None, Some(req.acc_id))
448            .await?;
449        Self::wrap_result(
450            handlers::trade::get_account_flag(&client, &req.env, req.acc_id, req.flag_id).await,
451        )
452    }
453
454    // ========================================================================
455    // v1.4.95 U2-B Tier M (mobile-driven extension): bond holdings + trade prep
456    //
457    // 5 endpoint × 5 cmd_id (9373/9374/9375/10043/10057). 仅 HK / US / SG
458    // 债券账户有数据.
459    //
460    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
461    // ========================================================================
462
463    async fn futu_get_bond_total_asset_impl(
464        &self,
465        Parameters(req): Parameters<BondAccountReq>,
466        req_ctx: RequestContext<RoleServer>,
467    ) -> std::result::Result<String, String> {
468        tracing::info!(
469            tool = "futu_get_bond_total_asset",
470            env = %req.env,
471            market = %req.market
472        );
473        let client = self
474            .read_client_or_err(
475                "futu_get_bond_total_asset",
476                &req_ctx,
477                None,
478                Some(req.acc_id),
479            )
480            .await?;
481        Self::wrap_result(
482            handlers::trade::get_bond_total_asset(&client, &req.env, req.acc_id, &req.market).await,
483        )
484    }
485
486    async fn futu_get_bond_single_asset_impl(
487        &self,
488        Parameters(req): Parameters<BondSymbolReq>,
489        req_ctx: RequestContext<RoleServer>,
490    ) -> std::result::Result<String, String> {
491        tracing::info!(
492            tool = "futu_get_bond_single_asset",
493            env = %req.env,
494            market = %req.market,
495            symbol = %req.symbol
496        );
497        let client = self
498            .read_client_or_err(
499                "futu_get_bond_single_asset",
500                &req_ctx,
501                None,
502                Some(req.acc_id),
503            )
504            .await?;
505        Self::wrap_result(
506            handlers::trade::get_bond_single_asset(
507                &client,
508                &req.env,
509                req.acc_id,
510                &req.market,
511                &req.symbol,
512            )
513            .await,
514        )
515    }
516
517    async fn futu_get_bond_position_list_impl(
518        &self,
519        Parameters(req): Parameters<BondAccountReq>,
520        req_ctx: RequestContext<RoleServer>,
521    ) -> std::result::Result<String, String> {
522        tracing::info!(
523            tool = "futu_get_bond_position_list",
524            env = %req.env,
525            market = %req.market
526        );
527        let client = self
528            .read_client_or_err(
529                "futu_get_bond_position_list",
530                &req_ctx,
531                None,
532                Some(req.acc_id),
533            )
534            .await?;
535        Self::wrap_result(
536            handlers::trade::get_bond_position_list(&client, &req.env, req.acc_id, &req.market)
537                .await,
538        )
539    }
540
541    async fn futu_get_bond_answer_state_impl(
542        &self,
543        Parameters(req): Parameters<BondSymbolReq>,
544        req_ctx: RequestContext<RoleServer>,
545    ) -> std::result::Result<String, String> {
546        tracing::info!(
547            tool = "futu_get_bond_answer_state",
548            env = %req.env,
549            market = %req.market,
550            symbol = %req.symbol
551        );
552        let client = self
553            .read_client_or_err(
554                "futu_get_bond_answer_state",
555                &req_ctx,
556                None,
557                Some(req.acc_id),
558            )
559            .await?;
560        Self::wrap_result(
561            handlers::trade::get_bond_answer_state(
562                &client,
563                &req.env,
564                req.acc_id,
565                &req.market,
566                &req.symbol,
567            )
568            .await,
569        )
570    }
571
572    async fn futu_get_bond_trade_reminder_impl(
573        &self,
574        Parameters(req): Parameters<BondSymbolReq>,
575        req_ctx: RequestContext<RoleServer>,
576    ) -> std::result::Result<String, String> {
577        tracing::info!(
578            tool = "futu_get_bond_trade_reminder",
579            env = %req.env,
580            market = %req.market,
581            symbol = %req.symbol
582        );
583        let client = self
584            .read_client_or_err(
585                "futu_get_bond_trade_reminder",
586                &req_ctx,
587                None,
588                Some(req.acc_id),
589            )
590            .await?;
591        Self::wrap_result(
592            handlers::trade::get_bond_trade_reminder(
593                &client,
594                &req.env,
595                req.acc_id,
596                &req.market,
597                &req.symbol,
598            )
599            .await,
600        )
601    }
602}
603
604include!(concat!(
605    env!("OUT_DIR"),
606    "/generated_mcp_routes_trade_read.rs"
607));