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::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
6
7use super::FutuServer;
8
9mod option_chain;
10use option_chain::option_chain_data_filter;
11
12impl FutuServer {
13    // ===== v1.4.25: 行情分析 =====
14
15    async fn futu_get_capital_flow_impl(
16        &self,
17        Parameters(req): Parameters<CapitalFlowReq>,
18        req_ctx: RequestContext<RoleServer>,
19    ) -> std::result::Result<String, String> {
20        tracing::info!(tool = "futu_get_capital_flow", symbol = %req.symbol);
21        let client = self
22            .read_client_or_err("futu_get_capital_flow", &req_ctx, None, None)
23            .await?;
24        Self::wrap_result(
25            handlers::analysis::get_capital_flow(
26                &client,
27                &req.symbol,
28                req.period_type,
29                req.begin_time,
30                req.end_time,
31            )
32            .await,
33        )
34    }
35
36    async fn futu_get_capital_distribution_impl(
37        &self,
38        Parameters(req): Parameters<SymbolReq>,
39        req_ctx: RequestContext<RoleServer>,
40    ) -> std::result::Result<String, String> {
41        tracing::info!(tool = "futu_get_capital_distribution", symbol = %req.symbol);
42        let client = self
43            .read_client_or_err("futu_get_capital_distribution", &req_ctx, None, None)
44            .await?;
45        Self::wrap_result(handlers::analysis::get_capital_distribution(&client, &req.symbol).await)
46    }
47
48    async fn futu_get_company_profile_impl(
49        &self,
50        Parameters(req): Parameters<SymbolReq>,
51        req_ctx: RequestContext<RoleServer>,
52    ) -> std::result::Result<String, String> {
53        tracing::info!(tool = "futu_get_company_profile", symbol = %req.symbol);
54        let client = self
55            .read_client_or_err("futu_get_company_profile", &req_ctx, None, None)
56            .await?;
57        Self::wrap_result(handlers::reference::get_company_profile(&client, &req.symbol).await)
58    }
59
60    async fn futu_get_company_executives_impl(
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_executives", symbol = %req.symbol);
66        let client = self
67            .read_client_or_err("futu_get_company_executives", &req_ctx, None, None)
68            .await?;
69        Self::wrap_result(handlers::reference::get_company_executives(&client, &req.symbol).await)
70    }
71
72    async fn futu_get_company_executive_background_impl(
73        &self,
74        Parameters(req): Parameters<CompanyExecutiveBackgroundReq>,
75        req_ctx: RequestContext<RoleServer>,
76    ) -> std::result::Result<String, String> {
77        tracing::info!(
78            tool = "futu_get_company_executive_background",
79            symbol = %req.symbol,
80            leader_name = %req.leader_name
81        );
82        let client = self
83            .read_client_or_err(
84                "futu_get_company_executive_background",
85                &req_ctx,
86                None,
87                None,
88            )
89            .await?;
90        Self::wrap_result(
91            handlers::reference::get_company_executive_background(
92                &client,
93                &req.symbol,
94                &req.leader_name,
95            )
96            .await,
97        )
98    }
99
100    async fn futu_get_market_state_impl(
101        &self,
102        Parameters(req): Parameters<MarketStateReq>,
103        req_ctx: RequestContext<RoleServer>,
104    ) -> std::result::Result<String, String> {
105        tracing::info!(tool = "futu_get_market_state", count = req.symbols.len());
106        let client = self
107            .read_client_or_err("futu_get_market_state", &req_ctx, None, None)
108            .await?;
109        Self::wrap_result(handlers::analysis::get_market_state(&client, &req.symbols).await)
110    }
111
112    // ===== v1.4.26 新增:history_kline / owner_plate / reference / option_chain =====
113
114    async fn futu_get_history_kline_impl(
115        &self,
116        Parameters(req): Parameters<HistoryKLineReq>,
117        req_ctx: RequestContext<RoleServer>,
118    ) -> std::result::Result<String, String> {
119        let max_count = req.validated_max_count()?;
120        let session = req.validated_session()?;
121        let next_req_key = req.decoded_next_req_key()?;
122        tracing::info!(
123            tool = "futu_get_history_kline",
124            symbol = %req.symbol,
125            kl_type = %req.kl_type,
126            rehab = %req.rehab_type,
127            extended_time = ?req.extended_time,
128            session = ?session,
129            has_next_req_key = next_req_key.is_some(),
130        );
131        let client = self
132            .read_client_or_err("futu_get_history_kline", &req_ctx, None, None)
133            .await?;
134        Self::wrap_result(
135            handlers::analysis::get_history_kline(
136                &client,
137                &req.symbol,
138                &req.kl_type,
139                &req.rehab_type,
140                &req.begin,
141                &req.end,
142                max_count,
143                req.need_kl_fields_flag,
144                req.extended_time,
145                session,
146                next_req_key.as_deref(),
147            )
148            .await,
149        )
150    }
151
152    async fn futu_get_owner_plate_impl(
153        &self,
154        Parameters(req): Parameters<SymbolListReq>,
155        req_ctx: RequestContext<RoleServer>,
156    ) -> std::result::Result<String, String> {
157        tracing::info!(tool = "futu_get_owner_plate", count = req.symbols.len());
158        let client = self
159            .read_client_or_err("futu_get_owner_plate", &req_ctx, None, None)
160            .await?;
161        Self::wrap_result(handlers::analysis::get_owner_plate(&client, &req.symbols).await)
162    }
163
164    async fn futu_get_reference_impl(
165        &self,
166        Parameters(req): Parameters<ReferenceReq>,
167        req_ctx: RequestContext<RoleServer>,
168    ) -> std::result::Result<String, String> {
169        tracing::info!(
170            tool = "futu_get_reference",
171            symbol = %req.symbol,
172            reference_type = %req.reference_type
173        );
174        let client = self
175            .read_client_or_err("futu_get_reference", &req_ctx, None, None)
176            .await?;
177        Self::wrap_result(
178            handlers::analysis::get_reference(&client, &req.symbol, &req.reference_type).await,
179        )
180    }
181
182    async fn futu_get_option_chain_impl(
183        &self,
184        Parameters(req): Parameters<OptionChainReq>,
185        req_ctx: RequestContext<RoleServer>,
186    ) -> std::result::Result<String, String> {
187        req.validate()?;
188        tracing::info!(
189            tool = "futu_get_option_chain",
190            owner = %req.owner_symbol,
191            begin = %req.begin_time,
192            end = %req.end_time
193        );
194        let client = self
195            .read_client_or_err("futu_get_option_chain", &req_ctx, None, None)
196            .await?;
197        // v1.4.38 Phase 3: 把 MCP 侧的 Greek filter 字段组装成 proto DataFilter。
198        let data_filter = option_chain_data_filter(&req);
199        Self::wrap_result(
200            handlers::analysis::get_option_chain(
201                &client,
202                handlers::analysis::OptionChainInput {
203                    owner_symbol: &req.owner_symbol,
204                    begin_time: &req.begin_time,
205                    end_time: &req.end_time,
206                    option_type_str: req.option_type.as_deref(),
207                    data_filter,
208                },
209            )
210            .await,
211        )
212    }
213
214    // ------- v1.4.29 参考数据 / 衍生证券 -------
215
216    async fn futu_get_warrant_impl(
217        &self,
218        Parameters(req): Parameters<WarrantReq>,
219        req_ctx: RequestContext<RoleServer>,
220    ) -> std::result::Result<String, String> {
221        req.validate()?;
222        tracing::info!(
223            tool = "futu_get_warrant",
224            // v1.4.90 P2-C: Option<String> → &str ("" 哨兵)
225            owner = %crate::state::audit_fmt::opt_str(req.owner_symbol.as_deref()),
226            begin = req.begin,
227            num = req.num
228        );
229        let client = self
230            .read_client_or_err("futu_get_warrant", &req_ctx, None, None)
231            .await?;
232        Self::wrap_result(
233            handlers::reference::get_warrant(
234                &client,
235                req.owner_symbol.as_deref(),
236                req.begin,
237                req.num,
238            )
239            .await,
240        )
241    }
242
243    async fn futu_get_ipo_list_impl(
244        &self,
245        Parameters(req): Parameters<IpoListReq>,
246        req_ctx: RequestContext<RoleServer>,
247    ) -> std::result::Result<String, String> {
248        req.validate()?;
249        tracing::info!(tool = "futu_get_ipo_list", market = req.market);
250        let client = self
251            .read_client_or_err("futu_get_ipo_list", &req_ctx, None, None)
252            .await?;
253        Self::wrap_result(handlers::reference::get_ipo_list(&client, req.market).await)
254    }
255
256    async fn futu_get_ipo_calendar_impl(
257        &self,
258        Parameters(req): Parameters<IpoCalendarReq>,
259        req_ctx: RequestContext<RoleServer>,
260    ) -> std::result::Result<String, String> {
261        req.validate()?;
262        tracing::info!(
263            tool = "futu_get_ipo_calendar",
264            market = req.market,
265            event_types = ?req.event_types,
266            begin_date = ?req.begin_date,
267            end_date = ?req.end_date
268        );
269        let client = self
270            .read_client_or_err("futu_get_ipo_calendar", &req_ctx, None, None)
271            .await?;
272        Self::wrap_result(
273            handlers::reference::get_ipo_calendar(
274                &client,
275                req.market,
276                &req.event_types,
277                req.begin_date.as_deref(),
278                req.end_date.as_deref(),
279            )
280            .await,
281        )
282    }
283
284    async fn futu_get_future_info_impl(
285        &self,
286        Parameters(req): Parameters<FutureInfoReq>,
287        req_ctx: RequestContext<RoleServer>,
288    ) -> std::result::Result<String, String> {
289        tracing::info!(tool = "futu_get_future_info", count = req.symbols.len());
290        let client = self
291            .read_client_or_err("futu_get_future_info", &req_ctx, None, None)
292            .await?;
293        Self::wrap_result(handlers::reference::get_future_info(&client, &req.symbols).await)
294    }
295
296    async fn futu_get_user_security_group_impl(
297        &self,
298        Parameters(req): Parameters<UserSecurityGroupReq>,
299        req_ctx: RequestContext<RoleServer>,
300    ) -> std::result::Result<String, String> {
301        req.validate()?;
302        tracing::info!(
303            tool = "futu_get_user_security_group",
304            group_type = req.group_type
305        );
306        let client = self
307            .read_client_or_err("futu_get_user_security_group", &req_ctx, None, None)
308            .await?;
309        Self::wrap_result(
310            handlers::reference::get_user_security_group(&client, req.group_type).await,
311        )
312    }
313
314    async fn futu_get_stock_filter_impl(
315        &self,
316        Parameters(req): Parameters<StockFilterReq>,
317        req_ctx: RequestContext<RoleServer>,
318    ) -> std::result::Result<String, String> {
319        req.validate()?;
320        tracing::info!(
321            tool = "futu_get_stock_filter",
322            market = req.market,
323            begin = req.begin,
324            num = req.num
325        );
326        let client = self
327            .read_client_or_err("futu_get_stock_filter", &req_ctx, None, None)
328            .await?;
329        Self::wrap_result(
330            handlers::reference::get_stock_filter(&client, req.market, req.begin, req.num).await,
331        )
332    }
333
334    // ------- v1.4.30 市场元数据 / 复权 / 自选 -------
335
336    async fn futu_get_trading_days_impl(
337        &self,
338        Parameters(req): Parameters<TradingDaysReq>,
339        req_ctx: RequestContext<RoleServer>,
340    ) -> std::result::Result<String, String> {
341        req.validate()?;
342        tracing::info!(
343            tool = "futu_get_trading_days",
344            market = req.market,
345            begin = %req.begin_time,
346            end = %req.end_time
347        );
348        let client = self
349            .read_client_or_err("futu_get_trading_days", &req_ctx, None, None)
350            .await?;
351        Self::wrap_result(
352            handlers::reference::get_trading_days(
353                &client,
354                req.market,
355                &req.begin_time,
356                &req.end_time,
357            )
358            .await,
359        )
360    }
361
362    async fn futu_get_rehab_impl(
363        &self,
364        Parameters(req): Parameters<SymbolReq>,
365        req_ctx: RequestContext<RoleServer>,
366    ) -> std::result::Result<String, String> {
367        tracing::info!(tool = "futu_get_rehab", symbol = %req.symbol);
368        let client = self
369            .read_client_or_err("futu_get_rehab", &req_ctx, None, None)
370            .await?;
371        Self::wrap_result(handlers::reference::get_rehab(&client, &req.symbol).await)
372    }
373
374    async fn futu_get_suspend_impl(
375        &self,
376        Parameters(req): Parameters<SuspendReq>,
377        req_ctx: RequestContext<RoleServer>,
378    ) -> std::result::Result<String, String> {
379        tracing::info!(
380            tool = "futu_get_suspend",
381            count = req.symbols.len(),
382            begin = %req.begin_time,
383            end = %req.end_time
384        );
385        let client = self
386            .read_client_or_err("futu_get_suspend", &req_ctx, None, None)
387            .await?;
388        Self::wrap_result(
389            handlers::reference::get_suspend(&client, &req.symbols, &req.begin_time, &req.end_time)
390                .await,
391        )
392    }
393
394    async fn futu_get_user_security_impl(
395        &self,
396        Parameters(req): Parameters<UserSecurityReq>,
397        req_ctx: RequestContext<RoleServer>,
398    ) -> std::result::Result<String, String> {
399        tracing::info!(tool = "futu_get_user_security", group = %req.group_name);
400        let client = self
401            .read_client_or_err("futu_get_user_security", &req_ctx, None, None)
402            .await?;
403        Self::wrap_result(handlers::reference::get_user_security(&client, &req.group_name).await)
404    }
405
406    // ------- v1.4.30 系统元数据 -------
407
408    async fn futu_get_global_state_impl(
409        &self,
410        Parameters(_req): Parameters<NoArgs>,
411        req_ctx: RequestContext<RoleServer>,
412    ) -> std::result::Result<String, String> {
413        tracing::info!(tool = "futu_get_global_state");
414        let client = self
415            .read_client_or_err("futu_get_global_state", &req_ctx, None, None)
416            .await?;
417        Self::wrap_result(handlers::core::get_global_state(&client).await)
418    }
419
420    async fn futu_get_user_info_impl(
421        &self,
422        Parameters(_req): Parameters<NoArgs>,
423        req_ctx: RequestContext<RoleServer>,
424    ) -> std::result::Result<String, String> {
425        tracing::info!(tool = "futu_get_user_info");
426        let client = self
427            .read_client_or_err("futu_get_user_info", &req_ctx, None, None)
428            .await?;
429        Self::wrap_result(handlers::core::get_user_info(&client).await)
430    }
431
432    async fn futu_get_quote_rights_impl(
433        &self,
434        Parameters(req): Parameters<QuoteRightsReq>,
435        req_ctx: RequestContext<RoleServer>,
436    ) -> std::result::Result<String, String> {
437        tracing::info!(
438            tool = "futu_get_quote_rights",
439            refresh = req.refresh.unwrap_or(false)
440        );
441        let client = self
442            .read_client_or_err("futu_get_quote_rights", &req_ctx, None, None)
443            .await?;
444        Self::wrap_result(
445            handlers::core::get_quote_rights(&client, req.refresh.unwrap_or(false)).await,
446        )
447    }
448
449    async fn futu_get_delay_statistics_impl(
450        &self,
451        Parameters(_req): Parameters<NoArgs>,
452        req_ctx: RequestContext<RoleServer>,
453    ) -> std::result::Result<String, String> {
454        tracing::info!(tool = "futu_get_delay_statistics");
455        let client = self
456            .read_client_or_err("futu_get_delay_statistics", &req_ctx, None, None)
457            .await?;
458        Self::wrap_result(handlers::core::get_delay_statistics(&client).await)
459    }
460
461    async fn futu_get_token_state_impl(
462        &self,
463        Parameters(_req): Parameters<NoArgs>,
464        req_ctx: RequestContext<RoleServer>,
465    ) -> std::result::Result<String, String> {
466        tracing::info!(tool = "futu_get_token_state");
467        let client = self
468            .read_client_or_err("futu_get_token_state", &req_ctx, None, None)
469            .await?;
470        // app_id default "all" (查 NN+MM 两边); caller 没传 = None → daemon 内部 default.
471        Self::wrap_result(handlers::core::get_token_state(&client, None).await)
472    }
473
474    async fn futu_get_risk_free_rate_impl(
475        &self,
476        Parameters(_req): Parameters<NoArgs>,
477        req_ctx: RequestContext<RoleServer>,
478    ) -> std::result::Result<String, String> {
479        tracing::info!(tool = "futu_get_risk_free_rate");
480        let client = self
481            .read_client_or_err("futu_get_risk_free_rate", &req_ctx, None, None)
482            .await?;
483        Self::wrap_result(handlers::core::get_risk_free_rate(&client).await)
484    }
485
486    async fn futu_get_spread_table_impl(
487        &self,
488        Parameters(_req): Parameters<NoArgs>,
489        req_ctx: RequestContext<RoleServer>,
490    ) -> std::result::Result<String, String> {
491        tracing::info!(tool = "futu_get_spread_table");
492        let client = self
493            .read_client_or_err("futu_get_spread_table", &req_ctx, None, None)
494            .await?;
495        Self::wrap_result(handlers::core::get_spread_table(&client).await)
496    }
497
498    async fn futu_get_ticker_statistic_impl(
499        &self,
500        Parameters(req): Parameters<TickerStatisticReq>,
501        req_ctx: RequestContext<RoleServer>,
502    ) -> std::result::Result<String, String> {
503        tracing::info!(tool = "futu_get_ticker_statistic", symbol = %req.symbol);
504        let client = self
505            .read_client_or_err("futu_get_ticker_statistic", &req_ctx, None, None)
506            .await?;
507        Self::wrap_result(
508            handlers::core::get_ticker_statistic(
509                &client,
510                &req.symbol,
511                req.ticker_type,
512                req.stat_type,
513            )
514            .await,
515        )
516    }
517
518    async fn futu_get_ticker_statistic_detail_impl(
519        &self,
520        Parameters(req): Parameters<TickerStatisticDetailReq>,
521        req_ctx: RequestContext<RoleServer>,
522    ) -> std::result::Result<String, String> {
523        req.validate()?;
524        tracing::info!(tool = "futu_get_ticker_statistic_detail", symbol = %req.symbol);
525        let client = self
526            .read_client_or_err("futu_get_ticker_statistic_detail", &req_ctx, None, None)
527            .await?;
528        Self::wrap_result(
529            handlers::core::get_ticker_statistic_detail(
530                &client,
531                handlers::core::TickerStatisticDetailInput {
532                    symbol: &req.symbol,
533                    ticker_type: req.ticker_type,
534                    ticker_time: req.ticker_time,
535                    select_num: req.select_num,
536                    data_from: req.data_from,
537                    data_max_count: req.data_max_count,
538                    stat_type: req.stat_type,
539                },
540            )
541            .await,
542        )
543    }
544
545    // ------- v1.4.30 交易扩展 -------
546
547    // ------- v1.4.30 P2 完成品(100% 覆盖) -------
548
549    async fn futu_get_history_kl_quota_impl(
550        &self,
551        Parameters(req): Parameters<HistoryKlQuotaReq>,
552        req_ctx: RequestContext<RoleServer>,
553    ) -> std::result::Result<String, String> {
554        tracing::info!(tool = "futu_get_history_kl_quota", detail = req.get_detail);
555        let client = self
556            .read_client_or_err("futu_get_history_kl_quota", &req_ctx, None, None)
557            .await?;
558        Self::wrap_result(handlers::reference::get_history_kl_quota(&client, req.get_detail).await)
559    }
560
561    async fn futu_get_holding_change_impl(
562        &self,
563        Parameters(req): Parameters<HoldingChangeReq>,
564        req_ctx: RequestContext<RoleServer>,
565    ) -> std::result::Result<String, String> {
566        tracing::info!(
567            tool = "futu_get_holding_change",
568            symbol = %req.symbol,
569            category = req.holder_category
570        );
571        let client = self
572            .read_client_or_err("futu_get_holding_change", &req_ctx, None, None)
573            .await?;
574        Self::wrap_result(
575            handlers::reference::get_holding_change(
576                &client,
577                &req.symbol,
578                req.holder_category,
579                req.begin_time.as_deref(),
580                req.end_time.as_deref(),
581            )
582            .await,
583        )
584    }
585
586    async fn futu_modify_user_security_impl(
587        &self,
588        Parameters(req): Parameters<ModifyUserSecurityReq>,
589        req_ctx: RequestContext<RoleServer>,
590    ) -> std::result::Result<String, String> {
591        req.validate()?;
592        // v1.4.104 codex round 1 F2 (P1) fix: 改 caller-specific scope check
593        // (之前 require_tool_scope 只看 startup key, narrow Bearer 仍可用
594        // startup key 的 qot:read scope 修改 watchlist 全局状态).
595        // require_acc_read_with_acc_id 内部用 scope_for_tool(tool) 拿 needed_scope
596        // (此 tool 是 ToolScope::Read(QotRead)), 走 caller-specific 路径.
597        tracing::info!(
598            tool = "futu_modify_user_security",
599            group = %req.group_name,
600            op = req.op,
601            count = req.symbols.len()
602        );
603        let client = self
604            .read_client_or_err("futu_modify_user_security", &req_ctx, None, None)
605            .await?;
606        Self::wrap_result(
607            handlers::reference::modify_user_security(
608                &client,
609                &req.group_name,
610                req.op,
611                &req.symbols,
612            )
613            .await,
614        )
615    }
616
617    async fn futu_get_code_change_impl(
618        &self,
619        Parameters(req): Parameters<CodeChangeReq>,
620        req_ctx: RequestContext<RoleServer>,
621    ) -> std::result::Result<String, String> {
622        tracing::info!(tool = "futu_get_code_change", count = req.symbols.len());
623        let client = self
624            .read_client_or_err("futu_get_code_change", &req_ctx, None, None)
625            .await?;
626        Self::wrap_result(handlers::reference::get_code_change(&client, &req.symbols).await)
627    }
628
629    async fn futu_get_option_volatility_impl(
630        &self,
631        Parameters(req): Parameters<OptionVolatilityReq>,
632        req_ctx: RequestContext<RoleServer>,
633    ) -> std::result::Result<String, String> {
634        req.validate()?;
635        tracing::info!(
636            tool = "futu_get_option_volatility",
637            symbol = %req.symbol,
638            query_time_period = ?req.query_time_period,
639            hv_time_period = ?req.hv_time_period
640        );
641        let client = self
642            .read_client_or_err("futu_get_option_volatility", &req_ctx, None, None)
643            .await?;
644        Self::wrap_result(
645            handlers::reference::get_option_volatility(
646                &client,
647                &req.symbol,
648                req.query_time_period,
649                req.hv_time_period,
650            )
651            .await,
652        )
653    }
654
655    async fn futu_get_option_exercise_probability_impl(
656        &self,
657        Parameters(req): Parameters<OptionExerciseProbabilityReq>,
658        req_ctx: RequestContext<RoleServer>,
659    ) -> std::result::Result<String, String> {
660        tracing::info!(tool = "futu_get_option_exercise_probability", symbol = %req.symbol);
661        let client = self
662            .read_client_or_err("futu_get_option_exercise_probability", &req_ctx, None, None)
663            .await?;
664        Self::wrap_result(
665            handlers::reference::get_option_exercise_probability(&client, &req.symbol).await,
666        )
667    }
668}
669
670include!(concat!(
671    env!("OUT_DIR"),
672    "/generated_mcp_routes_reference.rs"
673));