1use futu_auth::CheckCtx;
4use rmcp::{
5 handler::server::wrapper::Parameters, schemars, service::RequestContext, tool, tool_router,
6 RoleServer,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::guard;
11use crate::handlers;
12use crate::state::ServerState;
13
14#[derive(Debug, Deserialize, schemars::JsonSchema)]
17pub struct SymbolReq {
18 #[schemars(description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL")]
19 pub symbol: String,
20}
21
22#[derive(Debug, Deserialize, schemars::JsonSchema)]
23pub struct SymbolListReq {
24 #[schemars(description = "List of security symbols, each MARKET.CODE format")]
25 pub symbols: Vec<String>,
26}
27
28#[derive(Debug, Deserialize, schemars::JsonSchema)]
29pub struct KLineReq {
30 #[schemars(description = "Security symbol (MARKET.CODE)")]
31 pub symbol: String,
32 #[schemars(
33 description = "K-line type: day|week|month|quarter|year|1min|3min|5min|15min|30min|60min"
34 )]
35 #[serde(default = "default_kl_type")]
36 pub kl_type: String,
37 #[schemars(description = "Number of candles to return (default 100)")]
38 pub count: Option<i32>,
39 #[schemars(description = "Start date yyyy-MM-dd (optional; default computed from count)")]
40 pub begin: Option<String>,
41 #[schemars(description = "End date yyyy-MM-dd (optional; default today)")]
42 pub end: Option<String>,
43}
44
45fn default_kl_type() -> String {
46 "day".to_string()
47}
48
49#[derive(Debug, Deserialize, schemars::JsonSchema)]
50pub struct OrderBookReq {
51 pub symbol: String,
52 #[schemars(description = "Order book depth, 1-10 (default 10)")]
53 #[serde(default = "default_depth")]
54 pub depth: i32,
55}
56
57fn default_depth() -> i32 {
58 10
59}
60
61#[derive(Debug, Deserialize, schemars::JsonSchema)]
62pub struct TickerReq {
63 pub symbol: String,
64 #[schemars(description = "Number of ticks to fetch (default 100, max 1000)")]
65 #[serde(default = "default_ticker_count")]
66 pub count: i32,
67}
68
69fn default_ticker_count() -> i32 {
70 100
71}
72
73#[derive(Debug, Deserialize, schemars::JsonSchema)]
74pub struct PlateListReq {
75 #[schemars(description = "Market: HK|HK_FUTURE|US|SH|SZ")]
76 pub market: String,
77 #[schemars(description = "Plate set: all|industry|region|concept (default all)")]
78 #[serde(default = "default_plate_set")]
79 pub plate_set: String,
80}
81
82fn default_plate_set() -> String {
83 "all".to_string()
84}
85
86#[derive(Debug, Deserialize, schemars::JsonSchema)]
87pub struct PlateStocksReq {
88 #[schemars(description = "Plate symbol, MARKET.CODE format (e.g. HK.LIST1001)")]
89 pub plate: String,
90}
91
92#[derive(Debug, Deserialize, schemars::JsonSchema)]
93pub struct TrdAccReq {
94 #[schemars(description = "Trade market: HK|US|CN|HKCC")]
95 pub market: String,
96 #[schemars(description = "Trading account ID (u64)")]
97 pub acc_id: u64,
98 #[schemars(description = "Trade environment: real|simulate (default real)")]
99 #[serde(default = "default_env")]
100 pub env: String,
101}
102
103fn default_env() -> String {
104 "real".to_string()
105}
106
107#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
110pub struct PlaceOrderReq {
111 #[schemars(description = "Trade market: HK|US|CN|HKCC")]
112 pub market: String,
113 #[schemars(description = "Trading account ID (u64)")]
114 pub acc_id: u64,
115 #[schemars(description = "Trade environment: real|simulate. Defaults to simulate for safety.")]
116 #[serde(default = "default_env_simulate")]
117 pub env: String,
118 #[schemars(description = "Order side: BUY|SELL|SELL_SHORT|BUY_BACK")]
119 pub side: String,
120 #[schemars(
121 description = "Order type: NORMAL (limit) | MARKET | ABSOLUTE_LIMIT | AUCTION | AUCTION_LIMIT | SPECIAL_LIMIT"
122 )]
123 #[serde(default = "default_order_type")]
124 pub order_type: String,
125 #[schemars(description = "Security code WITHOUT market prefix, e.g. 00700 / AAPL / 600519")]
126 pub code: String,
127 #[schemars(description = "Order quantity (shares / contracts)")]
128 pub qty: f64,
129 #[schemars(description = "Limit price (required for NORMAL; optional for MARKET)")]
130 pub price: Option<f64>,
131 #[schemars(
132 description = "Optional per-call API key override (plaintext). When set, this key is used for scope/limits/audit instead of the process-wide FUTU_MCP_API_KEY. Useful for multi-tenant scenarios where different calls should be billed / scoped to different keys."
133 )]
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub api_key: Option<String>,
136}
137
138fn default_env_simulate() -> String {
139 "simulate".to_string()
140}
141
142fn default_order_type() -> String {
143 "NORMAL".to_string()
144}
145
146#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
147pub struct ModifyOrderReq {
148 #[schemars(description = "Trade market: HK|US|CN|HKCC")]
149 pub market: String,
150 #[schemars(description = "Trading account ID (u64)")]
151 pub acc_id: u64,
152 #[schemars(description = "Trade environment: real|simulate (default simulate)")]
153 #[serde(default = "default_env_simulate")]
154 pub env: String,
155 #[schemars(description = "Order ID to modify")]
156 pub order_id: u64,
157 #[schemars(
158 description = "Modify op: NORMAL (change qty/price) | CANCEL | DISABLE | ENABLE | DELETE"
159 )]
160 #[serde(default = "default_modify_op")]
161 pub op: String,
162 #[schemars(description = "New quantity (for NORMAL op)")]
163 pub qty: Option<f64>,
164 #[schemars(description = "New price (for NORMAL op)")]
165 pub price: Option<f64>,
166 #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub api_key: Option<String>,
169}
170
171fn default_modify_op() -> String {
172 "NORMAL".to_string()
173}
174
175#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
176pub struct CancelOrderReq {
177 #[schemars(description = "Trade market: HK|US|CN|HKCC")]
178 pub market: String,
179 #[schemars(description = "Trading account ID (u64)")]
180 pub acc_id: u64,
181 #[schemars(description = "Trade environment: real|simulate (default simulate)")]
182 #[serde(default = "default_env_simulate")]
183 pub env: String,
184 #[schemars(description = "Order ID to cancel")]
185 pub order_id: u64,
186 #[schemars(description = "Optional per-call API key override. See PlaceOrderReq.api_key.")]
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub api_key: Option<String>,
189}
190
191#[derive(Clone)]
194pub struct FutuServer {
195 pub state: ServerState,
196}
197
198impl FutuServer {
199 pub fn new(state: ServerState) -> Self {
200 Self { state }
201 }
202
203 fn err(msg: impl std::fmt::Display) -> String {
204 serde_json::json!({ "error": msg.to_string() }).to_string()
205 }
206
207 fn wrap<E: std::fmt::Display>(res: std::result::Result<String, E>) -> String {
208 match res {
209 Ok(s) => s,
210 Err(e) => Self::err(e),
211 }
212 }
213
214 async fn client_or_err(
215 &self,
216 ) -> std::result::Result<std::sync::Arc<futu_net::client::FutuClient>, String> {
217 self.state
218 .client()
219 .await
220 .map_err(|e| Self::err(format!("gateway connect failed: {e}")))
221 }
222
223 fn require_tool_scope(&self, tool: &'static str) -> Option<String> {
226 guard::require_tool_scope(&self.state, tool).into_err_json()
227 }
228
229 fn require_trading(
231 &self,
232 tool: &'static str,
233 env: &str,
234 ctx: Option<CheckCtx>,
235 override_key: Option<&str>,
236 ) -> Option<String> {
237 guard::require_trading(&self.state, tool, env, ctx, override_key).into_err_json()
238 }
239
240 fn current_key_id(&self, override_key: Option<&str>) -> Option<String> {
244 if let Some(pt) = override_key.filter(|p| !p.is_empty()) {
245 if let Some(rec) = self.state.key_store.verify(pt) {
246 return Some(rec.id.clone());
247 }
248 }
249 self.state.authed_key.as_ref().map(|k| k.id.clone())
250 }
251}
252
253fn http_bearer_token(ctx: &RequestContext<RoleServer>) -> Option<String> {
261 let parts = ctx.extensions.get::<http::request::Parts>()?;
262 parts
263 .headers
264 .get("authorization")
265 .and_then(|v| v.to_str().ok())
266 .and_then(|v| v.strip_prefix("Bearer ").map(|s| s.trim().to_string()))
267 .filter(|s| !s.is_empty())
268}
269
270#[tool_router(server_handler)]
271impl FutuServer {
272 #[tool(description = "Ping the Futu gateway. Returns RTT and connection status.")]
275 async fn futu_ping(&self) -> String {
276 if let Some(rej) = self.require_tool_scope("futu_ping") {
277 return rej;
278 }
279 let gateway = self.state.inner.lock().await.gateway.clone();
280 let client = match self.state.client().await {
281 Ok(c) => c,
282 Err(e) => {
283 tracing::info!(tool = "futu_ping", ok = false, "connect failed: {e}");
284 return serde_json::to_string_pretty(&handlers::core::PingOut {
285 gateway,
286 ok: false,
287 rtt_ms: 0.0,
288 message: format!("connect failed: {e}"),
289 })
290 .unwrap_or_default();
291 }
292 };
293 let out = handlers::core::ping(&client, &gateway).await;
294 tracing::info!(tool = "futu_ping", ok = out.ok, rtt_ms = out.rtt_ms);
295 serde_json::to_string_pretty(&out).unwrap_or_default()
296 }
297
298 #[tool(
299 description = "Get real-time basic quote (price, volume, turnover) for a security. Auto-subscribes SubType::Basic on first call."
300 )]
301 async fn futu_get_quote(&self, Parameters(req): Parameters<SymbolReq>) -> String {
302 if let Some(rej) = self.require_tool_scope("futu_get_quote") {
303 return rej;
304 }
305 tracing::info!(tool = "futu_get_quote", symbol = %req.symbol);
306 let client = match self.client_or_err().await {
307 Ok(c) => c,
308 Err(e) => return e,
309 };
310 Self::wrap(handlers::core::get_quote(&client, &req.symbol).await)
311 }
312
313 #[tool(
314 description = "Get a security snapshot (one-shot, no subscription) with extended fields: 52-week high/low, avg price, volume ratio, amplitude, bid/ask."
315 )]
316 async fn futu_get_snapshot(&self, Parameters(req): Parameters<SymbolReq>) -> String {
317 if let Some(rej) = self.require_tool_scope("futu_get_snapshot") {
318 return rej;
319 }
320 tracing::info!(tool = "futu_get_snapshot", symbol = %req.symbol);
321 let client = match self.client_or_err().await {
322 Ok(c) => c,
323 Err(e) => return e,
324 };
325 Self::wrap(handlers::core::get_snapshot(&client, &req.symbol).await)
326 }
327
328 #[tool(
331 description = "Get historical K-line (OHLCV). Supports day/week/month/quarter/year plus 1/3/5/15/30/60 minute bars."
332 )]
333 async fn futu_get_kline(&self, Parameters(req): Parameters<KLineReq>) -> String {
334 if let Some(rej) = self.require_tool_scope("futu_get_kline") {
335 return rej;
336 }
337 tracing::info!(
338 tool = "futu_get_kline",
339 symbol = %req.symbol,
340 kl_type = %req.kl_type,
341 count = ?req.count
342 );
343 let client = match self.client_or_err().await {
344 Ok(c) => c,
345 Err(e) => return e,
346 };
347 Self::wrap(
348 handlers::market::get_kline(
349 &client,
350 &req.symbol,
351 &req.kl_type,
352 req.count,
353 req.begin.as_deref(),
354 req.end.as_deref(),
355 )
356 .await,
357 )
358 }
359
360 #[tool(
361 description = "Get the order book (bids and asks with price, volume, order count). Auto-subscribes OrderBook."
362 )]
363 async fn futu_get_orderbook(&self, Parameters(req): Parameters<OrderBookReq>) -> String {
364 if let Some(rej) = self.require_tool_scope("futu_get_orderbook") {
365 return rej;
366 }
367 tracing::info!(tool = "futu_get_orderbook", symbol = %req.symbol, depth = req.depth);
368 let client = match self.client_or_err().await {
369 Ok(c) => c,
370 Err(e) => return e,
371 };
372 Self::wrap(handlers::market::get_orderbook(&client, &req.symbol, req.depth).await)
373 }
374
375 #[tool(description = "Get recent ticker (trade-by-trade). Auto-subscribes Ticker.")]
376 async fn futu_get_ticker(&self, Parameters(req): Parameters<TickerReq>) -> String {
377 if let Some(rej) = self.require_tool_scope("futu_get_ticker") {
378 return rej;
379 }
380 tracing::info!(tool = "futu_get_ticker", symbol = %req.symbol, count = req.count);
381 let client = match self.client_or_err().await {
382 Ok(c) => c,
383 Err(e) => return e,
384 };
385 Self::wrap(handlers::market::get_ticker(&client, &req.symbol, req.count).await)
386 }
387
388 #[tool(
389 description = "Get intraday (RT / time-sharing) minute-by-minute price series. Auto-subscribes RT."
390 )]
391 async fn futu_get_rt(&self, Parameters(req): Parameters<SymbolReq>) -> String {
392 if let Some(rej) = self.require_tool_scope("futu_get_rt") {
393 return rej;
394 }
395 tracing::info!(tool = "futu_get_rt", symbol = %req.symbol);
396 let client = match self.client_or_err().await {
397 Ok(c) => c,
398 Err(e) => return e,
399 };
400 Self::wrap(handlers::market::get_rt(&client, &req.symbol).await)
401 }
402
403 #[tool(
404 description = "Get static info (name, lot size, listing date) for one or more securities. No subscription needed."
405 )]
406 async fn futu_get_static(&self, Parameters(req): Parameters<SymbolListReq>) -> String {
407 if let Some(rej) = self.require_tool_scope("futu_get_static") {
408 return rej;
409 }
410 tracing::info!(tool = "futu_get_static", symbols = ?req.symbols);
411 let client = match self.client_or_err().await {
412 Ok(c) => c,
413 Err(e) => return e,
414 };
415 Self::wrap(handlers::market::get_static(&client, &req.symbols).await)
416 }
417
418 #[tool(description = "Get the broker queue (HK only). Auto-subscribes Broker.")]
419 async fn futu_get_broker(&self, Parameters(req): Parameters<SymbolReq>) -> String {
420 if let Some(rej) = self.require_tool_scope("futu_get_broker") {
421 return rej;
422 }
423 tracing::info!(tool = "futu_get_broker", symbol = %req.symbol);
424 let client = match self.client_or_err().await {
425 Ok(c) => c,
426 Err(e) => return e,
427 };
428 Self::wrap(handlers::market::get_broker(&client, &req.symbol).await)
429 }
430
431 #[tool(description = "List plates by market and set type (industry / region / concept / all).")]
434 async fn futu_list_plates(&self, Parameters(req): Parameters<PlateListReq>) -> String {
435 if let Some(rej) = self.require_tool_scope("futu_list_plates") {
436 return rej;
437 }
438 tracing::info!(tool = "futu_list_plates", market = %req.market, set = %req.plate_set);
439 let client = match self.client_or_err().await {
440 Ok(c) => c,
441 Err(e) => return e,
442 };
443 Self::wrap(handlers::plate::list_plates(&client, &req.market, &req.plate_set).await)
444 }
445
446 #[tool(description = "List constituent securities of a plate.")]
447 async fn futu_plate_stocks(&self, Parameters(req): Parameters<PlateStocksReq>) -> String {
448 if let Some(rej) = self.require_tool_scope("futu_plate_stocks") {
449 return rej;
450 }
451 tracing::info!(tool = "futu_plate_stocks", plate = %req.plate);
452 let client = match self.client_or_err().await {
453 Ok(c) => c,
454 Err(e) => return e,
455 };
456 Self::wrap(handlers::plate::plate_stocks(&client, &req.plate).await)
457 }
458
459 #[tool(
462 description = "List all trading accounts (real + simulate) visible to the gateway login."
463 )]
464 async fn futu_list_accounts(&self) -> String {
465 if let Some(rej) = self.require_tool_scope("futu_list_accounts") {
466 return rej;
467 }
468 tracing::info!(tool = "futu_list_accounts");
469 let client = match self.client_or_err().await {
470 Ok(c) => c,
471 Err(e) => return e,
472 };
473 Self::wrap(handlers::trade::list_accounts(&client).await)
474 }
475
476 #[tool(
477 description = "Get account funds summary (total assets, cash, market value, buying power) for a given account + market."
478 )]
479 async fn futu_get_funds(&self, Parameters(req): Parameters<TrdAccReq>) -> String {
480 if let Some(rej) = self.require_tool_scope("futu_get_funds") {
481 return rej;
482 }
483 tracing::info!(tool = "futu_get_funds", market = %req.market, acc_id = req.acc_id, env = %req.env);
484 let client = match self.client_or_err().await {
485 Ok(c) => c,
486 Err(e) => return e,
487 };
488 Self::wrap(handlers::trade::get_funds(&client, &req.env, req.acc_id, &req.market).await)
489 }
490
491 #[tool(description = "Get current positions (holdings) for an account in a given market.")]
492 async fn futu_get_positions(&self, Parameters(req): Parameters<TrdAccReq>) -> String {
493 if let Some(rej) = self.require_tool_scope("futu_get_positions") {
494 return rej;
495 }
496 tracing::info!(tool = "futu_get_positions", market = %req.market, acc_id = req.acc_id);
497 let client = match self.client_or_err().await {
498 Ok(c) => c,
499 Err(e) => return e,
500 };
501 Self::wrap(handlers::trade::get_positions(&client, &req.env, req.acc_id, &req.market).await)
502 }
503
504 #[tool(
505 description = "Get today's orders (including pending / filled / cancelled) for an account in a given market."
506 )]
507 async fn futu_get_orders(&self, Parameters(req): Parameters<TrdAccReq>) -> String {
508 if let Some(rej) = self.require_tool_scope("futu_get_orders") {
509 return rej;
510 }
511 tracing::info!(tool = "futu_get_orders", market = %req.market, acc_id = req.acc_id);
512 let client = match self.client_or_err().await {
513 Ok(c) => c,
514 Err(e) => return e,
515 };
516 Self::wrap(handlers::trade::get_orders(&client, &req.env, req.acc_id, &req.market).await)
517 }
518
519 #[tool(description = "Get today's deals / order fills for an account in a given market.")]
520 async fn futu_get_deals(&self, Parameters(req): Parameters<TrdAccReq>) -> String {
521 if let Some(rej) = self.require_tool_scope("futu_get_deals") {
522 return rej;
523 }
524 tracing::info!(tool = "futu_get_deals", market = %req.market, acc_id = req.acc_id);
525 let client = match self.client_or_err().await {
526 Ok(c) => c,
527 Err(e) => return e,
528 };
529 Self::wrap(handlers::trade::get_deals(&client, &req.env, req.acc_id, &req.market).await)
530 }
531
532 #[tool(
535 description = "Place an order. REQUIRES futu-mcp to be started with --enable-trading. Real-env requires --allow-real-trading. Gateway must have been unlocked (password-based unlock_trade) beforehand."
536 )]
537 async fn futu_place_order(
538 &self,
539 Parameters(req): Parameters<PlaceOrderReq>,
540 req_ctx: RequestContext<RoleServer>,
541 ) -> String {
542 let header_token = http_bearer_token(&req_ctx);
544 let override_key = req.api_key.as_deref().or(header_token.as_deref());
545 let args_hash = guard::args_short_hash(&req);
546 tracing::warn!(
547 target: futu_auth::audit::TARGET,
548 iface = "mcp",
549 endpoint = "futu_place_order",
550 env = %req.env,
551 market = %req.market,
552 acc_id = req.acc_id,
553 side = %req.side,
554 order_type = %req.order_type,
555 code = %req.code,
556 qty = req.qty,
557 price = ?req.price,
558 args_hash = %args_hash,
559 outcome = "request",
560 "place_order request received"
561 );
562 let ctx = CheckCtx {
563 market: req.market.trim().to_ascii_uppercase(),
564 symbol: format!(
565 "{}.{}",
566 req.market.trim().to_ascii_uppercase(),
567 req.code.trim()
568 ),
569 order_value: req.price.map(|p| p * req.qty),
570 trd_side: Some(req.side.trim().to_ascii_uppercase()),
571 };
572 if let Some(rej) =
573 self.require_trading("futu_place_order", &req.env, Some(ctx), override_key)
574 {
575 return rej;
576 }
577 let client = match self.client_or_err().await {
578 Ok(c) => c,
579 Err(e) => return e,
580 };
581 let result = Self::wrap(
582 handlers::trade_write::place_order(
583 &client,
584 &req.env,
585 req.acc_id,
586 &req.market,
587 &req.side,
588 &req.order_type,
589 &req.code,
590 req.qty,
591 req.price,
592 )
593 .await,
594 );
595 guard::emit_trade_outcome(
596 "futu_place_order",
597 self.current_key_id(override_key).as_deref(),
598 &args_hash,
599 &result,
600 );
601 result
602 }
603
604 #[tool(
605 description = "Modify an existing order (change qty/price, cancel, disable/enable/delete). REQUIRES --enable-trading. For simple cancel, prefer futu_cancel_order."
606 )]
607 async fn futu_modify_order(
608 &self,
609 Parameters(req): Parameters<ModifyOrderReq>,
610 req_ctx: RequestContext<RoleServer>,
611 ) -> String {
612 let header_token = http_bearer_token(&req_ctx);
613 let override_key = req.api_key.as_deref().or(header_token.as_deref());
614 let args_hash = guard::args_short_hash(&req);
615 tracing::warn!(
616 target: futu_auth::audit::TARGET,
617 iface = "mcp",
618 endpoint = "futu_modify_order",
619 env = %req.env,
620 market = %req.market,
621 acc_id = req.acc_id,
622 order_id = req.order_id,
623 op = %req.op,
624 qty = ?req.qty,
625 price = ?req.price,
626 args_hash = %args_hash,
627 outcome = "request",
628 "modify_order request received"
629 );
630 let mutation_ctx = CheckCtx {
634 market: req.market.trim().to_ascii_uppercase(),
635 symbol: String::new(),
636 order_value: None,
637 trd_side: None,
638 };
639 if let Some(rej) = self.require_trading(
640 "futu_modify_order",
641 &req.env,
642 Some(mutation_ctx),
643 override_key,
644 ) {
645 return rej;
646 }
647 let client = match self.client_or_err().await {
648 Ok(c) => c,
649 Err(e) => return e,
650 };
651 let result = Self::wrap(
652 handlers::trade_write::modify_order(
653 &client,
654 &req.env,
655 req.acc_id,
656 &req.market,
657 req.order_id,
658 &req.op,
659 req.qty,
660 req.price,
661 )
662 .await,
663 );
664 guard::emit_trade_outcome(
665 "futu_modify_order",
666 self.current_key_id(override_key).as_deref(),
667 &args_hash,
668 &result,
669 );
670 result
671 }
672
673 #[tool(
674 description = "Cancel an order by order_id. REQUIRES --enable-trading. Convenience wrapper over modify_order with op=CANCEL."
675 )]
676 async fn futu_cancel_order(
677 &self,
678 Parameters(req): Parameters<CancelOrderReq>,
679 req_ctx: RequestContext<RoleServer>,
680 ) -> String {
681 let header_token = http_bearer_token(&req_ctx);
682 let override_key = req.api_key.as_deref().or(header_token.as_deref());
683 let args_hash = guard::args_short_hash(&req);
684 tracing::warn!(
685 target: futu_auth::audit::TARGET,
686 iface = "mcp",
687 endpoint = "futu_cancel_order",
688 env = %req.env,
689 market = %req.market,
690 acc_id = req.acc_id,
691 order_id = req.order_id,
692 args_hash = %args_hash,
693 outcome = "request",
694 "cancel_order request received"
695 );
696 let mutation_ctx = CheckCtx {
697 market: req.market.trim().to_ascii_uppercase(),
698 symbol: String::new(),
699 order_value: None,
700 trd_side: None,
701 };
702 if let Some(rej) = self.require_trading(
703 "futu_cancel_order",
704 &req.env,
705 Some(mutation_ctx),
706 override_key,
707 ) {
708 return rej;
709 }
710 let client = match self.client_or_err().await {
711 Ok(c) => c,
712 Err(e) => return e,
713 };
714 let result = Self::wrap(
715 handlers::trade_write::cancel_order(
716 &client,
717 &req.env,
718 req.acc_id,
719 &req.market,
720 req.order_id,
721 )
722 .await,
723 );
724 guard::emit_trade_outcome(
725 "futu_cancel_order",
726 self.current_key_id(override_key).as_deref(),
727 &args_hash,
728 &result,
729 );
730 result
731 }
732}