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 = trade_read_tool_router, vis = "pub(crate)")]
12impl FutuServer {
13 #[tool(
16 description = "List all trading accounts (real + simulate) visible to the gateway login."
17 )]
18 async fn futu_list_accounts(
19 &self,
20 Parameters(_req): Parameters<NoArgs>,
21 req_ctx: RequestContext<RoleServer>,
22 ) -> std::result::Result<String, String> {
23 let snap = self.require_acc_read_with_acc_id("futu_list_accounts", &req_ctx, None, None)?;
28 tracing::info!(tool = "futu_list_accounts");
29 let client = self.client_or_err().await?;
30 let allowed_card_nums = snap
35 .rec
36 .as_ref()
37 .and_then(|r| r.allowed_card_nums.as_deref());
38 Self::wrap_result(
39 handlers::trade::list_accounts_filtered(
40 &client,
41 snap.allowed_acc_ids.as_ref(),
42 allowed_card_nums,
43 )
44 .await,
45 )
46 }
47
48 #[tool(
49 description = "Get account funds summary (total assets, cash, market value, buying power) for a given account + market.\n\n\
50 **Cash semantics**: top-level `cash` field is \
51 backend's summary cash in the response `currency` (i.e. `union_currency` for \
52 futures/universal, primary market currency for legacy accounts). It is **NOT** \
53 the sum of `cash_info_list[].cash` across currencies (different currencies \
54 cannot be summed without FX conversion). For per-currency breakdown, read \
55 `cash_info_list`. To match Futu mobile app's '现金总值 in HKD' display, \
56 client-side compute `sum(cash_info_list[i].cash * fx_rate(currency[i], HKD))` \
57 — daemon does not perform FX aggregation."
58 )]
59 async fn futu_get_funds(
60 &self,
61 Parameters(req): Parameters<TrdAccReq>,
62 req_ctx: RequestContext<RoleServer>,
63 ) -> std::result::Result<String, String> {
64 let resolved = self
65 .resolve_read_trd_account("futu_get_funds", &req, &req_ctx)
66 .await?;
67 tracing::info!(
68 tool = "futu_get_funds",
69 market = %req.market,
70 acc_id = resolved.acc_id,
71 env = %req.env,
72 currency = ?req.currency,
73 );
74 Self::wrap_result(
76 handlers::trade::get_funds_with_currency(
77 &resolved.client,
78 &req.env,
79 resolved.acc_id,
80 &req.market,
81 req.currency.as_deref(),
82 )
83 .await,
84 )
85 }
86
87 #[tool(description = "Get current positions (holdings) for an account in a given market.")]
88 async fn futu_get_positions(
89 &self,
90 Parameters(req): Parameters<TrdAccReq>,
91 req_ctx: RequestContext<RoleServer>,
92 ) -> std::result::Result<String, String> {
93 let resolved = self
94 .resolve_read_trd_account("futu_get_positions", &req, &req_ctx)
95 .await?;
96 tracing::info!(tool = "futu_get_positions", market = %req.market, acc_id = resolved.acc_id);
97 Self::wrap_result(
98 handlers::trade::get_positions(
99 &resolved.client,
100 &req.env,
101 resolved.acc_id,
102 &req.market,
103 )
104 .await,
105 )
106 }
107
108 #[tool(
109 description = "Get today's orders (including pending / filled / cancelled) for an account in a given market."
110 )]
111 async fn futu_get_orders(
112 &self,
113 Parameters(req): Parameters<TrdAccReq>,
114 req_ctx: RequestContext<RoleServer>,
115 ) -> std::result::Result<String, String> {
116 let resolved = self
117 .resolve_read_trd_account("futu_get_orders", &req, &req_ctx)
118 .await?;
119 tracing::info!(tool = "futu_get_orders", market = %req.market, acc_id = resolved.acc_id);
120 Self::wrap_result(
121 handlers::trade::get_orders(&resolved.client, &req.env, resolved.acc_id, &req.market)
122 .await,
123 )
124 }
125
126 #[tool(description = "Get today's deals / order fills for an account in a given market.")]
127 async fn futu_get_deals(
128 &self,
129 Parameters(req): Parameters<TrdAccReq>,
130 req_ctx: RequestContext<RoleServer>,
131 ) -> std::result::Result<String, String> {
132 let resolved = self
133 .resolve_read_trd_account("futu_get_deals", &req, &req_ctx)
134 .await?;
135 tracing::info!(tool = "futu_get_deals", market = %req.market, acc_id = resolved.acc_id);
136 Self::wrap_result(
137 handlers::trade::get_deals(&resolved.client, &req.env, resolved.acc_id, &req.market)
138 .await,
139 )
140 }
141
142 #[tool(
145 description = "Max buy/sell/short/buy-back qtys before placing an order. Python SDK: OpenTradeContext.acctradinginfo_query. For NORMAL (limit) orders, price is required. order_type aligns with Trd_Common.OrderType enum (1=limit, 2=market, etc)."
146 )]
147 async fn futu_get_max_trd_qtys(
148 &self,
149 Parameters(req): Parameters<MaxTrdQtysReq>,
150 req_ctx: RequestContext<RoleServer>,
151 ) -> std::result::Result<String, String> {
152 tracing::info!(tool = "futu_get_max_trd_qtys", market = %req.market, acc_id = req.acc_id, code = %req.code);
153 let client = self
154 .read_client_or_err("futu_get_max_trd_qtys", &req_ctx, None, Some(req.acc_id))
155 .await?;
156 Self::wrap_result(
157 handlers::trade::get_max_trd_qtys(
158 &client,
159 handlers::trade::MaxTrdQtysInput {
160 env: &req.env,
161 acc_id: req.acc_id,
162 market: &req.market,
163 order_type: req.order_type,
164 code: &req.code,
165 price: req.price,
166 order_id: req.order_id,
167 },
168 )
169 .await,
170 )
171 }
172
173 #[tool(
174 description = "Query order fee breakdown (commission / platform fee / stamp duty) by order_id_ex list. Python SDK: OpenTradeContext.order_fee_query."
175 )]
176 async fn futu_get_order_fee(
177 &self,
178 Parameters(req): Parameters<OrderFeeReq>,
179 req_ctx: RequestContext<RoleServer>,
180 ) -> std::result::Result<String, String> {
181 tracing::info!(tool = "futu_get_order_fee", market = %req.market, acc_id = req.acc_id, count = req.order_id_ex_list.len());
182 let client = self
183 .read_client_or_err("futu_get_order_fee", &req_ctx, None, Some(req.acc_id))
184 .await?;
185 Self::wrap_result(
186 handlers::trade::get_order_fee(
187 &client,
188 &req.env,
189 req.acc_id,
190 &req.market,
191 &req.order_id_ex_list,
192 )
193 .await,
194 )
195 }
196
197 #[tool(
198 description = "Query margin ratio (long/short permissions, short-pool remaining, long/short initial margin ratios) by symbol list. Python SDK: OpenTradeContext.get_margin_ratio."
199 )]
200 async fn futu_get_margin_ratio(
201 &self,
202 Parameters(req): Parameters<MarginRatioReq>,
203 req_ctx: RequestContext<RoleServer>,
204 ) -> std::result::Result<String, String> {
205 tracing::info!(tool = "futu_get_margin_ratio", market = %req.market, acc_id = req.acc_id, count = req.codes.len());
206 let client = self
207 .read_client_or_err("futu_get_margin_ratio", &req_ctx, None, Some(req.acc_id))
208 .await?;
209 Self::wrap_result(
210 handlers::trade::get_margin_ratio(
211 &client,
212 &req.env,
213 req.acc_id,
214 &req.market,
215 &req.codes,
216 )
217 .await,
218 )
219 }
220
221 #[tool(
222 description = "Query historical orders (filled / cancelled) with optional time range + code filter. Python SDK: OpenTradeContext.history_order_list_query."
223 )]
224 async fn futu_get_history_orders(
225 &self,
226 Parameters(req): Parameters<HistoryQueryReq>,
227 req_ctx: RequestContext<RoleServer>,
228 ) -> std::result::Result<String, String> {
229 tracing::info!(tool = "futu_get_history_orders", market = %req.market, acc_id = req.acc_id);
230 let client = self
231 .read_client_or_err("futu_get_history_orders", &req_ctx, None, Some(req.acc_id))
232 .await?;
233 Self::wrap_result(
234 handlers::trade::get_history_orders(
235 &client,
236 handlers::trade::HistoryQueryInput {
237 env: &req.env,
238 acc_id: req.acc_id,
239 market: &req.market,
240 code_list: req.code_list,
241 begin_time: req.begin_time,
242 end_time: req.end_time,
243 },
244 )
245 .await,
246 )
247 }
248
249 #[tool(
250 description = "Query historical deals / fills with optional time range + code filter. Python SDK: OpenTradeContext.history_deal_list_query."
251 )]
252 async fn futu_get_history_deals(
253 &self,
254 Parameters(req): Parameters<HistoryQueryReq>,
255 req_ctx: RequestContext<RoleServer>,
256 ) -> std::result::Result<String, String> {
257 tracing::info!(tool = "futu_get_history_deals", market = %req.market, acc_id = req.acc_id);
258 let client = self
259 .read_client_or_err("futu_get_history_deals", &req_ctx, None, Some(req.acc_id))
260 .await?;
261 Self::wrap_result(
262 handlers::trade::get_history_deals(
263 &client,
264 handlers::trade::HistoryQueryInput {
265 env: &req.env,
266 acc_id: req.acc_id,
267 market: &req.market,
268 code_list: req.code_list,
269 begin_time: req.begin_time,
270 end_time: req.end_time,
271 },
272 )
273 .await,
274 )
275 }
276
277 #[tool(
278 description = "Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
279 )]
280 async fn futu_get_acc_cash_flow(
281 &self,
282 Parameters(req): Parameters<AccCashFlowReq>,
283 req_ctx: RequestContext<RoleServer>,
284 ) -> std::result::Result<String, String> {
285 tracing::info!(
286 tool = "futu_get_acc_cash_flow",
287 env = %req.env,
288 acc_id = req.acc_id,
289 market = %req.market,
290 date = %req.clearing_date
291 );
292 let client = self
293 .read_client_or_err("futu_get_acc_cash_flow", &req_ctx, None, Some(req.acc_id))
294 .await?;
295 Self::wrap_result(
296 handlers::trade::get_acc_cash_flow(
297 &client,
298 &req.env,
299 req.acc_id,
300 &req.market,
301 &req.clearing_date,
302 req.direction,
303 )
304 .await,
305 )
306 }
307
308 #[tool(
319 description = "Alias of futu_get_acc_cash_flow (MCP-REST naming symmetry with /api/flow-summary). Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
320 )]
321 async fn futu_get_flow_summary(
322 &self,
323 Parameters(req): Parameters<AccCashFlowReq>,
324 req_ctx: RequestContext<RoleServer>,
325 ) -> std::result::Result<String, String> {
326 tracing::info!(
328 tool = "futu_get_flow_summary",
329 alias_of = "futu_get_acc_cash_flow",
330 env = %req.env,
331 acc_id = req.acc_id,
332 market = %req.market,
333 date = %req.clearing_date
334 );
335 let client = self
336 .read_client_or_err("futu_get_flow_summary", &req_ctx, None, Some(req.acc_id))
337 .await?;
338 Self::wrap_result(
339 handlers::trade::get_acc_cash_flow(
340 &client,
341 &req.env,
342 req.acc_id,
343 &req.market,
344 &req.clearing_date,
345 req.direction,
346 )
347 .await,
348 )
349 }
350
351 #[tool(
363 description = "Fetch detailed account cash log entries (mobile-driven, richer than futu_get_acc_cash_flow). Native time range, business group / currency / keyword / symbol / direction filters, cursor-based pagination. When max_cnt is omitted the daemon uses the mobile default of 50."
364 )]
365 async fn futu_get_cash_log(
366 &self,
367 Parameters(req): Parameters<CashLogReq>,
368 req_ctx: RequestContext<RoleServer>,
369 ) -> std::result::Result<String, String> {
370 tracing::info!(
371 tool = "futu_get_cash_log",
372 env = %req.env,
373 acc_id = req.acc_id,
374 market = ?req.market,
375 has_keyword = req.keyword.is_some(),
376 has_symbol = req.symbol.is_some()
377 );
378 let client = self
379 .read_client_or_err("futu_get_cash_log", &req_ctx, None, Some(req.acc_id))
380 .await?;
381 Self::wrap_result(
382 handlers::trade::get_cash_log(
383 &client,
384 handlers::trade::CashLogInput {
385 env: &req.env,
386 acc_id: req.acc_id,
387 begin_time: req.begin_time,
388 end_time: req.end_time,
389 biz_group_id: req.biz_group_id,
390 biz_sub_group_id: req.biz_sub_group_id,
391 in_out: req.in_out,
392 keyword: req.keyword,
393 symbol: req.symbol,
394 stock_id: req.stock_id,
395 log_id: req.log_id,
396 max_cnt: req.max_cnt,
397 currency: req.currency,
398 },
399 )
400 .await,
401 )
402 }
403
404 #[tool(
405 description = "Fetch a single cash log entry detail. Use after futu_get_cash_log; log_id comes from monthly_logs[].entries[].log_id."
406 )]
407 async fn futu_get_cash_detail(
408 &self,
409 Parameters(req): Parameters<CashDetailReq>,
410 req_ctx: RequestContext<RoleServer>,
411 ) -> std::result::Result<String, String> {
412 tracing::info!(
413 tool = "futu_get_cash_detail",
414 env = %req.env,
415 acc_id = req.acc_id,
416 market = ?req.market,
417 log_id_len = req.log_id.len()
418 );
419 let client = self
420 .read_client_or_err("futu_get_cash_detail", &req_ctx, None, Some(req.acc_id))
421 .await?;
422 Self::wrap_result(
423 handlers::trade::get_cash_detail(&client, &req.env, req.acc_id, req.log_id.clone())
424 .await,
425 )
426 }
427
428 #[tool(
429 description = "Fetch cash log business group, currency, and direction metadata for client UI filters. Returns biz_groups with sub_groups, currencies, and directions."
430 )]
431 async fn futu_get_biz_group(
432 &self,
433 Parameters(req): Parameters<BizGroupReq>,
434 req_ctx: RequestContext<RoleServer>,
435 ) -> std::result::Result<String, String> {
436 tracing::info!(
437 tool = "futu_get_biz_group",
438 env = %req.env,
439 acc_id = req.acc_id,
440 market = ?req.market
441 );
442 let client = self
443 .read_client_or_err("futu_get_biz_group", &req_ctx, None, Some(req.acc_id))
444 .await?;
445 Self::wrap_result(handlers::trade::get_biz_group(&client, &req.env, req.acc_id).await)
446 }
447
448 #[tool(
459 description = "Per-account margin info: buying power, leverage, risk status, liquidity, HK margin fields, and mobile risk metadata. Supports HK / US / CN_AH markets (mobile cmd 3101/3102/3107). Complements futu_get_margin_ratio, which is per-security."
460 )]
461 async fn futu_get_margin_info(
462 &self,
463 Parameters(req): Parameters<MarginInfoReq>,
464 req_ctx: RequestContext<RoleServer>,
465 ) -> std::result::Result<String, String> {
466 tracing::info!(
467 tool = "futu_get_margin_info",
468 env = %req.env,
469 acc_id = req.acc_id,
470 market = %req.market
471 );
472 let client = self
473 .read_client_or_err("futu_get_margin_info", &req_ctx, None, Some(req.acc_id))
474 .await?;
475 Self::wrap_result(
476 handlers::trade::get_margin_info(&client, &req.env, req.acc_id, &req.market).await,
477 )
478 }
479
480 #[tool(
490 description = "Query account compliance flag (product access, risk disclosure, opt-in status). Common flag_id values: 5=US options, 22=derivatives disclosure, 10=fund KYC R1-R5, 16=PDT, 23=US OTC, 11=HK options. The response includes item_present and flag_value_present so clients can distinguish a missing flag record from an explicit flag_value=0."
491 )]
492 async fn futu_get_account_flag(
493 &self,
494 Parameters(req): Parameters<AccountFlagReq>,
495 req_ctx: RequestContext<RoleServer>,
496 ) -> std::result::Result<String, String> {
497 tracing::info!(
498 tool = "futu_get_account_flag",
499 env = %req.env,
500 acc_id = req.acc_id,
501 flag_id = req.flag_id
502 );
503 let client = self
504 .read_client_or_err("futu_get_account_flag", &req_ctx, None, Some(req.acc_id))
505 .await?;
506 Self::wrap_result(
507 handlers::trade::get_account_flag(&client, &req.env, req.acc_id, req.flag_id).await,
508 )
509 }
510
511 #[tool(
521 description = "Bond account total asset and P&L summary for HK/US/SG bond accounts. Returns total_asset, position_incomes, today_incomes, accrued_interest, and ccy."
522 )]
523 async fn futu_get_bond_total_asset(
524 &self,
525 Parameters(req): Parameters<BondAccountReq>,
526 req_ctx: RequestContext<RoleServer>,
527 ) -> std::result::Result<String, String> {
528 tracing::info!(
529 tool = "futu_get_bond_total_asset",
530 env = %req.env,
531 acc_id = req.acc_id,
532 market = %req.market
533 );
534 let client = self
535 .read_client_or_err(
536 "futu_get_bond_total_asset",
537 &req_ctx,
538 None,
539 Some(req.acc_id),
540 )
541 .await?;
542 Self::wrap_result(
543 handlers::trade::get_bond_total_asset(&client, &req.env, req.acc_id, &req.market).await,
544 )
545 }
546
547 #[tool(
548 description = "Single bond position for HK/US/SG bond accounts, including market value, quantity, cost, expiry, dividend schedule, accrued interest, legacy notice fields, notice_list, currency, and price."
549 )]
550 async fn futu_get_bond_single_asset(
551 &self,
552 Parameters(req): Parameters<BondSymbolReq>,
553 req_ctx: RequestContext<RoleServer>,
554 ) -> std::result::Result<String, String> {
555 tracing::info!(
556 tool = "futu_get_bond_single_asset",
557 env = %req.env,
558 acc_id = req.acc_id,
559 market = %req.market,
560 symbol = %req.symbol
561 );
562 let client = self
563 .read_client_or_err(
564 "futu_get_bond_single_asset",
565 &req_ctx,
566 None,
567 Some(req.acc_id),
568 )
569 .await?;
570 Self::wrap_result(
571 handlers::trade::get_bond_single_asset(
572 &client,
573 &req.env,
574 req.acc_id,
575 &req.market,
576 &req.symbol,
577 )
578 .await,
579 )
580 }
581
582 #[tool(
583 description = "Bond account position list for HK/US/SG bond accounts. Returns total and bond_list items with name, symbol, market value, quantity, price, cost, incomes, accrued interest, notice, call flag, and ccy."
584 )]
585 async fn futu_get_bond_position_list(
586 &self,
587 Parameters(req): Parameters<BondAccountReq>,
588 req_ctx: RequestContext<RoleServer>,
589 ) -> std::result::Result<String, String> {
590 tracing::info!(
591 tool = "futu_get_bond_position_list",
592 env = %req.env,
593 acc_id = req.acc_id,
594 market = %req.market
595 );
596 let client = self
597 .read_client_or_err(
598 "futu_get_bond_position_list",
599 &req_ctx,
600 None,
601 Some(req.acc_id),
602 )
603 .await?;
604 Self::wrap_result(
605 handlers::trade::get_bond_position_list(&client, &req.env, req.acc_id, &req.market)
606 .await,
607 )
608 }
609
610 #[tool(
611 description = "Query whether the user needs to answer a suitability questionnaire before bond trading. Returns need_to_answer plus notice fields such as title, content, and confirm_url."
612 )]
613 async fn futu_get_bond_answer_state(
614 &self,
615 Parameters(req): Parameters<BondSymbolReq>,
616 req_ctx: RequestContext<RoleServer>,
617 ) -> std::result::Result<String, String> {
618 tracing::info!(
619 tool = "futu_get_bond_answer_state",
620 env = %req.env,
621 acc_id = req.acc_id,
622 market = %req.market,
623 symbol = %req.symbol
624 );
625 let client = self
626 .read_client_or_err(
627 "futu_get_bond_answer_state",
628 &req_ctx,
629 None,
630 Some(req.acc_id),
631 )
632 .await?;
633 Self::wrap_result(
634 handlers::trade::get_bond_answer_state(
635 &client,
636 &req.env,
637 req.acc_id,
638 &req.market,
639 &req.symbol,
640 )
641 .await,
642 )
643 }
644
645 #[tool(
646 description = "Bond trade reminders for buy/sell availability, complex product, high risk, and pre-qualification. Returns ReminderItem fields for tradeable, complex_product, high_risk, sell_tradeable, and pre_qualification."
647 )]
648 async fn futu_get_bond_trade_reminder(
649 &self,
650 Parameters(req): Parameters<BondSymbolReq>,
651 req_ctx: RequestContext<RoleServer>,
652 ) -> std::result::Result<String, String> {
653 tracing::info!(
654 tool = "futu_get_bond_trade_reminder",
655 env = %req.env,
656 acc_id = req.acc_id,
657 market = %req.market,
658 symbol = %req.symbol
659 );
660 let client = self
661 .read_client_or_err(
662 "futu_get_bond_trade_reminder",
663 &req_ctx,
664 None,
665 Some(req.acc_id),
666 )
667 .await?;
668 Self::wrap_result(
669 handlers::trade::get_bond_trade_reminder(
670 &client,
671 &req.env,
672 req.acc_id,
673 &req.market,
674 &req.symbol,
675 )
676 .await,
677 )
678 }
679}