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 (external reviewer 反馈 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<PositionReq>,
91        req_ctx: RequestContext<RoleServer>,
92    ) -> std::result::Result<String, String> {
93        let acc_req = req.as_trd_acc_req();
94        let resolved = self
95            .resolve_read_trd_account("futu_get_positions", &acc_req, &req_ctx)
96            .await?;
97        tracing::info!(
98            tool = "futu_get_positions",
99            market = %req.market,
100            acc_id = resolved.acc_id,
101            currency = ?req.currency,
102            option_strategy_view = req.option_strategy_view,
103        );
104        Self::wrap_result(
105            handlers::trade::get_positions(
106                &resolved.client,
107                &req.env,
108                resolved.acc_id,
109                &req.market,
110                req.currency.as_deref(),
111                req.option_strategy_view,
112            )
113            .await,
114        )
115    }
116
117    #[tool(
118        description = "Get today's orders (including pending / filled / cancelled) for an account in a given market."
119    )]
120    async fn futu_get_orders(
121        &self,
122        Parameters(req): Parameters<TrdAccReq>,
123        req_ctx: RequestContext<RoleServer>,
124    ) -> std::result::Result<String, String> {
125        let resolved = self
126            .resolve_read_trd_account("futu_get_orders", &req, &req_ctx)
127            .await?;
128        tracing::info!(tool = "futu_get_orders", market = %req.market, acc_id = resolved.acc_id);
129        Self::wrap_result(
130            handlers::trade::get_orders(&resolved.client, &req.env, resolved.acc_id, &req.market)
131                .await,
132        )
133    }
134
135    #[tool(description = "Get today's deals / order fills for an account in a given market.")]
136    async fn futu_get_deals(
137        &self,
138        Parameters(req): Parameters<TrdAccReq>,
139        req_ctx: RequestContext<RoleServer>,
140    ) -> std::result::Result<String, String> {
141        let resolved = self
142            .resolve_read_trd_account("futu_get_deals", &req, &req_ctx)
143            .await?;
144        tracing::info!(tool = "futu_get_deals", market = %req.market, acc_id = resolved.acc_id);
145        Self::wrap_result(
146            handlers::trade::get_deals(&resolved.client, &req.env, resolved.acc_id, &req.market)
147                .await,
148        )
149    }
150
151    // ===== v1.4.25: 交易扩展查询 (py-futu-api 对齐) =====
152
153    #[tool(
154        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)."
155    )]
156    async fn futu_get_max_trd_qtys(
157        &self,
158        Parameters(req): Parameters<MaxTrdQtysReq>,
159        req_ctx: RequestContext<RoleServer>,
160    ) -> std::result::Result<String, String> {
161        tracing::info!(tool = "futu_get_max_trd_qtys", market = %req.market, acc_id = req.acc_id, code = %req.code);
162        let client = self
163            .read_client_or_err("futu_get_max_trd_qtys", &req_ctx, None, Some(req.acc_id))
164            .await?;
165        Self::wrap_result(
166            handlers::trade::get_max_trd_qtys(
167                &client,
168                handlers::trade::MaxTrdQtysInput {
169                    env: &req.env,
170                    acc_id: req.acc_id,
171                    market: &req.market,
172                    order_type: req.order_type,
173                    code: &req.code,
174                    price: req.price,
175                    jp_acc_type: req.jp_acc_type,
176                    order_id: req.order_id,
177                },
178            )
179            .await,
180        )
181    }
182
183    #[tool(
184        description = "Query order fee breakdown (commission / platform fee / stamp duty) by order_id_ex list. Python SDK: OpenTradeContext.order_fee_query."
185    )]
186    async fn futu_get_order_fee(
187        &self,
188        Parameters(req): Parameters<OrderFeeReq>,
189        req_ctx: RequestContext<RoleServer>,
190    ) -> std::result::Result<String, String> {
191        tracing::info!(tool = "futu_get_order_fee", market = %req.market, acc_id = req.acc_id, count = req.order_id_ex_list.len());
192        let client = self
193            .read_client_or_err("futu_get_order_fee", &req_ctx, None, Some(req.acc_id))
194            .await?;
195        Self::wrap_result(
196            handlers::trade::get_order_fee(
197                &client,
198                &req.env,
199                req.acc_id,
200                &req.market,
201                &req.order_id_ex_list,
202            )
203            .await,
204        )
205    }
206
207    #[tool(
208        description = "Query margin ratio (long/short permissions, short-pool remaining, long/short initial margin ratios) by symbol list. Python SDK: OpenTradeContext.get_margin_ratio."
209    )]
210    async fn futu_get_margin_ratio(
211        &self,
212        Parameters(req): Parameters<MarginRatioReq>,
213        req_ctx: RequestContext<RoleServer>,
214    ) -> std::result::Result<String, String> {
215        tracing::info!(tool = "futu_get_margin_ratio", market = %req.market, acc_id = req.acc_id, count = req.codes.len());
216        let client = self
217            .read_client_or_err("futu_get_margin_ratio", &req_ctx, None, Some(req.acc_id))
218            .await?;
219        Self::wrap_result(
220            handlers::trade::get_margin_ratio(
221                &client,
222                &req.env,
223                req.acc_id,
224                &req.market,
225                &req.codes,
226            )
227            .await,
228        )
229    }
230
231    #[tool(
232        description = "Query historical orders (filled / cancelled) with optional time range + code filter. Python SDK: OpenTradeContext.history_order_list_query."
233    )]
234    async fn futu_get_history_orders(
235        &self,
236        Parameters(req): Parameters<HistoryQueryReq>,
237        req_ctx: RequestContext<RoleServer>,
238    ) -> std::result::Result<String, String> {
239        tracing::info!(tool = "futu_get_history_orders", market = %req.market, acc_id = req.acc_id);
240        let client = self
241            .read_client_or_err("futu_get_history_orders", &req_ctx, None, Some(req.acc_id))
242            .await?;
243        Self::wrap_result(
244            handlers::trade::get_history_orders(
245                &client,
246                handlers::trade::HistoryQueryInput {
247                    env: &req.env,
248                    acc_id: req.acc_id,
249                    market: &req.market,
250                    code_list: req.code_list,
251                    begin_time: req.begin_time,
252                    end_time: req.end_time,
253                },
254            )
255            .await,
256        )
257    }
258
259    #[tool(
260        description = "Query historical deals / fills with optional time range + code filter. Python SDK: OpenTradeContext.history_deal_list_query."
261    )]
262    async fn futu_get_history_deals(
263        &self,
264        Parameters(req): Parameters<HistoryQueryReq>,
265        req_ctx: RequestContext<RoleServer>,
266    ) -> std::result::Result<String, String> {
267        tracing::info!(tool = "futu_get_history_deals", market = %req.market, acc_id = req.acc_id);
268        let client = self
269            .read_client_or_err("futu_get_history_deals", &req_ctx, None, Some(req.acc_id))
270            .await?;
271        Self::wrap_result(
272            handlers::trade::get_history_deals(
273                &client,
274                handlers::trade::HistoryQueryInput {
275                    env: &req.env,
276                    acc_id: req.acc_id,
277                    market: &req.market,
278                    code_list: req.code_list,
279                    begin_time: req.begin_time,
280                    end_time: req.end_time,
281                },
282            )
283            .await,
284        )
285    }
286
287    #[tool(
288        description = "Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
289    )]
290    async fn futu_get_acc_cash_flow(
291        &self,
292        Parameters(req): Parameters<AccCashFlowReq>,
293        req_ctx: RequestContext<RoleServer>,
294    ) -> std::result::Result<String, String> {
295        tracing::info!(
296            tool = "futu_get_acc_cash_flow",
297            env = %req.env,
298            acc_id = req.acc_id,
299            market = %req.market,
300            date = %req.clearing_date
301        );
302        let client = self
303            .read_client_or_err("futu_get_acc_cash_flow", &req_ctx, None, Some(req.acc_id))
304            .await?;
305        Self::wrap_result(
306            handlers::trade::get_acc_cash_flow(
307                &client,
308                &req.env,
309                req.acc_id,
310                &req.market,
311                &req.clearing_date,
312                req.direction,
313            )
314            .await,
315        )
316    }
317
318    /// v1.4.73 BUG-002 fix: MCP alias `futu_get_flow_summary` 指向同 handler。
319    ///
320    /// external reviewer v1.4.71 验收报告的 AI tester 列此为 P0("MCP 58 工具无对应 `futu_get_flow_summary`,
321    /// 有 `margin_ratio` / `order_fee` 同类查询")。审查后发现:REST 侧两个
322    /// endpoint 并存 `/api/flow-summary` (原) + `/api/acc-cash-flow` (v1.4.51 alias),
323    /// MCP 只有 `futu_get_acc_cash_flow` (v1.4.30 P2 实装)。
324    ///
325    /// 用户期待 MCP 与 REST `/api/flow-summary` 同名的 tool,这是 UX 对称问题。
326    /// 本 alias 保持与主 tool `futu_get_acc_cash_flow` **语义完全一致**,
327    /// 参数结构 + handler 调用 + scope 登记全部共享。
328    #[tool(
329        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."
330    )]
331    async fn futu_get_flow_summary(
332        &self,
333        Parameters(req): Parameters<AccCashFlowReq>,
334        req_ctx: RequestContext<RoleServer>,
335    ) -> std::result::Result<String, String> {
336        // v1.4.73: alias 下面所有行为等同 futu_get_acc_cash_flow
337        tracing::info!(
338            tool = "futu_get_flow_summary",
339            alias_of = "futu_get_acc_cash_flow",
340            env = %req.env,
341            acc_id = req.acc_id,
342            market = %req.market,
343            date = %req.clearing_date
344        );
345        let client = self
346            .read_client_or_err("futu_get_flow_summary", &req_ctx, None, Some(req.acc_id))
347            .await?;
348        Self::wrap_result(
349            handlers::trade::get_acc_cash_flow(
350                &client,
351                &req.env,
352                req.acc_id,
353                &req.market,
354                &req.clearing_date,
355                req.direction,
356            )
357            .await,
358        )
359    }
360
361    // ========================================================================
362    // v1.4.95 U1 (Tier M MCP): cash log mobile-driven extension tools
363    //
364    // 来源: v1.4.94 M1 ship 了 REST + gRPC FTAPI, MCP 推迟到 v1.4.95.
365    // 比 futu_get_acc_cash_flow 字段更全 (10+ vs 3): 时间范围 / 业务分组 /
366    // 货币 / 关键词 / 股票 / 方向 多维过滤 + cursor 分页.
367    //
368    // gateway handler 与 REST `/api/cash-log` 共用同一实现:从已鉴权账户派生
369    // native account/market,并补齐移动端默认分页字段。
370    // ========================================================================
371
372    #[tool(
373        description = "Fetch detailed account cash log entries with richer filters 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."
374    )]
375    async fn futu_get_cash_log(
376        &self,
377        Parameters(req): Parameters<CashLogReq>,
378        req_ctx: RequestContext<RoleServer>,
379    ) -> std::result::Result<String, String> {
380        tracing::info!(
381            tool = "futu_get_cash_log",
382            env = %req.env,
383            acc_id = req.acc_id,
384            market = ?req.market,
385            has_keyword = req.keyword.is_some(),
386            has_symbol = req.symbol.is_some()
387        );
388        let client = self
389            .read_client_or_err("futu_get_cash_log", &req_ctx, None, Some(req.acc_id))
390            .await?;
391        Self::wrap_result(
392            handlers::trade::get_cash_log(
393                &client,
394                handlers::trade::CashLogInput {
395                    env: &req.env,
396                    acc_id: req.acc_id,
397                    begin_time: req.begin_time,
398                    end_time: req.end_time,
399                    biz_group_id: req.biz_group_id,
400                    biz_sub_group_id: req.biz_sub_group_id,
401                    in_out: req.in_out,
402                    keyword: req.keyword,
403                    symbol: req.symbol,
404                    stock_id: req.stock_id,
405                    log_id: req.log_id,
406                    max_cnt: req.max_cnt,
407                    currency: req.currency,
408                },
409            )
410            .await,
411        )
412    }
413
414    #[tool(
415        description = "Fetch a single cash log entry detail. Use after futu_get_cash_log; log_id comes from monthly_logs[].entries[].log_id."
416    )]
417    async fn futu_get_cash_detail(
418        &self,
419        Parameters(req): Parameters<CashDetailReq>,
420        req_ctx: RequestContext<RoleServer>,
421    ) -> std::result::Result<String, String> {
422        tracing::info!(
423            tool = "futu_get_cash_detail",
424            env = %req.env,
425            acc_id = req.acc_id,
426            market = ?req.market,
427            log_id_len = req.log_id.len()
428        );
429        let client = self
430            .read_client_or_err("futu_get_cash_detail", &req_ctx, None, Some(req.acc_id))
431            .await?;
432        Self::wrap_result(
433            handlers::trade::get_cash_detail(&client, &req.env, req.acc_id, req.log_id.clone())
434                .await,
435        )
436    }
437
438    #[tool(
439        description = "Fetch cash log business group, currency, and direction metadata for client UI filters. Returns biz_groups with sub_groups, currencies, and directions."
440    )]
441    async fn futu_get_biz_group(
442        &self,
443        Parameters(req): Parameters<BizGroupReq>,
444        req_ctx: RequestContext<RoleServer>,
445    ) -> std::result::Result<String, String> {
446        tracing::info!(
447            tool = "futu_get_biz_group",
448            env = %req.env,
449            acc_id = req.acc_id,
450            market = ?req.market
451        );
452        let client = self
453            .read_client_or_err("futu_get_biz_group", &req_ctx, None, Some(req.acc_id))
454            .await?;
455        Self::wrap_result(handlers::trade::get_biz_group(&client, &req.env, req.acc_id).await)
456    }
457
458    // ========================================================================
459    // v1.4.95 U2-D Tier M (mobile-driven extension): per-account margin info
460    //
461    // 与 futu_get_margin_ratio (per-security ratio) 互补: 本 tool 给账户全景.
462    // 仅 HK / US / CN_AH 3 市场 (mobile cmd 3101/3102/3107).
463    //
464    // v1.4.107: risk_user_account_info::MarginInfo 字段号对齐 mobile proto,
465    // MCP 投影不再裁剪 backend 已返回字段。
466    // ========================================================================
467
468    #[tool(
469        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."
470    )]
471    async fn futu_get_margin_info(
472        &self,
473        Parameters(req): Parameters<MarginInfoReq>,
474        req_ctx: RequestContext<RoleServer>,
475    ) -> std::result::Result<String, String> {
476        tracing::info!(
477            tool = "futu_get_margin_info",
478            env = %req.env,
479            acc_id = req.acc_id,
480            market = %req.market
481        );
482        let client = self
483            .read_client_or_err("futu_get_margin_info", &req_ctx, None, Some(req.acc_id))
484            .await?;
485        Self::wrap_result(
486            handlers::trade::get_margin_info(&client, &req.env, req.acc_id, &req.market).await,
487        )
488    }
489
490    // ========================================================================
491    // v1.4.95 U2-A Tier M (mobile-driven extension): account compliance flag
492    //
493    // 用户高级交易准入 (期权 / 衍生品 / OTC / CFD 等) 强制要求 flag=1.
494    // LLM agent 用此 tool 检查用户合规状态.
495    //
496    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
497    // ========================================================================
498
499    #[tool(
500        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."
501    )]
502    async fn futu_get_account_flag(
503        &self,
504        Parameters(req): Parameters<AccountFlagReq>,
505        req_ctx: RequestContext<RoleServer>,
506    ) -> std::result::Result<String, String> {
507        tracing::info!(
508            tool = "futu_get_account_flag",
509            env = %req.env,
510            acc_id = req.acc_id,
511            flag_id = req.flag_id
512        );
513        let client = self
514            .read_client_or_err("futu_get_account_flag", &req_ctx, None, Some(req.acc_id))
515            .await?;
516        Self::wrap_result(
517            handlers::trade::get_account_flag(&client, &req.env, req.acc_id, req.flag_id).await,
518        )
519    }
520
521    // ========================================================================
522    // v1.4.95 U2-B Tier M (mobile-driven extension): bond holdings + trade prep
523    //
524    // 5 endpoint × 5 cmd_id (9373/9374/9375/10043/10057). 仅 HK / US / SG
525    // 债券账户有数据.
526    //
527    // v1.4.107: MCP schema 只描述可调用契约;内部 verify/来源证据留在 codex 报告。
528    // ========================================================================
529
530    #[tool(
531        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."
532    )]
533    async fn futu_get_bond_total_asset(
534        &self,
535        Parameters(req): Parameters<BondAccountReq>,
536        req_ctx: RequestContext<RoleServer>,
537    ) -> std::result::Result<String, String> {
538        tracing::info!(
539            tool = "futu_get_bond_total_asset",
540            env = %req.env,
541            acc_id = req.acc_id,
542            market = %req.market
543        );
544        let client = self
545            .read_client_or_err(
546                "futu_get_bond_total_asset",
547                &req_ctx,
548                None,
549                Some(req.acc_id),
550            )
551            .await?;
552        Self::wrap_result(
553            handlers::trade::get_bond_total_asset(&client, &req.env, req.acc_id, &req.market).await,
554        )
555    }
556
557    #[tool(
558        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."
559    )]
560    async fn futu_get_bond_single_asset(
561        &self,
562        Parameters(req): Parameters<BondSymbolReq>,
563        req_ctx: RequestContext<RoleServer>,
564    ) -> std::result::Result<String, String> {
565        tracing::info!(
566            tool = "futu_get_bond_single_asset",
567            env = %req.env,
568            acc_id = req.acc_id,
569            market = %req.market,
570            symbol = %req.symbol
571        );
572        let client = self
573            .read_client_or_err(
574                "futu_get_bond_single_asset",
575                &req_ctx,
576                None,
577                Some(req.acc_id),
578            )
579            .await?;
580        Self::wrap_result(
581            handlers::trade::get_bond_single_asset(
582                &client,
583                &req.env,
584                req.acc_id,
585                &req.market,
586                &req.symbol,
587            )
588            .await,
589        )
590    }
591
592    #[tool(
593        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."
594    )]
595    async fn futu_get_bond_position_list(
596        &self,
597        Parameters(req): Parameters<BondAccountReq>,
598        req_ctx: RequestContext<RoleServer>,
599    ) -> std::result::Result<String, String> {
600        tracing::info!(
601            tool = "futu_get_bond_position_list",
602            env = %req.env,
603            acc_id = req.acc_id,
604            market = %req.market
605        );
606        let client = self
607            .read_client_or_err(
608                "futu_get_bond_position_list",
609                &req_ctx,
610                None,
611                Some(req.acc_id),
612            )
613            .await?;
614        Self::wrap_result(
615            handlers::trade::get_bond_position_list(&client, &req.env, req.acc_id, &req.market)
616                .await,
617        )
618    }
619
620    #[tool(
621        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."
622    )]
623    async fn futu_get_bond_answer_state(
624        &self,
625        Parameters(req): Parameters<BondSymbolReq>,
626        req_ctx: RequestContext<RoleServer>,
627    ) -> std::result::Result<String, String> {
628        tracing::info!(
629            tool = "futu_get_bond_answer_state",
630            env = %req.env,
631            acc_id = req.acc_id,
632            market = %req.market,
633            symbol = %req.symbol
634        );
635        let client = self
636            .read_client_or_err(
637                "futu_get_bond_answer_state",
638                &req_ctx,
639                None,
640                Some(req.acc_id),
641            )
642            .await?;
643        Self::wrap_result(
644            handlers::trade::get_bond_answer_state(
645                &client,
646                &req.env,
647                req.acc_id,
648                &req.market,
649                &req.symbol,
650            )
651            .await,
652        )
653    }
654
655    #[tool(
656        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."
657    )]
658    async fn futu_get_bond_trade_reminder(
659        &self,
660        Parameters(req): Parameters<BondSymbolReq>,
661        req_ctx: RequestContext<RoleServer>,
662    ) -> std::result::Result<String, String> {
663        tracing::info!(
664            tool = "futu_get_bond_trade_reminder",
665            env = %req.env,
666            acc_id = req.acc_id,
667            market = %req.market,
668            symbol = %req.symbol
669        );
670        let client = self
671            .read_client_or_err(
672                "futu_get_bond_trade_reminder",
673                &req_ctx,
674                None,
675                Some(req.acc_id),
676            )
677            .await?;
678        Self::wrap_result(
679            handlers::trade::get_bond_trade_reminder(
680                &client,
681                &req.env,
682                req.acc_id,
683                &req.market,
684                &req.symbol,
685            )
686            .await,
687        )
688    }
689}