Skip to main content

futu_mcp/tools/
reference.rs

1//! MCP market analysis, reference-data, and quote-system metadata tools.
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 = reference_tool_router, vis = "pub(crate)")]
12impl FutuServer {
13    // ===== v1.4.25: 行情分析 =====
14
15    #[tool(
16        description = "Capital flow (net inflow) time series for a security. Python SDK: OpenQuoteContext.get_capital_flow."
17    )]
18    async fn futu_get_capital_flow(
19        &self,
20        Parameters(req): Parameters<CapitalFlowReq>,
21        req_ctx: RequestContext<RoleServer>,
22    ) -> std::result::Result<String, String> {
23        tracing::info!(tool = "futu_get_capital_flow", symbol = %req.symbol);
24        let client = self
25            .read_client_or_err("futu_get_capital_flow", &req_ctx, None, None)
26            .await?;
27        Self::wrap_result(
28            handlers::analysis::get_capital_flow(
29                &client,
30                &req.symbol,
31                req.period_type,
32                req.begin_time,
33                req.end_time,
34            )
35            .await,
36        )
37    }
38
39    #[tool(
40        description = "Capital distribution (super/big/mid/small order in/out flow amounts) snapshot. Python SDK: OpenQuoteContext.get_capital_distribution."
41    )]
42    async fn futu_get_capital_distribution(
43        &self,
44        Parameters(req): Parameters<SymbolReq>,
45        req_ctx: RequestContext<RoleServer>,
46    ) -> std::result::Result<String, String> {
47        tracing::info!(tool = "futu_get_capital_distribution", symbol = %req.symbol);
48        let client = self
49            .read_client_or_err("futu_get_capital_distribution", &req_ctx, None, None)
50            .await?;
51        Self::wrap_result(handlers::analysis::get_capital_distribution(&client, &req.symbol).await)
52    }
53
54    #[tool(
55        description = "Query current market state for a list of securities (open/closed/lunch-break etc). Python SDK: OpenQuoteContext.get_market_state."
56    )]
57    async fn futu_get_market_state(
58        &self,
59        Parameters(req): Parameters<MarketStateReq>,
60        req_ctx: RequestContext<RoleServer>,
61    ) -> std::result::Result<String, String> {
62        tracing::info!(tool = "futu_get_market_state", count = req.symbols.len());
63        let client = self
64            .read_client_or_err("futu_get_market_state", &req_ctx, None, None)
65            .await?;
66        Self::wrap_result(handlers::analysis::get_market_state(&client, &req.symbols).await)
67    }
68
69    // ===== v1.4.26 新增:history_kline / owner_plate / reference / option_chain =====
70
71    #[tool(
72        description = "Historical K-line / OHLCV time series with rehab type control (forward/backward/none) and pagination-friendly max_count. Python SDK: OpenQuoteContext.request_history_kline."
73    )]
74    async fn futu_get_history_kline(
75        &self,
76        Parameters(req): Parameters<HistoryKLineReq>,
77        req_ctx: RequestContext<RoleServer>,
78    ) -> std::result::Result<String, String> {
79        tracing::info!(
80            tool = "futu_get_history_kline",
81            symbol = %req.symbol,
82            kl_type = %req.kl_type,
83            rehab = %req.rehab_type
84        );
85        let client = self
86            .read_client_or_err("futu_get_history_kline", &req_ctx, None, None)
87            .await?;
88        Self::wrap_result(
89            handlers::analysis::get_history_kline(
90                &client,
91                &req.symbol,
92                &req.kl_type,
93                &req.rehab_type,
94                &req.begin,
95                &req.end,
96                req.max_count,
97            )
98            .await,
99        )
100    }
101
102    #[tool(
103        description = "List plates (industry/concept/region) that contain given stocks. Python SDK: OpenQuoteContext.get_owner_plate."
104    )]
105    async fn futu_get_owner_plate(
106        &self,
107        Parameters(req): Parameters<SymbolListReq>,
108        req_ctx: RequestContext<RoleServer>,
109    ) -> std::result::Result<String, String> {
110        tracing::info!(tool = "futu_get_owner_plate", count = req.symbols.len());
111        let client = self
112            .read_client_or_err("futu_get_owner_plate", &req_ctx, None, None)
113            .await?;
114        Self::wrap_result(handlers::analysis::get_owner_plate(&client, &req.symbols).await)
115    }
116
117    #[tool(
118        description = "Related securities of an underlying: list all warrants/futures/options derived from a given stock. Python SDK: OpenQuoteContext.get_referencestock_list."
119    )]
120    async fn futu_get_reference(
121        &self,
122        Parameters(req): Parameters<ReferenceReq>,
123        req_ctx: RequestContext<RoleServer>,
124    ) -> std::result::Result<String, String> {
125        tracing::info!(
126            tool = "futu_get_reference",
127            symbol = %req.symbol,
128            reference_type = %req.reference_type
129        );
130        let client = self
131            .read_client_or_err("futu_get_reference", &req_ctx, None, None)
132            .await?;
133        Self::wrap_result(
134            handlers::analysis::get_reference(&client, &req.symbol, &req.reference_type).await,
135        )
136    }
137
138    #[tool(
139        description = "Option chain of an underlying stock within an expiry date range, grouped by strike time with call/put symbol lists. Python SDK: OpenQuoteContext.get_option_chain."
140    )]
141    async fn futu_get_option_chain(
142        &self,
143        Parameters(req): Parameters<OptionChainReq>,
144        req_ctx: RequestContext<RoleServer>,
145    ) -> std::result::Result<String, String> {
146        tracing::info!(
147            tool = "futu_get_option_chain",
148            owner = %req.owner_symbol,
149            begin = %req.begin_time,
150            end = %req.end_time
151        );
152        let client = self
153            .read_client_or_err("futu_get_option_chain", &req_ctx, None, None)
154            .await?;
155        // v1.4.38 Phase 3: 把 MCP 侧的 Greek filter 字段组装成 proto DataFilter
156        let any_filter = req.delta_min.is_some()
157            || req.delta_max.is_some()
158            || req.iv_min.is_some()
159            || req.iv_max.is_some()
160            || req.oi_min.is_some()
161            || req.oi_max.is_some()
162            || req.gamma_min.is_some()
163            || req.gamma_max.is_some()
164            || req.vega_min.is_some()
165            || req.vega_max.is_some()
166            || req.theta_min.is_some()
167            || req.theta_max.is_some();
168        let data_filter = if any_filter {
169            Some(futu_proto::qot_get_option_chain::DataFilter {
170                implied_volatility_min: req.iv_min,
171                implied_volatility_max: req.iv_max,
172                delta_min: req.delta_min,
173                delta_max: req.delta_max,
174                gamma_min: req.gamma_min,
175                gamma_max: req.gamma_max,
176                vega_min: req.vega_min,
177                vega_max: req.vega_max,
178                theta_min: req.theta_min,
179                theta_max: req.theta_max,
180                rho_min: None,
181                rho_max: None,
182                net_open_interest_min: None,
183                net_open_interest_max: None,
184                open_interest_min: req.oi_min,
185                open_interest_max: req.oi_max,
186                vol_min: None,
187                vol_max: None,
188            })
189        } else {
190            None
191        };
192        Self::wrap_result(
193            handlers::analysis::get_option_chain(
194                &client,
195                handlers::analysis::OptionChainInput {
196                    owner_symbol: &req.owner_symbol,
197                    begin_time: &req.begin_time,
198                    end_time: &req.end_time,
199                    option_type_str: req.option_type.as_deref(),
200                    data_filter,
201                },
202            )
203            .await,
204        )
205    }
206
207    // ------- v1.4.29 参考数据 / 衍生证券 -------
208
209    #[tool(
210        description = "List warrants on an underlying stock (or whole-market when owner_symbol omitted), sorted by volume desc. Python SDK: OpenQuoteContext.get_warrant. For advanced filtering (strike/premium/delta/etc.) use REST /api/warrant directly."
211    )]
212    async fn futu_get_warrant(
213        &self,
214        Parameters(req): Parameters<WarrantReq>,
215        req_ctx: RequestContext<RoleServer>,
216    ) -> std::result::Result<String, String> {
217        tracing::info!(
218            tool = "futu_get_warrant",
219            // v1.4.90 P2-C: Option<String> → &str ("" 哨兵)
220            owner = %crate::state::audit_fmt::opt_str(req.owner_symbol.as_deref()),
221            begin = req.begin,
222            num = req.num
223        );
224        let client = self
225            .read_client_or_err("futu_get_warrant", &req_ctx, None, None)
226            .await?;
227        Self::wrap_result(
228            handlers::reference::get_warrant(
229                &client,
230                req.owner_symbol.as_deref(),
231                req.begin,
232                req.num,
233            )
234            .await,
235        )
236    }
237
238    #[tool(
239        description = "Upcoming / recent IPOs for a market. Python SDK: OpenQuoteContext.get_ipo_list. market: 1=HK, 11=US, 21=SH, 22=SZ."
240    )]
241    async fn futu_get_ipo_list(
242        &self,
243        Parameters(req): Parameters<IpoListReq>,
244        req_ctx: RequestContext<RoleServer>,
245    ) -> std::result::Result<String, String> {
246        tracing::info!(tool = "futu_get_ipo_list", market = req.market);
247        let client = self
248            .read_client_or_err("futu_get_ipo_list", &req_ctx, None, None)
249            .await?;
250        Self::wrap_result(handlers::reference::get_ipo_list(&client, req.market).await)
251    }
252
253    #[tool(
254        description = "Future contract info (contract size, last trade date, trading hours). Python SDK: OpenQuoteContext.get_future_info."
255    )]
256    async fn futu_get_future_info(
257        &self,
258        Parameters(req): Parameters<FutureInfoReq>,
259        req_ctx: RequestContext<RoleServer>,
260    ) -> std::result::Result<String, String> {
261        tracing::info!(tool = "futu_get_future_info", count = req.symbols.len());
262        let client = self
263            .read_client_or_err("futu_get_future_info", &req_ctx, None, None)
264            .await?;
265        Self::wrap_result(handlers::reference::get_future_info(&client, &req.symbols).await)
266    }
267
268    #[tool(
269        description = "List the user's custom + system watchlist groups. Python SDK: OpenQuoteContext.get_user_security_group. group_type: 1=all, 2=custom, 3=system."
270    )]
271    async fn futu_get_user_security_group(
272        &self,
273        Parameters(req): Parameters<UserSecurityGroupReq>,
274        req_ctx: RequestContext<RoleServer>,
275    ) -> std::result::Result<String, String> {
276        tracing::info!(
277            tool = "futu_get_user_security_group",
278            group_type = req.group_type
279        );
280        let client = self
281            .read_client_or_err("futu_get_user_security_group", &req_ctx, None, None)
282            .await?;
283        Self::wrap_result(
284            handlers::reference::get_user_security_group(&client, req.group_type).await,
285        )
286    }
287
288    #[tool(
289        description = "Stock filter / scanner (minimal: market + pagination). Python SDK: OpenQuoteContext.get_stock_filter. For condition-based filters (PE/cap/volume/etc.) use REST /api/stock-filter directly."
290    )]
291    async fn futu_get_stock_filter(
292        &self,
293        Parameters(req): Parameters<StockFilterReq>,
294        req_ctx: RequestContext<RoleServer>,
295    ) -> std::result::Result<String, String> {
296        tracing::info!(
297            tool = "futu_get_stock_filter",
298            market = req.market,
299            begin = req.begin,
300            num = req.num
301        );
302        let client = self
303            .read_client_or_err("futu_get_stock_filter", &req_ctx, None, None)
304            .await?;
305        Self::wrap_result(
306            handlers::reference::get_stock_filter(&client, req.market, req.begin, req.num).await,
307        )
308    }
309
310    // ------- v1.4.30 市场元数据 / 复权 / 自选 -------
311
312    #[tool(
313        description = "Trading days for a market in a date range. Python SDK: OpenQuoteContext.request_trading_days. Note: returns natural-day-minus-weekends-and-holidays, excluding temporary market closures."
314    )]
315    async fn futu_get_trading_days(
316        &self,
317        Parameters(req): Parameters<TradingDaysReq>,
318        req_ctx: RequestContext<RoleServer>,
319    ) -> std::result::Result<String, String> {
320        tracing::info!(
321            tool = "futu_get_trading_days",
322            market = req.market,
323            begin = %req.begin_time,
324            end = %req.end_time
325        );
326        let client = self
327            .read_client_or_err("futu_get_trading_days", &req_ctx, None, None)
328            .await?;
329        Self::wrap_result(
330            handlers::reference::get_trading_days(
331                &client,
332                req.market,
333                &req.begin_time,
334                &req.end_time,
335            )
336            .await,
337        )
338    }
339
340    #[tool(
341        description = "Rehab (dividend / split / bonus) events and adjustment factors. Required for long-term K-line alignment. Python SDK: OpenQuoteContext.get_rehab."
342    )]
343    async fn futu_get_rehab(
344        &self,
345        Parameters(req): Parameters<SymbolReq>,
346        req_ctx: RequestContext<RoleServer>,
347    ) -> std::result::Result<String, String> {
348        tracing::info!(tool = "futu_get_rehab", symbol = %req.symbol);
349        let client = self
350            .read_client_or_err("futu_get_rehab", &req_ctx, None, None)
351            .await?;
352        Self::wrap_result(handlers::reference::get_rehab(&client, &req.symbol).await)
353    }
354
355    #[tool(
356        description = "Suspend (trading halt) days for securities in a date range. Python SDK: OpenQuoteContext.get_suspend."
357    )]
358    async fn futu_get_suspend(
359        &self,
360        Parameters(req): Parameters<SuspendReq>,
361        req_ctx: RequestContext<RoleServer>,
362    ) -> std::result::Result<String, String> {
363        tracing::info!(
364            tool = "futu_get_suspend",
365            count = req.symbols.len(),
366            begin = %req.begin_time,
367            end = %req.end_time
368        );
369        let client = self
370            .read_client_or_err("futu_get_suspend", &req_ctx, None, None)
371            .await?;
372        Self::wrap_result(
373            handlers::reference::get_suspend(&client, &req.symbols, &req.begin_time, &req.end_time)
374                .await,
375        )
376    }
377
378    #[tool(
379        description = "List securities in a user watchlist group. Python SDK: OpenQuoteContext.get_user_security. Use futu_get_user_security_group to find available group names."
380    )]
381    async fn futu_get_user_security(
382        &self,
383        Parameters(req): Parameters<UserSecurityReq>,
384        req_ctx: RequestContext<RoleServer>,
385    ) -> std::result::Result<String, String> {
386        tracing::info!(tool = "futu_get_user_security", group = %req.group_name);
387        let client = self
388            .read_client_or_err("futu_get_user_security", &req_ctx, None, None)
389            .await?;
390        Self::wrap_result(handlers::reference::get_user_security(&client, &req.group_name).await)
391    }
392
393    // ------- v1.4.30 系统元数据 -------
394
395    #[tool(
396        description = "Get gateway global state: per-market trading status, server version / time, quote & trade login status. Python SDK: OpenContext.get_global_state."
397    )]
398    async fn futu_get_global_state(
399        &self,
400        Parameters(_req): Parameters<NoArgs>,
401        req_ctx: RequestContext<RoleServer>,
402    ) -> std::result::Result<String, String> {
403        tracing::info!(tool = "futu_get_global_state");
404        let client = self
405            .read_client_or_err("futu_get_global_state", &req_ctx, None, None)
406            .await?;
407        Self::wrap_result(handlers::core::get_global_state(&client).await)
408    }
409
410    #[tool(
411        description = "Get user info: nickname, per-market quote permissions, subscribe quota, history-K quota. Python SDK: OpenContext.get_user_info."
412    )]
413    async fn futu_get_user_info(
414        &self,
415        Parameters(_req): Parameters<NoArgs>,
416        req_ctx: RequestContext<RoleServer>,
417    ) -> std::result::Result<String, String> {
418        tracing::info!(tool = "futu_get_user_info");
419        let client = self
420            .read_client_or_err("futu_get_user_info", &req_ctx, None, None)
421            .await?;
422        Self::wrap_result(handlers::core::get_user_info(&client).await)
423    }
424
425    #[tool(
426        description = "Get quote-rights profile grouped like Futu OpenD GUI: HK/US/CN/SG/JP/crypto permissions, raw values, labels and quota. Set refresh=true to trigger request_highest_quote_right first."
427    )]
428    async fn futu_get_quote_rights(
429        &self,
430        Parameters(req): Parameters<QuoteRightsReq>,
431        req_ctx: RequestContext<RoleServer>,
432    ) -> std::result::Result<String, String> {
433        tracing::info!(
434            tool = "futu_get_quote_rights",
435            refresh = req.refresh.unwrap_or(false)
436        );
437        let client = self
438            .read_client_or_err("futu_get_quote_rights", &req_ctx, None, None)
439            .await?;
440        Self::wrap_result(
441            handlers::core::get_quote_rights(&client, req.refresh.unwrap_or(false)).await,
442        )
443    }
444
445    #[tool(
446        description = "Get delay-statistics summary: counts of quote-push / request-reply / place-order samples. Python SDK: OpenContext.get_delay_statistics. For raw per-segment buckets use REST /api/delay-statistics."
447    )]
448    async fn futu_get_delay_statistics(
449        &self,
450        Parameters(_req): Parameters<NoArgs>,
451        req_ctx: RequestContext<RoleServer>,
452    ) -> std::result::Result<String, String> {
453        tracing::info!(tool = "futu_get_delay_statistics");
454        let client = self
455            .read_client_or_err("futu_get_delay_statistics", &req_ctx, None, None)
456            .await?;
457        Self::wrap_result(handlers::core::get_delay_statistics(&client).await)
458    }
459
460    #[tool(
461        description = "Query Futu Token / moomoo Token enable + bind state. Returns 4 fields: \
462            nn_token_enable, nn_token_bind, mm_token_enable, mm_token_bind \
463            (1=enabled/bound, 0=disabled/unbound). \
464            Use case: when /api/unlock-trade fails with -20011 (\"please enable Futu Token\"), \
465            call this tool first to diagnose which side is missing token binding."
466    )]
467    async fn futu_get_token_state(
468        &self,
469        Parameters(_req): Parameters<NoArgs>,
470        req_ctx: RequestContext<RoleServer>,
471    ) -> std::result::Result<String, String> {
472        tracing::info!(tool = "futu_get_token_state");
473        let client = self
474            .read_client_or_err("futu_get_token_state", &req_ctx, None, None)
475            .await?;
476        // app_id default "all" (查 NN+MM 两边); caller 没传 = None → daemon 内部 default.
477        Self::wrap_result(handlers::core::get_token_state(&client, None).await)
478    }
479
480    #[tool(
481        description = "Risk-free rate for HK / US / JP markets (option pricing baseline, \
482            e.g. Black-Scholes). Returns rate as percent (e.g. 4.5) plus raw uint64 \
483            (×10^9). Useful for pricing options or computing implied volatility / cost of carry."
484    )]
485    async fn futu_get_risk_free_rate(
486        &self,
487        Parameters(_req): Parameters<NoArgs>,
488        req_ctx: RequestContext<RoleServer>,
489    ) -> std::result::Result<String, String> {
490        tracing::info!(tool = "futu_get_risk_free_rate");
491        let client = self
492            .read_client_or_err("futu_get_risk_free_rate", &req_ctx, None, None)
493            .await?;
494        Self::wrap_result(handlers::core::get_risk_free_rate(&client).await)
495    }
496
497    #[tool(
498        description = "Get full spread tables (price tick rules per market). Returns \
499            spread_table_list with spread_code + price intervals (price_from / price_to / \
500            value, in actual decimals). Useful for client-side price validation before \
501            PlaceOrder / ModifyOrder."
502    )]
503    async fn futu_get_spread_table(
504        &self,
505        Parameters(_req): Parameters<NoArgs>,
506        req_ctx: RequestContext<RoleServer>,
507    ) -> std::result::Result<String, String> {
508        tracing::info!(tool = "futu_get_spread_table");
509        let client = self
510            .read_client_or_err("futu_get_spread_table", &req_ctx, None, None)
511            .await?;
512        Self::wrap_result(handlers::core::get_spread_table(&client).await)
513    }
514
515    #[tool(description = "Per-stock ticker statistic \
516            (avg_price / volume / buy_volume / sell_volume / neutral_volume / trade_num). \
517            Symbol format: 'HK.00700' / 'US.AAPL'. Pre-condition: must \
518            subscribe / get_static_info first to populate stock_id in static_cache. \
519            ticker_type: 0=ALL, 1=BUY, 2=SELL, 3=BUY_AND_SELL, 4=NEUTRAL. \
520            stat_type: 0=ALL, 1=BEFORE, 2=TRADING, 3=AFTER (market session).")]
521    async fn futu_get_ticker_statistic(
522        &self,
523        Parameters(req): Parameters<TickerStatisticReq>,
524        req_ctx: RequestContext<RoleServer>,
525    ) -> std::result::Result<String, String> {
526        tracing::info!(tool = "futu_get_ticker_statistic", symbol = %req.symbol);
527        let client = self
528            .read_client_or_err("futu_get_ticker_statistic", &req_ctx, None, None)
529            .await?;
530        Self::wrap_result(
531            handlers::core::get_ticker_statistic(
532                &client,
533                &req.symbol,
534                req.ticker_type,
535                req.stat_type,
536            )
537            .await,
538        )
539    }
540
541    #[tool(
542        description = "Per-stock ticker statistic detail (price-level distribution). \
543            Companion of futu_get_ticker_statistic. Typical flow: \
544            (1) call futu_get_ticker_statistic to get ticker_time + summary stats, \
545            (2) call this tool with same ticker_time to get DetailItem list \
546            (price / buy_volume / sell_volume / volume / ratio / neutral_volume per price level). \
547            Symbol format: 'HK.00700' / 'US.AAPL'. Pre-condition: must subscribe / get_static_info \
548            first to populate stock_id in static_cache. \
549            ticker_type: 0=ALL, 1=BUY, 2=SELL, 3=BUY_AND_SELL, 4=NEUTRAL. \
550            stat_type: 0=ALL, 1=BEFORE, 2=TRADING, 3=AFTER. \
551            select_num: 0=all levels, 1..N=top N (backend max ~100). \
552            data_from / data_max_count: pagination."
553    )]
554    async fn futu_get_ticker_statistic_detail(
555        &self,
556        Parameters(req): Parameters<TickerStatisticDetailReq>,
557        req_ctx: RequestContext<RoleServer>,
558    ) -> std::result::Result<String, String> {
559        tracing::info!(tool = "futu_get_ticker_statistic_detail", symbol = %req.symbol);
560        let client = self
561            .read_client_or_err("futu_get_ticker_statistic_detail", &req_ctx, None, None)
562            .await?;
563        Self::wrap_result(
564            handlers::core::get_ticker_statistic_detail(
565                &client,
566                handlers::core::TickerStatisticDetailInput {
567                    symbol: &req.symbol,
568                    ticker_type: req.ticker_type,
569                    ticker_time: req.ticker_time,
570                    select_num: req.select_num,
571                    data_from: req.data_from,
572                    data_max_count: req.data_max_count,
573                    stat_type: req.stat_type,
574                },
575            )
576            .await,
577        )
578    }
579
580    // ------- v1.4.30 交易扩展 -------
581
582    // ------- v1.4.30 P2 完成品(100% 覆盖) -------
583
584    #[tool(
585        description = "Historical K-line download quota (used / remain). Python SDK: OpenQuoteContext.get_history_kl_quota."
586    )]
587    async fn futu_get_history_kl_quota(
588        &self,
589        Parameters(req): Parameters<HistoryKlQuotaReq>,
590        req_ctx: RequestContext<RoleServer>,
591    ) -> std::result::Result<String, String> {
592        tracing::info!(tool = "futu_get_history_kl_quota", detail = req.get_detail);
593        let client = self
594            .read_client_or_err("futu_get_history_kl_quota", &req_ctx, None, None)
595            .await?;
596        Self::wrap_result(handlers::reference::get_history_kl_quota(&client, req.get_detail).await)
597    }
598
599    #[tool(
600        description = "Top-holder share change list (institution / fund / executive). Python SDK: OpenQuoteContext.get_holding_change_list."
601    )]
602    async fn futu_get_holding_change(
603        &self,
604        Parameters(req): Parameters<HoldingChangeReq>,
605        req_ctx: RequestContext<RoleServer>,
606    ) -> std::result::Result<String, String> {
607        tracing::info!(
608            tool = "futu_get_holding_change",
609            symbol = %req.symbol,
610            category = req.holder_category
611        );
612        let client = self
613            .read_client_or_err("futu_get_holding_change", &req_ctx, None, None)
614            .await?;
615        Self::wrap_result(
616            handlers::reference::get_holding_change(
617                &client,
618                &req.symbol,
619                req.holder_category,
620                req.begin_time.as_deref(),
621                req.end_time.as_deref(),
622            )
623            .await,
624        )
625    }
626
627    #[tool(
628        description = "Modify watchlist group — add / delete / move-out stocks. `op` is an INTEGER (not a string literal): 1=AddInto, 2=Delete-from-group, 3=MoveOut. Python SDK: OpenQuoteContext.modify_user_security."
629    )]
630    async fn futu_modify_user_security(
631        &self,
632        Parameters(req): Parameters<ModifyUserSecurityReq>,
633        req_ctx: RequestContext<RoleServer>,
634    ) -> std::result::Result<String, String> {
635        // v1.4.104 codex round 1 F2 (P1) fix: 改 caller-specific scope check
636        // (之前 require_tool_scope 只看 startup key, narrow Bearer 仍可用
637        // startup key 的 qot:read scope 修改 watchlist 全局状态).
638        // require_acc_read_with_acc_id 内部用 scope_for_tool(tool) 拿 needed_scope
639        // (此 tool 是 ToolScope::Read(QotRead)), 走 caller-specific 路径.
640        tracing::info!(
641            tool = "futu_modify_user_security",
642            group = %req.group_name,
643            op = req.op,
644            count = req.symbols.len()
645        );
646        let client = self
647            .read_client_or_err("futu_modify_user_security", &req_ctx, None, None)
648            .await?;
649        Self::wrap_result(
650            handlers::reference::modify_user_security(
651                &client,
652                &req.group_name,
653                req.op,
654                &req.symbols,
655            )
656            .await,
657        )
658    }
659
660    #[tool(
661        description = "Code change / temporary-ticker info (currently HK market only). Python SDK: OpenQuoteContext.get_code_change."
662    )]
663    async fn futu_get_code_change(
664        &self,
665        Parameters(req): Parameters<CodeChangeReq>,
666        req_ctx: RequestContext<RoleServer>,
667    ) -> std::result::Result<String, String> {
668        tracing::info!(tool = "futu_get_code_change", count = req.symbols.len());
669        let client = self
670            .read_client_or_err("futu_get_code_change", &req_ctx, None, None)
671            .await?;
672        Self::wrap_result(handlers::reference::get_code_change(&client, &req.symbols).await)
673    }
674
675    #[tool(description = "Set price reminder. `op` accepts integer code 1-6 \
676                       (1=Add, 2=Del, 3=Enable, 4=Disable, 5=Modify, 6=DeleteAll); \
677                       MCP also accepts string aliases Add/Del/Enable/Disable/Modify/DeleteAll \
678                       (and legacy SetAdd/SetDel/SetEnable/SetDisable/DelAll). \
679                       Add (op=1) requires reminder_type + freq + value. \
680                       Modify (op=5) requires key (other fields preserved when omitted). \
681                       Python SDK: OpenQuoteContext.set_price_reminder.")]
682    async fn futu_set_price_reminder(
683        &self,
684        Parameters(req): Parameters<SetPriceReminderReq>,
685        req_ctx: RequestContext<RoleServer>,
686    ) -> std::result::Result<String, String> {
687        // v1.4.104 codex round 1 F2 (P1) fix: 改 caller-specific scope check
688        // (之前 require_tool_scope 只看 startup key, HTTP Bearer 调用方权限
689        // 没真核, narrow Bearer 仍能修改 / 删除全局 price reminder 状态).
690        // v1.4.84 §5 B4: op-conditional required field check
691        req.validate()?;
692        tracing::info!(
693            tool = "futu_set_price_reminder",
694            symbol = %req.symbol,
695            op = req.op
696        );
697        let client = self
698            .read_client_or_err("futu_set_price_reminder", &req_ctx, None, None)
699            .await?;
700        Self::wrap_result(
701            handlers::reference::set_price_reminder(
702                &client,
703                handlers::reference::SetPriceReminderInput {
704                    symbol: &req.symbol,
705                    op: req.op,
706                    key: req.key,
707                    reminder_type: req.reminder_type,
708                    freq: req.freq,
709                    value: req.value,
710                    note: req.note.as_deref(),
711                    reminder_session_list: &req.reminder_session_list,
712                },
713            )
714            .await,
715        )
716    }
717
718    #[tool(
719        description = "Query price reminders (by symbol or market). Python SDK: OpenQuoteContext.get_price_reminder."
720    )]
721    async fn futu_get_price_reminder(
722        &self,
723        Parameters(req): Parameters<GetPriceReminderReq>,
724        req_ctx: RequestContext<RoleServer>,
725    ) -> std::result::Result<String, String> {
726        // v1.4.84 §5 B4: symbol XOR market required
727        req.validate()?;
728        tracing::info!(
729            tool = "futu_get_price_reminder",
730            // v1.4.90 P2-C: Option<String> / Option<i32> 都 flatten,避免 record_debug 编 string
731            symbol = %crate::state::audit_fmt::opt_str(req.symbol.as_deref()),
732            market = crate::state::audit_fmt::opt_i32(req.market)
733        );
734        let client = self
735            .read_client_or_err("futu_get_price_reminder", &req_ctx, None, None)
736            .await?;
737        Self::wrap_result(
738            handlers::reference::get_price_reminder(&client, req.symbol.as_deref(), req.market)
739                .await,
740        )
741    }
742
743    #[tool(
744        description = "Option expiration-date list for an underlying (HSI / HSCEI or HK/US equity). Python SDK: OpenQuoteContext.get_option_expiration_date."
745    )]
746    async fn futu_get_option_expiration_date(
747        &self,
748        Parameters(req): Parameters<OptionExpirationDateReq>,
749        req_ctx: RequestContext<RoleServer>,
750    ) -> std::result::Result<String, String> {
751        tracing::info!(
752            tool = "futu_get_option_expiration_date",
753            owner = %req.owner_symbol
754        );
755        let client = self
756            .read_client_or_err("futu_get_option_expiration_date", &req_ctx, None, None)
757            .await?;
758        Self::wrap_result(
759            handlers::reference::get_option_expiration_date(
760                &client,
761                &req.owner_symbol,
762                req.index_option_type,
763            )
764            .await,
765        )
766    }
767}