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::{
6    RoleServer, handler::server::wrapper::Parameters, service::RequestContext, tool, tool_router,
7};
8
9use super::FutuServer;
10
11#[tool_router(router = trade_read_tool_router, vis = "pub(crate)")]
12impl FutuServer {
13    // ------- 账户(只读) -------
14
15    #[tool(
16        description = "List all trading accounts (real + simulate) visible to the gateway login."
17    )]
18    async fn futu_list_accounts(
19        &self,
20        Parameters(_req): Parameters<NoArgs>,
21        req_ctx: RequestContext<RoleServer>,
22    ) -> std::result::Result<String, String> {
23        // v1.4.103 B5: per-request Bearer 解析 (HTTP) → caller-specific scope check.
24        // v1.4.104 codex F3 (P2) fix: 用 pipeline 返的 snapshot 做 list filter,
25        // 不再 re-resolve from Bearer/startup (避免 SIGHUP race / drift —
26        // pipeline 授权时的 KeyRecord 与 filter 用的同一实例).
27        let snap = self.require_acc_read_with_acc_id("futu_list_accounts", &req_ctx, None, None)?;
28        tracing::info!(tool = "futu_list_accounts");
29        let client = self.client_or_err().await?;
30        // v1.4.103 codex F2.5 (P2) + v1.4.104 codex F3 (P2): 按 caller 的
31        // allowed_acc_ids snapshot (来自 pipeline 授权决策, 同一 KeyRecord)
32        // filter list, 防受限 key 跨账户 enumerate. 与 REST `/api/accounts`
33        // filter (codex F6) 对齐.
34        let allowed_card_nums = snap
35            .rec
36            .as_ref()
37            .and_then(|r| r.allowed_card_nums.as_deref());
38        Self::wrap_result(
39            handlers::trade::list_accounts_filtered(
40                &client,
41                snap.allowed_acc_ids.as_ref(),
42                allowed_card_nums,
43            )
44            .await,
45        )
46    }
47
48    #[tool(
49        description = "Get account funds summary (total assets, cash, market value, buying power) for a given account + market.\n\n\
50                       **Cash semantics**: top-level `cash` field is \
51                       backend's summary cash in the response `currency` (i.e. `union_currency` for \
52                       futures/universal, primary market currency for legacy accounts). It is **NOT** \
53                       the sum of `cash_info_list[].cash` across currencies (different currencies \
54                       cannot be summed without FX conversion). For per-currency breakdown, read \
55                       `cash_info_list`. To match Futu mobile app's '现金总值 in HKD' display, \
56                       client-side compute `sum(cash_info_list[i].cash * fx_rate(currency[i], HKD))` \
57                       — daemon does not perform FX aggregation."
58    )]
59    async fn futu_get_funds(
60        &self,
61        Parameters(req): Parameters<TrdAccReq>,
62        req_ctx: RequestContext<RoleServer>,
63    ) -> std::result::Result<String, String> {
64        let resolved = self
65            .resolve_read_trd_account("futu_get_funds", &req, &req_ctx)
66            .await?;
67        tracing::info!(
68            tool = "futu_get_funds",
69            market = %req.market,
70            acc_id = resolved.acc_id,
71            env = %req.env,
72            currency = ?req.currency,
73        );
74        // v1.4.103 (deger 反馈 P1): 综合账户币种不一致 fix — 透传 currency.
75        Self::wrap_result(
76            handlers::trade::get_funds_with_currency(
77                &resolved.client,
78                &req.env,
79                resolved.acc_id,
80                &req.market,
81                req.currency.as_deref(),
82            )
83            .await,
84        )
85    }
86
87    #[tool(description = "Get current positions (holdings) for an account in a given market.")]
88    async fn futu_get_positions(
89        &self,
90        Parameters(req): Parameters<TrdAccReq>,
91        req_ctx: RequestContext<RoleServer>,
92    ) -> std::result::Result<String, String> {
93        let resolved = self
94            .resolve_read_trd_account("futu_get_positions", &req, &req_ctx)
95            .await?;
96        tracing::info!(tool = "futu_get_positions", market = %req.market, acc_id = resolved.acc_id);
97        Self::wrap_result(
98            handlers::trade::get_positions(
99                &resolved.client,
100                &req.env,
101                resolved.acc_id,
102                &req.market,
103            )
104            .await,
105        )
106    }
107
108    #[tool(
109        description = "Get today's orders (including pending / filled / cancelled) for an account in a given market."
110    )]
111    async fn futu_get_orders(
112        &self,
113        Parameters(req): Parameters<TrdAccReq>,
114        req_ctx: RequestContext<RoleServer>,
115    ) -> std::result::Result<String, String> {
116        let resolved = self
117            .resolve_read_trd_account("futu_get_orders", &req, &req_ctx)
118            .await?;
119        tracing::info!(tool = "futu_get_orders", market = %req.market, acc_id = resolved.acc_id);
120        Self::wrap_result(
121            handlers::trade::get_orders(&resolved.client, &req.env, resolved.acc_id, &req.market)
122                .await,
123        )
124    }
125
126    #[tool(description = "Get today's deals / order fills for an account in a given market.")]
127    async fn futu_get_deals(
128        &self,
129        Parameters(req): Parameters<TrdAccReq>,
130        req_ctx: RequestContext<RoleServer>,
131    ) -> std::result::Result<String, String> {
132        let resolved = self
133            .resolve_read_trd_account("futu_get_deals", &req, &req_ctx)
134            .await?;
135        tracing::info!(tool = "futu_get_deals", market = %req.market, acc_id = resolved.acc_id);
136        Self::wrap_result(
137            handlers::trade::get_deals(&resolved.client, &req.env, resolved.acc_id, &req.market)
138                .await,
139        )
140    }
141
142    // ===== v1.4.25: 交易扩展查询 (py-futu-api 对齐) =====
143
144    #[tool(
145        description = "Max buy/sell/short/buy-back qtys before placing an order. Python SDK: OpenTradeContext.acctradinginfo_query. For NORMAL (limit) orders, price is required. order_type aligns with Trd_Common.OrderType enum (1=limit, 2=market, etc)."
146    )]
147    async fn futu_get_max_trd_qtys(
148        &self,
149        Parameters(req): Parameters<MaxTrdQtysReq>,
150        req_ctx: RequestContext<RoleServer>,
151    ) -> std::result::Result<String, String> {
152        tracing::info!(tool = "futu_get_max_trd_qtys", market = %req.market, acc_id = req.acc_id, code = %req.code);
153        let client = self
154            .read_client_or_err("futu_get_max_trd_qtys", &req_ctx, None, Some(req.acc_id))
155            .await?;
156        Self::wrap_result(
157            handlers::trade::get_max_trd_qtys(
158                &client,
159                handlers::trade::MaxTrdQtysInput {
160                    env: &req.env,
161                    acc_id: req.acc_id,
162                    market: &req.market,
163                    order_type: req.order_type,
164                    code: &req.code,
165                    price: req.price,
166                    order_id: req.order_id,
167                },
168            )
169            .await,
170        )
171    }
172
173    #[tool(
174        description = "Query order fee breakdown (commission / platform fee / stamp duty) by order_id_ex list. Python SDK: OpenTradeContext.order_fee_query."
175    )]
176    async fn futu_get_order_fee(
177        &self,
178        Parameters(req): Parameters<OrderFeeReq>,
179        req_ctx: RequestContext<RoleServer>,
180    ) -> std::result::Result<String, String> {
181        tracing::info!(tool = "futu_get_order_fee", market = %req.market, acc_id = req.acc_id, count = req.order_id_ex_list.len());
182        let client = self
183            .read_client_or_err("futu_get_order_fee", &req_ctx, None, Some(req.acc_id))
184            .await?;
185        Self::wrap_result(
186            handlers::trade::get_order_fee(
187                &client,
188                &req.env,
189                req.acc_id,
190                &req.market,
191                &req.order_id_ex_list,
192            )
193            .await,
194        )
195    }
196
197    #[tool(
198        description = "Query margin ratio (long/short permissions, short-pool remaining, long/short initial margin ratios) by symbol list. Python SDK: OpenTradeContext.get_margin_ratio."
199    )]
200    async fn futu_get_margin_ratio(
201        &self,
202        Parameters(req): Parameters<MarginRatioReq>,
203        req_ctx: RequestContext<RoleServer>,
204    ) -> std::result::Result<String, String> {
205        tracing::info!(tool = "futu_get_margin_ratio", market = %req.market, acc_id = req.acc_id, count = req.codes.len());
206        let client = self
207            .read_client_or_err("futu_get_margin_ratio", &req_ctx, None, Some(req.acc_id))
208            .await?;
209        Self::wrap_result(
210            handlers::trade::get_margin_ratio(
211                &client,
212                &req.env,
213                req.acc_id,
214                &req.market,
215                &req.codes,
216            )
217            .await,
218        )
219    }
220
221    #[tool(
222        description = "Query historical orders (filled / cancelled) with optional time range + code filter. Python SDK: OpenTradeContext.history_order_list_query."
223    )]
224    async fn futu_get_history_orders(
225        &self,
226        Parameters(req): Parameters<HistoryQueryReq>,
227        req_ctx: RequestContext<RoleServer>,
228    ) -> std::result::Result<String, String> {
229        tracing::info!(tool = "futu_get_history_orders", market = %req.market, acc_id = req.acc_id);
230        let client = self
231            .read_client_or_err("futu_get_history_orders", &req_ctx, None, Some(req.acc_id))
232            .await?;
233        Self::wrap_result(
234            handlers::trade::get_history_orders(
235                &client,
236                handlers::trade::HistoryQueryInput {
237                    env: &req.env,
238                    acc_id: req.acc_id,
239                    market: &req.market,
240                    code_list: req.code_list,
241                    begin_time: req.begin_time,
242                    end_time: req.end_time,
243                },
244            )
245            .await,
246        )
247    }
248
249    #[tool(
250        description = "Query historical deals / fills with optional time range + code filter. Python SDK: OpenTradeContext.history_deal_list_query."
251    )]
252    async fn futu_get_history_deals(
253        &self,
254        Parameters(req): Parameters<HistoryQueryReq>,
255        req_ctx: RequestContext<RoleServer>,
256    ) -> std::result::Result<String, String> {
257        tracing::info!(tool = "futu_get_history_deals", market = %req.market, acc_id = req.acc_id);
258        let client = self
259            .read_client_or_err("futu_get_history_deals", &req_ctx, None, Some(req.acc_id))
260            .await?;
261        Self::wrap_result(
262            handlers::trade::get_history_deals(
263                &client,
264                handlers::trade::HistoryQueryInput {
265                    env: &req.env,
266                    acc_id: req.acc_id,
267                    market: &req.market,
268                    code_list: req.code_list,
269                    begin_time: req.begin_time,
270                    end_time: req.end_time,
271                },
272            )
273            .await,
274        )
275    }
276
277    #[tool(
278        description = "Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
279    )]
280    async fn futu_get_acc_cash_flow(
281        &self,
282        Parameters(req): Parameters<AccCashFlowReq>,
283        req_ctx: RequestContext<RoleServer>,
284    ) -> std::result::Result<String, String> {
285        tracing::info!(
286            tool = "futu_get_acc_cash_flow",
287            env = %req.env,
288            acc_id = req.acc_id,
289            market = %req.market,
290            date = %req.clearing_date
291        );
292        let client = self
293            .read_client_or_err("futu_get_acc_cash_flow", &req_ctx, None, Some(req.acc_id))
294            .await?;
295        Self::wrap_result(
296            handlers::trade::get_acc_cash_flow(
297                &client,
298                &req.env,
299                req.acc_id,
300                &req.market,
301                &req.clearing_date,
302                req.direction,
303            )
304            .await,
305        )
306    }
307
308    /// v1.4.73 BUG-002 fix: MCP alias `futu_get_flow_summary` 指向同 handler。
309    ///
310    /// eli v1.4.71 验收报告的 AI tester 列此为 P0("MCP 58 工具无对应 `futu_get_flow_summary`,
311    /// 有 `margin_ratio` / `order_fee` 同类查询")。审查后发现:REST 侧两个
312    /// endpoint 并存 `/api/flow-summary` (原) + `/api/acc-cash-flow` (v1.4.51 alias),
313    /// MCP 只有 `futu_get_acc_cash_flow` (v1.4.30 P2 实装)。
314    ///
315    /// 用户期待 MCP 与 REST `/api/flow-summary` 同名的 tool,这是 UX 对称问题。
316    /// 本 alias 保持与主 tool `futu_get_acc_cash_flow` **语义完全一致**,
317    /// 参数结构 + handler 调用 + scope 登记全部共享。
318    #[tool(
319        description = "Alias of futu_get_acc_cash_flow (MCP-REST naming symmetry with /api/flow-summary). Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
320    )]
321    async fn futu_get_flow_summary(
322        &self,
323        Parameters(req): Parameters<AccCashFlowReq>,
324        req_ctx: RequestContext<RoleServer>,
325    ) -> std::result::Result<String, String> {
326        // v1.4.73: alias 下面所有行为等同 futu_get_acc_cash_flow
327        tracing::info!(
328            tool = "futu_get_flow_summary",
329            alias_of = "futu_get_acc_cash_flow",
330            env = %req.env,
331            acc_id = req.acc_id,
332            market = %req.market,
333            date = %req.clearing_date
334        );
335        let client = self
336            .read_client_or_err("futu_get_flow_summary", &req_ctx, None, Some(req.acc_id))
337            .await?;
338        Self::wrap_result(
339            handlers::trade::get_acc_cash_flow(
340                &client,
341                &req.env,
342                req.acc_id,
343                &req.market,
344                &req.clearing_date,
345                req.direction,
346            )
347            .await,
348        )
349    }
350
351    // ========================================================================
352    // v1.4.95 U1 (Tier M MCP): cash log mobile-driven extension tools
353    //
354    // 来源: v1.4.94 M1 ship 了 REST + gRPC FTAPI, MCP 推迟到 v1.4.95.
355    // 比 futu_get_acc_cash_flow 字段更全 (10+ vs 3): 时间范围 / 业务分组 /
356    // 货币 / 关键词 / 股票 / 方向 多维过滤 + cursor 分页.
357    //
358    // gateway handler 与 REST `/api/cash-log` 共用同一实现:从已鉴权账户派生
359    // native account/market,并补齐移动端默认分页字段。
360    // ========================================================================
361
362    #[tool(
363        description = "Fetch detailed account cash log entries (mobile-driven, richer than futu_get_acc_cash_flow). Native time range, business group / currency / keyword / symbol / direction filters, cursor-based pagination. When max_cnt is omitted the daemon uses the mobile default of 50."
364    )]
365    async fn futu_get_cash_log(
366        &self,
367        Parameters(req): Parameters<CashLogReq>,
368        req_ctx: RequestContext<RoleServer>,
369    ) -> std::result::Result<String, String> {
370        tracing::info!(
371            tool = "futu_get_cash_log",
372            env = %req.env,
373            acc_id = req.acc_id,
374            market = ?req.market,
375            has_keyword = req.keyword.is_some(),
376            has_symbol = req.symbol.is_some()
377        );
378        let client = self
379            .read_client_or_err("futu_get_cash_log", &req_ctx, None, Some(req.acc_id))
380            .await?;
381        Self::wrap_result(
382            handlers::trade::get_cash_log(
383                &client,
384                handlers::trade::CashLogInput {
385                    env: &req.env,
386                    acc_id: req.acc_id,
387                    begin_time: req.begin_time,
388                    end_time: req.end_time,
389                    biz_group_id: req.biz_group_id,
390                    biz_sub_group_id: req.biz_sub_group_id,
391                    in_out: req.in_out,
392                    keyword: req.keyword,
393                    symbol: req.symbol,
394                    stock_id: req.stock_id,
395                    log_id: req.log_id,
396                    max_cnt: req.max_cnt,
397                    currency: req.currency,
398                },
399            )
400            .await,
401        )
402    }
403
404    #[tool(
405        description = "Fetch a single cash log entry detail. Use after futu_get_cash_log; log_id comes from monthly_logs[].entries[].log_id."
406    )]
407    async fn futu_get_cash_detail(
408        &self,
409        Parameters(req): Parameters<CashDetailReq>,
410        req_ctx: RequestContext<RoleServer>,
411    ) -> std::result::Result<String, String> {
412        tracing::info!(
413            tool = "futu_get_cash_detail",
414            env = %req.env,
415            acc_id = req.acc_id,
416            market = ?req.market,
417            log_id_len = req.log_id.len()
418        );
419        let client = self
420            .read_client_or_err("futu_get_cash_detail", &req_ctx, None, Some(req.acc_id))
421            .await?;
422        Self::wrap_result(
423            handlers::trade::get_cash_detail(&client, &req.env, req.acc_id, req.log_id.clone())
424                .await,
425        )
426    }
427
428    #[tool(
429        description = "Fetch cash log business group, currency, and direction metadata for client UI filters. Returns biz_groups with sub_groups, currencies, and directions."
430    )]
431    async fn futu_get_biz_group(
432        &self,
433        Parameters(req): Parameters<BizGroupReq>,
434        req_ctx: RequestContext<RoleServer>,
435    ) -> std::result::Result<String, String> {
436        tracing::info!(
437            tool = "futu_get_biz_group",
438            env = %req.env,
439            acc_id = req.acc_id,
440            market = ?req.market
441        );
442        let client = self
443            .read_client_or_err("futu_get_biz_group", &req_ctx, None, Some(req.acc_id))
444            .await?;
445        Self::wrap_result(handlers::trade::get_biz_group(&client, &req.env, req.acc_id).await)
446    }
447
448    // ========================================================================
449    // v1.4.95 U2-D Tier M (mobile-driven extension): per-account margin info
450    //
451    // 与 futu_get_margin_ratio (per-security ratio) 互补: 本 tool 给账户全景.
452    // 仅 HK / US / CN_AH 3 市场 (mobile cmd 3101/3102/3107).
453    //
454    // v1.4.107: risk_user_account_info::MarginInfo 字段号对齐 mobile proto,
455    // MCP 投影不再裁剪 backend 已返回字段。
456    // ========================================================================
457
458    #[tool(
459        description = "Per-account margin info: buying power, leverage, risk status, liquidity, HK margin fields, and mobile risk metadata. Supports HK / US / CN_AH markets (mobile cmd 3101/3102/3107). Complements futu_get_margin_ratio, which is per-security."
460    )]
461    async fn futu_get_margin_info(
462        &self,
463        Parameters(req): Parameters<MarginInfoReq>,
464        req_ctx: RequestContext<RoleServer>,
465    ) -> std::result::Result<String, String> {
466        tracing::info!(
467            tool = "futu_get_margin_info",
468            env = %req.env,
469            acc_id = req.acc_id,
470            market = %req.market
471        );
472        let client = self
473            .read_client_or_err("futu_get_margin_info", &req_ctx, None, Some(req.acc_id))
474            .await?;
475        Self::wrap_result(
476            handlers::trade::get_margin_info(&client, &req.env, req.acc_id, &req.market).await,
477        )
478    }
479
480    // ========================================================================
481    // v1.4.95 U2-A Tier M (mobile-driven extension): account compliance flag
482    //
483    // 用户高级交易准入 (期权 / 衍生品 / OTC / CFD 等) 强制要求 flag=1.
484    // LLM agent 用此 tool 检查用户合规状态.
485    //
486    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
487    // ========================================================================
488
489    #[tool(
490        description = "Query account compliance flag (product access, risk disclosure, opt-in status). Common flag_id values: 5=US options, 22=derivatives disclosure, 10=fund KYC R1-R5, 16=PDT, 23=US OTC, 11=HK options. The response includes item_present and flag_value_present so clients can distinguish a missing flag record from an explicit flag_value=0."
491    )]
492    async fn futu_get_account_flag(
493        &self,
494        Parameters(req): Parameters<AccountFlagReq>,
495        req_ctx: RequestContext<RoleServer>,
496    ) -> std::result::Result<String, String> {
497        tracing::info!(
498            tool = "futu_get_account_flag",
499            env = %req.env,
500            acc_id = req.acc_id,
501            flag_id = req.flag_id
502        );
503        let client = self
504            .read_client_or_err("futu_get_account_flag", &req_ctx, None, Some(req.acc_id))
505            .await?;
506        Self::wrap_result(
507            handlers::trade::get_account_flag(&client, &req.env, req.acc_id, req.flag_id).await,
508        )
509    }
510
511    // ========================================================================
512    // v1.4.95 U2-B Tier M (mobile-driven extension): bond holdings + trade prep
513    //
514    // 5 endpoint × 5 cmd_id (9373/9374/9375/10043/10057). 仅 HK / US / SG
515    // 债券账户有数据.
516    //
517    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
518    // ========================================================================
519
520    #[tool(
521        description = "Bond account total asset and P&L summary for HK/US/SG bond accounts. Returns total_asset, position_incomes, today_incomes, accrued_interest, and ccy."
522    )]
523    async fn futu_get_bond_total_asset(
524        &self,
525        Parameters(req): Parameters<BondAccountReq>,
526        req_ctx: RequestContext<RoleServer>,
527    ) -> std::result::Result<String, String> {
528        tracing::info!(
529            tool = "futu_get_bond_total_asset",
530            env = %req.env,
531            acc_id = req.acc_id,
532            market = %req.market
533        );
534        let client = self
535            .read_client_or_err(
536                "futu_get_bond_total_asset",
537                &req_ctx,
538                None,
539                Some(req.acc_id),
540            )
541            .await?;
542        Self::wrap_result(
543            handlers::trade::get_bond_total_asset(&client, &req.env, req.acc_id, &req.market).await,
544        )
545    }
546
547    #[tool(
548        description = "Single bond position for HK/US/SG bond accounts, including market value, quantity, cost, expiry, dividend schedule, accrued interest, legacy notice fields, notice_list, currency, and price."
549    )]
550    async fn futu_get_bond_single_asset(
551        &self,
552        Parameters(req): Parameters<BondSymbolReq>,
553        req_ctx: RequestContext<RoleServer>,
554    ) -> std::result::Result<String, String> {
555        tracing::info!(
556            tool = "futu_get_bond_single_asset",
557            env = %req.env,
558            acc_id = req.acc_id,
559            market = %req.market,
560            symbol = %req.symbol
561        );
562        let client = self
563            .read_client_or_err(
564                "futu_get_bond_single_asset",
565                &req_ctx,
566                None,
567                Some(req.acc_id),
568            )
569            .await?;
570        Self::wrap_result(
571            handlers::trade::get_bond_single_asset(
572                &client,
573                &req.env,
574                req.acc_id,
575                &req.market,
576                &req.symbol,
577            )
578            .await,
579        )
580    }
581
582    #[tool(
583        description = "Bond account position list for HK/US/SG bond accounts. Returns total and bond_list items with name, symbol, market value, quantity, price, cost, incomes, accrued interest, notice, call flag, and ccy."
584    )]
585    async fn futu_get_bond_position_list(
586        &self,
587        Parameters(req): Parameters<BondAccountReq>,
588        req_ctx: RequestContext<RoleServer>,
589    ) -> std::result::Result<String, String> {
590        tracing::info!(
591            tool = "futu_get_bond_position_list",
592            env = %req.env,
593            acc_id = req.acc_id,
594            market = %req.market
595        );
596        let client = self
597            .read_client_or_err(
598                "futu_get_bond_position_list",
599                &req_ctx,
600                None,
601                Some(req.acc_id),
602            )
603            .await?;
604        Self::wrap_result(
605            handlers::trade::get_bond_position_list(&client, &req.env, req.acc_id, &req.market)
606                .await,
607        )
608    }
609
610    #[tool(
611        description = "Query whether the user needs to answer a suitability questionnaire before bond trading. Returns need_to_answer plus notice fields such as title, content, and confirm_url."
612    )]
613    async fn futu_get_bond_answer_state(
614        &self,
615        Parameters(req): Parameters<BondSymbolReq>,
616        req_ctx: RequestContext<RoleServer>,
617    ) -> std::result::Result<String, String> {
618        tracing::info!(
619            tool = "futu_get_bond_answer_state",
620            env = %req.env,
621            acc_id = req.acc_id,
622            market = %req.market,
623            symbol = %req.symbol
624        );
625        let client = self
626            .read_client_or_err(
627                "futu_get_bond_answer_state",
628                &req_ctx,
629                None,
630                Some(req.acc_id),
631            )
632            .await?;
633        Self::wrap_result(
634            handlers::trade::get_bond_answer_state(
635                &client,
636                &req.env,
637                req.acc_id,
638                &req.market,
639                &req.symbol,
640            )
641            .await,
642        )
643    }
644
645    #[tool(
646        description = "Bond trade reminders for buy/sell availability, complex product, high risk, and pre-qualification. Returns ReminderItem fields for tradeable, complex_product, high_risk, sell_tradeable, and pre_qualification."
647    )]
648    async fn futu_get_bond_trade_reminder(
649        &self,
650        Parameters(req): Parameters<BondSymbolReq>,
651        req_ctx: RequestContext<RoleServer>,
652    ) -> std::result::Result<String, String> {
653        tracing::info!(
654            tool = "futu_get_bond_trade_reminder",
655            env = %req.env,
656            acc_id = req.acc_id,
657            market = %req.market,
658            symbol = %req.symbol
659        );
660        let client = self
661            .read_client_or_err(
662                "futu_get_bond_trade_reminder",
663                &req_ctx,
664                None,
665                Some(req.acc_id),
666            )
667            .await?;
668        Self::wrap_result(
669            handlers::trade::get_bond_trade_reminder(
670                &client,
671                &req.env,
672                req.acc_id,
673                &req.market,
674                &req.symbol,
675            )
676            .await,
677        )
678    }
679}