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