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