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