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<PositionReq>,
91 req_ctx: RequestContext<RoleServer>,
92 ) -> std::result::Result<String, String> {
93 let acc_req = req.as_trd_acc_req();
94 let resolved = self
95 .resolve_read_trd_account("futu_get_positions", &acc_req, &req_ctx)
96 .await?;
97 tracing::info!(
98 tool = "futu_get_positions",
99 market = %req.market,
100 acc_id = resolved.acc_id,
101 currency = ?req.currency,
102 option_strategy_view = req.option_strategy_view,
103 );
104 Self::wrap_result(
105 handlers::trade::get_positions(
106 &resolved.client,
107 &req.env,
108 resolved.acc_id,
109 &req.market,
110 req.currency.as_deref(),
111 req.option_strategy_view,
112 )
113 .await,
114 )
115 }
116
117 #[tool(
118 description = "Get today's orders (including pending / filled / cancelled) for an account in a given market."
119 )]
120 async fn futu_get_orders(
121 &self,
122 Parameters(req): Parameters<TrdAccReq>,
123 req_ctx: RequestContext<RoleServer>,
124 ) -> std::result::Result<String, String> {
125 let resolved = self
126 .resolve_read_trd_account("futu_get_orders", &req, &req_ctx)
127 .await?;
128 tracing::info!(tool = "futu_get_orders", market = %req.market, acc_id = resolved.acc_id);
129 Self::wrap_result(
130 handlers::trade::get_orders(&resolved.client, &req.env, resolved.acc_id, &req.market)
131 .await,
132 )
133 }
134
135 #[tool(description = "Get today's deals / order fills for an account in a given market.")]
136 async fn futu_get_deals(
137 &self,
138 Parameters(req): Parameters<TrdAccReq>,
139 req_ctx: RequestContext<RoleServer>,
140 ) -> std::result::Result<String, String> {
141 let resolved = self
142 .resolve_read_trd_account("futu_get_deals", &req, &req_ctx)
143 .await?;
144 tracing::info!(tool = "futu_get_deals", market = %req.market, acc_id = resolved.acc_id);
145 Self::wrap_result(
146 handlers::trade::get_deals(&resolved.client, &req.env, resolved.acc_id, &req.market)
147 .await,
148 )
149 }
150
151 #[tool(
154 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)."
155 )]
156 async fn futu_get_max_trd_qtys(
157 &self,
158 Parameters(req): Parameters<MaxTrdQtysReq>,
159 req_ctx: RequestContext<RoleServer>,
160 ) -> std::result::Result<String, String> {
161 tracing::info!(tool = "futu_get_max_trd_qtys", market = %req.market, acc_id = req.acc_id, code = %req.code);
162 let client = self
163 .read_client_or_err("futu_get_max_trd_qtys", &req_ctx, None, Some(req.acc_id))
164 .await?;
165 Self::wrap_result(
166 handlers::trade::get_max_trd_qtys(
167 &client,
168 handlers::trade::MaxTrdQtysInput {
169 env: &req.env,
170 acc_id: req.acc_id,
171 market: &req.market,
172 order_type: req.order_type,
173 code: &req.code,
174 price: req.price,
175 jp_acc_type: req.jp_acc_type,
176 order_id: req.order_id,
177 },
178 )
179 .await,
180 )
181 }
182
183 #[tool(
184 description = "Query order fee breakdown (commission / platform fee / stamp duty) by order_id_ex list. Python SDK: OpenTradeContext.order_fee_query."
185 )]
186 async fn futu_get_order_fee(
187 &self,
188 Parameters(req): Parameters<OrderFeeReq>,
189 req_ctx: RequestContext<RoleServer>,
190 ) -> std::result::Result<String, String> {
191 tracing::info!(tool = "futu_get_order_fee", market = %req.market, acc_id = req.acc_id, count = req.order_id_ex_list.len());
192 let client = self
193 .read_client_or_err("futu_get_order_fee", &req_ctx, None, Some(req.acc_id))
194 .await?;
195 Self::wrap_result(
196 handlers::trade::get_order_fee(
197 &client,
198 &req.env,
199 req.acc_id,
200 &req.market,
201 &req.order_id_ex_list,
202 )
203 .await,
204 )
205 }
206
207 #[tool(
208 description = "Query margin ratio (long/short permissions, short-pool remaining, long/short initial margin ratios) by symbol list. Python SDK: OpenTradeContext.get_margin_ratio."
209 )]
210 async fn futu_get_margin_ratio(
211 &self,
212 Parameters(req): Parameters<MarginRatioReq>,
213 req_ctx: RequestContext<RoleServer>,
214 ) -> std::result::Result<String, String> {
215 tracing::info!(tool = "futu_get_margin_ratio", market = %req.market, acc_id = req.acc_id, count = req.codes.len());
216 let client = self
217 .read_client_or_err("futu_get_margin_ratio", &req_ctx, None, Some(req.acc_id))
218 .await?;
219 Self::wrap_result(
220 handlers::trade::get_margin_ratio(
221 &client,
222 &req.env,
223 req.acc_id,
224 &req.market,
225 &req.codes,
226 )
227 .await,
228 )
229 }
230
231 #[tool(
232 description = "Query historical orders (filled / cancelled) with optional time range + code filter. Python SDK: OpenTradeContext.history_order_list_query."
233 )]
234 async fn futu_get_history_orders(
235 &self,
236 Parameters(req): Parameters<HistoryQueryReq>,
237 req_ctx: RequestContext<RoleServer>,
238 ) -> std::result::Result<String, String> {
239 tracing::info!(tool = "futu_get_history_orders", market = %req.market, acc_id = req.acc_id);
240 let client = self
241 .read_client_or_err("futu_get_history_orders", &req_ctx, None, Some(req.acc_id))
242 .await?;
243 Self::wrap_result(
244 handlers::trade::get_history_orders(
245 &client,
246 handlers::trade::HistoryQueryInput {
247 env: &req.env,
248 acc_id: req.acc_id,
249 market: &req.market,
250 code_list: req.code_list,
251 begin_time: req.begin_time,
252 end_time: req.end_time,
253 },
254 )
255 .await,
256 )
257 }
258
259 #[tool(
260 description = "Query historical deals / fills with optional time range + code filter. Python SDK: OpenTradeContext.history_deal_list_query."
261 )]
262 async fn futu_get_history_deals(
263 &self,
264 Parameters(req): Parameters<HistoryQueryReq>,
265 req_ctx: RequestContext<RoleServer>,
266 ) -> std::result::Result<String, String> {
267 tracing::info!(tool = "futu_get_history_deals", market = %req.market, acc_id = req.acc_id);
268 let client = self
269 .read_client_or_err("futu_get_history_deals", &req_ctx, None, Some(req.acc_id))
270 .await?;
271 Self::wrap_result(
272 handlers::trade::get_history_deals(
273 &client,
274 handlers::trade::HistoryQueryInput {
275 env: &req.env,
276 acc_id: req.acc_id,
277 market: &req.market,
278 code_list: req.code_list,
279 begin_time: req.begin_time,
280 end_time: req.end_time,
281 },
282 )
283 .await,
284 )
285 }
286
287 #[tool(
288 description = "Account cash-flow statement for a clearing date. Python SDK: OpenTradeContext.get_acc_cash_flow."
289 )]
290 async fn futu_get_acc_cash_flow(
291 &self,
292 Parameters(req): Parameters<AccCashFlowReq>,
293 req_ctx: RequestContext<RoleServer>,
294 ) -> std::result::Result<String, String> {
295 tracing::info!(
296 tool = "futu_get_acc_cash_flow",
297 env = %req.env,
298 acc_id = req.acc_id,
299 market = %req.market,
300 date = %req.clearing_date
301 );
302 let client = self
303 .read_client_or_err("futu_get_acc_cash_flow", &req_ctx, None, Some(req.acc_id))
304 .await?;
305 Self::wrap_result(
306 handlers::trade::get_acc_cash_flow(
307 &client,
308 &req.env,
309 req.acc_id,
310 &req.market,
311 &req.clearing_date,
312 req.direction,
313 )
314 .await,
315 )
316 }
317
318 #[tool(
329 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."
330 )]
331 async fn futu_get_flow_summary(
332 &self,
333 Parameters(req): Parameters<AccCashFlowReq>,
334 req_ctx: RequestContext<RoleServer>,
335 ) -> std::result::Result<String, String> {
336 tracing::info!(
338 tool = "futu_get_flow_summary",
339 alias_of = "futu_get_acc_cash_flow",
340 env = %req.env,
341 acc_id = req.acc_id,
342 market = %req.market,
343 date = %req.clearing_date
344 );
345 let client = self
346 .read_client_or_err("futu_get_flow_summary", &req_ctx, None, Some(req.acc_id))
347 .await?;
348 Self::wrap_result(
349 handlers::trade::get_acc_cash_flow(
350 &client,
351 &req.env,
352 req.acc_id,
353 &req.market,
354 &req.clearing_date,
355 req.direction,
356 )
357 .await,
358 )
359 }
360
361 #[tool(
373 description = "Fetch detailed account cash log entries with richer filters 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."
374 )]
375 async fn futu_get_cash_log(
376 &self,
377 Parameters(req): Parameters<CashLogReq>,
378 req_ctx: RequestContext<RoleServer>,
379 ) -> std::result::Result<String, String> {
380 tracing::info!(
381 tool = "futu_get_cash_log",
382 env = %req.env,
383 acc_id = req.acc_id,
384 market = ?req.market,
385 has_keyword = req.keyword.is_some(),
386 has_symbol = req.symbol.is_some()
387 );
388 let client = self
389 .read_client_or_err("futu_get_cash_log", &req_ctx, None, Some(req.acc_id))
390 .await?;
391 Self::wrap_result(
392 handlers::trade::get_cash_log(
393 &client,
394 handlers::trade::CashLogInput {
395 env: &req.env,
396 acc_id: req.acc_id,
397 begin_time: req.begin_time,
398 end_time: req.end_time,
399 biz_group_id: req.biz_group_id,
400 biz_sub_group_id: req.biz_sub_group_id,
401 in_out: req.in_out,
402 keyword: req.keyword,
403 symbol: req.symbol,
404 stock_id: req.stock_id,
405 log_id: req.log_id,
406 max_cnt: req.max_cnt,
407 currency: req.currency,
408 },
409 )
410 .await,
411 )
412 }
413
414 #[tool(
415 description = "Fetch a single cash log entry detail. Use after futu_get_cash_log; log_id comes from monthly_logs[].entries[].log_id."
416 )]
417 async fn futu_get_cash_detail(
418 &self,
419 Parameters(req): Parameters<CashDetailReq>,
420 req_ctx: RequestContext<RoleServer>,
421 ) -> std::result::Result<String, String> {
422 tracing::info!(
423 tool = "futu_get_cash_detail",
424 env = %req.env,
425 acc_id = req.acc_id,
426 market = ?req.market,
427 log_id_len = req.log_id.len()
428 );
429 let client = self
430 .read_client_or_err("futu_get_cash_detail", &req_ctx, None, Some(req.acc_id))
431 .await?;
432 Self::wrap_result(
433 handlers::trade::get_cash_detail(&client, &req.env, req.acc_id, req.log_id.clone())
434 .await,
435 )
436 }
437
438 #[tool(
439 description = "Fetch cash log business group, currency, and direction metadata for client UI filters. Returns biz_groups with sub_groups, currencies, and directions."
440 )]
441 async fn futu_get_biz_group(
442 &self,
443 Parameters(req): Parameters<BizGroupReq>,
444 req_ctx: RequestContext<RoleServer>,
445 ) -> std::result::Result<String, String> {
446 tracing::info!(
447 tool = "futu_get_biz_group",
448 env = %req.env,
449 acc_id = req.acc_id,
450 market = ?req.market
451 );
452 let client = self
453 .read_client_or_err("futu_get_biz_group", &req_ctx, None, Some(req.acc_id))
454 .await?;
455 Self::wrap_result(handlers::trade::get_biz_group(&client, &req.env, req.acc_id).await)
456 }
457
458 #[tool(
469 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."
470 )]
471 async fn futu_get_margin_info(
472 &self,
473 Parameters(req): Parameters<MarginInfoReq>,
474 req_ctx: RequestContext<RoleServer>,
475 ) -> std::result::Result<String, String> {
476 tracing::info!(
477 tool = "futu_get_margin_info",
478 env = %req.env,
479 acc_id = req.acc_id,
480 market = %req.market
481 );
482 let client = self
483 .read_client_or_err("futu_get_margin_info", &req_ctx, None, Some(req.acc_id))
484 .await?;
485 Self::wrap_result(
486 handlers::trade::get_margin_info(&client, &req.env, req.acc_id, &req.market).await,
487 )
488 }
489
490 #[tool(
500 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."
501 )]
502 async fn futu_get_account_flag(
503 &self,
504 Parameters(req): Parameters<AccountFlagReq>,
505 req_ctx: RequestContext<RoleServer>,
506 ) -> std::result::Result<String, String> {
507 tracing::info!(
508 tool = "futu_get_account_flag",
509 env = %req.env,
510 acc_id = req.acc_id,
511 flag_id = req.flag_id
512 );
513 let client = self
514 .read_client_or_err("futu_get_account_flag", &req_ctx, None, Some(req.acc_id))
515 .await?;
516 Self::wrap_result(
517 handlers::trade::get_account_flag(&client, &req.env, req.acc_id, req.flag_id).await,
518 )
519 }
520
521 #[tool(
531 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."
532 )]
533 async fn futu_get_bond_total_asset(
534 &self,
535 Parameters(req): Parameters<BondAccountReq>,
536 req_ctx: RequestContext<RoleServer>,
537 ) -> std::result::Result<String, String> {
538 tracing::info!(
539 tool = "futu_get_bond_total_asset",
540 env = %req.env,
541 acc_id = req.acc_id,
542 market = %req.market
543 );
544 let client = self
545 .read_client_or_err(
546 "futu_get_bond_total_asset",
547 &req_ctx,
548 None,
549 Some(req.acc_id),
550 )
551 .await?;
552 Self::wrap_result(
553 handlers::trade::get_bond_total_asset(&client, &req.env, req.acc_id, &req.market).await,
554 )
555 }
556
557 #[tool(
558 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."
559 )]
560 async fn futu_get_bond_single_asset(
561 &self,
562 Parameters(req): Parameters<BondSymbolReq>,
563 req_ctx: RequestContext<RoleServer>,
564 ) -> std::result::Result<String, String> {
565 tracing::info!(
566 tool = "futu_get_bond_single_asset",
567 env = %req.env,
568 acc_id = req.acc_id,
569 market = %req.market,
570 symbol = %req.symbol
571 );
572 let client = self
573 .read_client_or_err(
574 "futu_get_bond_single_asset",
575 &req_ctx,
576 None,
577 Some(req.acc_id),
578 )
579 .await?;
580 Self::wrap_result(
581 handlers::trade::get_bond_single_asset(
582 &client,
583 &req.env,
584 req.acc_id,
585 &req.market,
586 &req.symbol,
587 )
588 .await,
589 )
590 }
591
592 #[tool(
593 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."
594 )]
595 async fn futu_get_bond_position_list(
596 &self,
597 Parameters(req): Parameters<BondAccountReq>,
598 req_ctx: RequestContext<RoleServer>,
599 ) -> std::result::Result<String, String> {
600 tracing::info!(
601 tool = "futu_get_bond_position_list",
602 env = %req.env,
603 acc_id = req.acc_id,
604 market = %req.market
605 );
606 let client = self
607 .read_client_or_err(
608 "futu_get_bond_position_list",
609 &req_ctx,
610 None,
611 Some(req.acc_id),
612 )
613 .await?;
614 Self::wrap_result(
615 handlers::trade::get_bond_position_list(&client, &req.env, req.acc_id, &req.market)
616 .await,
617 )
618 }
619
620 #[tool(
621 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."
622 )]
623 async fn futu_get_bond_answer_state(
624 &self,
625 Parameters(req): Parameters<BondSymbolReq>,
626 req_ctx: RequestContext<RoleServer>,
627 ) -> std::result::Result<String, String> {
628 tracing::info!(
629 tool = "futu_get_bond_answer_state",
630 env = %req.env,
631 acc_id = req.acc_id,
632 market = %req.market,
633 symbol = %req.symbol
634 );
635 let client = self
636 .read_client_or_err(
637 "futu_get_bond_answer_state",
638 &req_ctx,
639 None,
640 Some(req.acc_id),
641 )
642 .await?;
643 Self::wrap_result(
644 handlers::trade::get_bond_answer_state(
645 &client,
646 &req.env,
647 req.acc_id,
648 &req.market,
649 &req.symbol,
650 )
651 .await,
652 )
653 }
654
655 #[tool(
656 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."
657 )]
658 async fn futu_get_bond_trade_reminder(
659 &self,
660 Parameters(req): Parameters<BondSymbolReq>,
661 req_ctx: RequestContext<RoleServer>,
662 ) -> std::result::Result<String, String> {
663 tracing::info!(
664 tool = "futu_get_bond_trade_reminder",
665 env = %req.env,
666 acc_id = req.acc_id,
667 market = %req.market,
668 symbol = %req.symbol
669 );
670 let client = self
671 .read_client_or_err(
672 "futu_get_bond_trade_reminder",
673 &req_ctx,
674 None,
675 Some(req.acc_id),
676 )
677 .await?;
678 Self::wrap_result(
679 handlers::trade::get_bond_trade_reminder(
680 &client,
681 &req.env,
682 req.acc_id,
683 &req.market,
684 &req.symbol,
685 )
686 .await,
687 )
688 }
689}