1use futu_auth::CheckCtx;
4use rmcp::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
5
6use crate::guard;
7use crate::handlers;
8use crate::tool_args::*;
9use crate::tool_auth::{http_bearer_token, outcome_key_id_from_snapshot};
10
11use super::FutuServer;
12
13fn place_order_check_ctx(req: &PlaceOrderReq, resolved_acc_id: u64) -> CheckCtx {
14 let market = req.market.trim().to_ascii_uppercase();
15 let price_times_qty = req.price.map(|price| price * req.qty);
16 let order_value = if futu_core::trade_market::parse_trd_market_id(&market)
17 == Some(futu_core::trade_market::trd_market_id::PREDICTION)
18 {
19 req.amount.or(price_times_qty)
20 } else {
21 price_times_qty
22 };
23 CheckCtx {
24 symbol: format!("{}.{}", market, req.code.trim()),
25 market,
26 order_value,
27 trd_side: Some(req.side.trim().to_ascii_uppercase()),
28 acc_id: Some(resolved_acc_id),
29 mutation_no_exposure: false,
30 currency: None,
31 }
32}
33
34impl FutuServer {
35 fn require_place_order_limit(
38 &self,
39 req: &PlaceOrderReq,
40 resolved_acc_id: u64,
41 override_key: Option<&str>,
42 ) -> Option<String> {
43 self.require_trading(
44 "futu_place_order",
45 &req.env,
46 Some(place_order_check_ctx(req, resolved_acc_id)),
47 override_key,
48 )
49 }
50
51 async fn futu_place_order_impl(
52 &self,
53 Parameters(req): Parameters<PlaceOrderReq>,
54 req_ctx: RequestContext<RoleServer>,
55 ) -> std::result::Result<String, String> {
56 let header_token = http_bearer_token(&req_ctx);
58 let override_key = req.api_key.as_deref().or(header_token.as_deref());
59 let args_hash = guard::args_short_hash(&req);
60 tracing::warn!(
61 target: futu_auth::audit::TARGET,
62 iface = "mcp",
63 endpoint = "futu_place_order",
64 env = %req.env,
65 market = %req.market,
66 acc_id = req.acc_id.unwrap_or(0), card_num_provided = req.card_num.is_some(),
68 side = %req.side,
69 order_type = %req.order_type,
70 code = %req.code,
71 qty = req.qty,
72 price = crate::state::audit_fmt::opt_f64(req.price),
75 time_in_force = req.time_in_force.unwrap_or(-1),
76 fill_outside_rth = req.fill_outside_rth.unwrap_or(false),
77 session = req.session.unwrap_or(-1),
78 args_hash = %args_hash,
79 outcome = "request",
80 "place_order request received"
81 );
82 req.validate()?;
83 let caller_key_rec = match self.require_caller_key_strict("futu_place_order", override_key)
87 {
88 Ok(rec) => rec,
89 Err(reject_json) => return Err(reject_json),
90 };
91 if let Some(reject) =
96 self.require_trading_scope_only("futu_place_order", &req.env, caller_key_rec.as_ref())
97 {
98 return Err(reject);
99 }
100 let client = self.client_or_err().await?;
105 let allowed_card_nums = caller_key_rec
111 .as_ref()
112 .and_then(|r| r.allowed_card_nums.as_deref());
113 let caller_allowed_acc_ids = caller_key_rec
117 .as_ref()
118 .and_then(|r| r.allowed_acc_ids.as_ref());
119 let resolved_acc_id = match handlers::trade_write::resolve_acc_id_with_card_num(
120 &client,
121 req.acc_id.unwrap_or(0),
122 req.card_num.as_deref(),
123 allowed_card_nums,
124 caller_allowed_acc_ids,
125 )
126 .await
127 {
128 Ok(id) => id,
129 Err(msg) => return Self::tool_err(msg),
130 };
131 if let Some(rej) = self.require_place_order_limit(&req, resolved_acc_id, override_key) {
132 return Err(rej);
134 }
135 let result = Self::wrap_result(
136 handlers::trade_write::place_order(
137 &client,
138 handlers::trade_write::PlaceOrderInput {
139 env: &req.env,
140 acc_id: resolved_acc_id,
141 market: &req.market,
142 side: &req.side,
143 order_type: &req.order_type,
144 code: &req.code,
145 qty: req.qty,
146 price: req.price,
147 amount: req.amount,
148 pred_side: req.pred_side,
149 time_in_force: req.time_in_force,
150 fill_outside_rth: req.fill_outside_rth,
151 session: req.session,
152 expire_time: req.expire_time.as_deref(),
153 jp_acc_type: req.jp_acc_type,
154 idempotency_key: req.idempotency_key.clone(),
155 stop_price: req.stop_price,
157 trail_type: req.trail_type,
158 trail_value: req.trail_value,
159 trail_spread: req.trail_spread,
160 },
161 )
162 .await,
163 );
164 let startup_key = self.state.authed_key();
171 let outcome_key_id =
172 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
173 guard::emit_trade_outcome(
174 "futu_place_order",
175 outcome_key_id,
176 &args_hash,
177 Self::result_as_str(&result),
178 );
179 result
180 }
181
182 async fn futu_modify_order_impl(
183 &self,
184 Parameters(req): Parameters<ModifyOrderReq>,
185 req_ctx: RequestContext<RoleServer>,
186 ) -> std::result::Result<String, String> {
187 let header_token = http_bearer_token(&req_ctx);
188 let override_key = req.api_key.as_deref().or(header_token.as_deref());
189 let args_hash = guard::args_short_hash(&req);
190 tracing::warn!(
191 target: futu_auth::audit::TARGET,
192 iface = "mcp",
193 endpoint = "futu_modify_order",
194 env = %req.env,
195 market = %req.market,
196 acc_id = req.acc_id.unwrap_or(0), card_num_provided = req.card_num.is_some(),
198 order_id = %req.order_id,
199 op = %req.op,
200 qty = crate::state::audit_fmt::opt_f64(req.qty),
202 price = crate::state::audit_fmt::opt_f64(req.price),
203 args_hash = %args_hash,
204 outcome = "request",
205 "modify_order request received"
206 );
207 req.validate()?;
208 let caller_key_rec = match self.require_caller_key_strict("futu_modify_order", override_key)
210 {
211 Ok(rec) => rec,
212 Err(reject_json) => return Err(reject_json),
213 };
214 if let Some(reject) =
216 self.require_trading_scope_only("futu_modify_order", &req.env, caller_key_rec.as_ref())
217 {
218 return Err(reject);
219 }
220 let client = self.client_or_err().await?;
222 let allowed_card_nums = caller_key_rec
228 .as_ref()
229 .and_then(|r| r.allowed_card_nums.as_deref());
230 let caller_allowed_acc_ids = caller_key_rec
233 .as_ref()
234 .and_then(|r| r.allowed_acc_ids.as_ref());
235 let resolved_acc_id = match handlers::trade_write::resolve_acc_id_with_card_num(
236 &client,
237 req.acc_id.unwrap_or(0),
238 req.card_num.as_deref(),
239 allowed_card_nums,
240 caller_allowed_acc_ids,
241 )
242 .await
243 {
244 Ok(id) => id,
245 Err(msg) => return Self::tool_err(msg),
246 };
247 let mutation_ctx = CheckCtx {
251 market: req.market.trim().to_ascii_uppercase(),
252 symbol: String::new(),
253 order_value: None,
254 trd_side: None,
255 acc_id: Some(resolved_acc_id), mutation_no_exposure: false,
257 currency: None,
258 };
259 if let Some(rej) = self.require_trading(
260 "futu_modify_order",
261 &req.env,
262 Some(mutation_ctx),
263 override_key,
264 ) {
265 return Err(rej);
267 }
268 let result = Self::wrap_result(
269 handlers::trade_write::modify_order(
270 &client,
271 handlers::trade_write::ModifyOrderInput {
272 env: &req.env,
273 acc_id: resolved_acc_id,
274 market: &req.market,
275 order_id: &req.order_id,
276 op: &req.op,
277 qty: req.qty,
278 price: req.price,
279 jp_acc_type: req.jp_acc_type,
280 idempotency_key: req.idempotency_key.clone(),
281 },
282 )
283 .await,
284 );
285 let startup_key = self.state.authed_key();
287 let outcome_key_id =
288 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
289 guard::emit_trade_outcome(
290 "futu_modify_order",
291 outcome_key_id,
292 &args_hash,
293 Self::result_as_str(&result),
294 );
295 result
296 }
297
298 async fn futu_cancel_order_impl(
299 &self,
300 Parameters(req): Parameters<CancelOrderReq>,
301 req_ctx: RequestContext<RoleServer>,
302 ) -> std::result::Result<String, String> {
303 let header_token = http_bearer_token(&req_ctx);
304 let override_key = req.api_key.as_deref().or(header_token.as_deref());
305 let args_hash = guard::args_short_hash(&req);
306 tracing::warn!(
307 target: futu_auth::audit::TARGET,
308 iface = "mcp",
309 endpoint = "futu_cancel_order",
310 env = %req.env,
311 market = %req.market,
312 acc_id = req.acc_id.unwrap_or(0), card_num_provided = req.card_num.is_some(),
314 order_id = %req.order_id,
315 args_hash = %args_hash,
316 outcome = "request",
317 "cancel_order request received"
318 );
319 let caller_key_rec = match self.require_caller_key_strict("futu_cancel_order", override_key)
321 {
322 Ok(rec) => rec,
323 Err(reject_json) => return Err(reject_json),
324 };
325 if let Some(reject) =
327 self.require_trading_scope_only("futu_cancel_order", &req.env, caller_key_rec.as_ref())
328 {
329 return Err(reject);
330 }
331 let client = self.client_or_err().await?;
333 let allowed_card_nums = caller_key_rec
339 .as_ref()
340 .and_then(|r| r.allowed_card_nums.as_deref());
341 let caller_allowed_acc_ids = caller_key_rec
344 .as_ref()
345 .and_then(|r| r.allowed_acc_ids.as_ref());
346 let resolved_acc_id = match handlers::trade_write::resolve_acc_id_with_card_num(
347 &client,
348 req.acc_id.unwrap_or(0),
349 req.card_num.as_deref(),
350 allowed_card_nums,
351 caller_allowed_acc_ids,
352 )
353 .await
354 {
355 Ok(id) => id,
356 Err(msg) => return Self::tool_err(msg),
357 };
358 let mutation_ctx = CheckCtx {
359 market: req.market.trim().to_ascii_uppercase(),
360 symbol: String::new(),
361 order_value: None,
362 trd_side: None,
363 acc_id: Some(resolved_acc_id), mutation_no_exposure: false,
365 currency: None,
366 };
367 if let Some(rej) = self.require_trading(
368 "futu_cancel_order",
369 &req.env,
370 Some(mutation_ctx),
371 override_key,
372 ) {
373 return Err(rej);
375 }
376 let result = Self::wrap_result(
377 handlers::trade_write::cancel_order(
378 &client,
379 &req.env,
380 resolved_acc_id,
381 &req.market,
382 &req.order_id,
383 req.jp_acc_type,
384 req.idempotency_key.clone(),
385 )
386 .await,
387 );
388 let startup_key = self.state.authed_key();
390 let outcome_key_id =
391 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
392 guard::emit_trade_outcome(
393 "futu_cancel_order",
394 outcome_key_id,
395 &args_hash,
396 Self::result_as_str(&result),
397 );
398 result
399 }
400
401 async fn futu_reconfirm_order_impl(
402 &self,
403 Parameters(req): Parameters<ReconfirmOrderReq>,
404 req_ctx: RequestContext<RoleServer>,
405 ) -> std::result::Result<String, String> {
406 let header_token = http_bearer_token(&req_ctx);
407 let override_key = req.api_key.as_deref().or(header_token.as_deref());
408 let args_hash = guard::args_short_hash(&req);
409 tracing::warn!(
410 target: futu_auth::audit::TARGET,
411 iface = "mcp",
412 endpoint = "futu_reconfirm_order",
413 env = %req.env,
414 market = %req.market,
415 acc_id = req.acc_id.unwrap_or(0),
416 card_num_provided = req.card_num.is_some(),
417 order_id = %req.order_id,
418 reason = req.reason,
419 args_hash = %args_hash,
420 outcome = "request",
421 "reconfirm_order request received"
422 );
423 let caller_key_rec =
424 match self.require_caller_key_strict("futu_reconfirm_order", override_key) {
425 Ok(rec) => rec,
426 Err(reject_json) => return Err(reject_json),
427 };
428 if let Some(reject) = self.require_trading_scope_only(
429 "futu_reconfirm_order",
430 &req.env,
431 caller_key_rec.as_ref(),
432 ) {
433 return Err(reject);
434 }
435 let client = self.client_or_err().await?;
436 let allowed_card_nums = caller_key_rec
437 .as_ref()
438 .and_then(|r| r.allowed_card_nums.as_deref());
439 let caller_allowed_acc_ids = caller_key_rec
440 .as_ref()
441 .and_then(|r| r.allowed_acc_ids.as_ref());
442 let resolved_acc_id = match handlers::trade_write::resolve_acc_id_with_card_num(
443 &client,
444 req.acc_id.unwrap_or(0),
445 req.card_num.as_deref(),
446 allowed_card_nums,
447 caller_allowed_acc_ids,
448 )
449 .await
450 {
451 Ok(id) => id,
452 Err(msg) => return Self::tool_err(msg),
453 };
454 let mutation_ctx = CheckCtx {
455 market: req.market.trim().to_ascii_uppercase(),
456 symbol: String::new(),
457 order_value: None,
458 trd_side: None,
459 acc_id: Some(resolved_acc_id),
460 mutation_no_exposure: false,
461 currency: None,
462 };
463 if let Some(rej) = self.require_trading(
464 "futu_reconfirm_order",
465 &req.env,
466 Some(mutation_ctx),
467 override_key,
468 ) {
469 return Err(rej);
470 }
471 let result = Self::wrap_result(
472 handlers::trade_write::reconfirm_order(
473 &client,
474 handlers::trade_write::ReconfirmOrderInput {
475 env: &req.env,
476 acc_id: resolved_acc_id,
477 market: &req.market,
478 order_id: &req.order_id,
479 reason: req.reason,
480 jp_acc_type: req.jp_acc_type,
481 },
482 )
483 .await,
484 );
485 let startup_key = self.state.authed_key();
486 let outcome_key_id =
487 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
488 guard::emit_trade_outcome(
489 "futu_reconfirm_order",
490 outcome_key_id,
491 &args_hash,
492 Self::result_as_str(&result),
493 );
494 result
495 }
496
497 async fn futu_cancel_all_order_impl(
498 &self,
499 Parameters(req): Parameters<CancelAllOrderReq>,
500 req_ctx: RequestContext<RoleServer>,
501 ) -> std::result::Result<String, String> {
502 let header_token = http_bearer_token(&req_ctx);
503 let override_key = req.api_key.as_deref().or(header_token.as_deref());
504 let args_hash = guard::args_short_hash(&req);
505 tracing::warn!(
506 target: futu_auth::audit::TARGET,
507 iface = "mcp",
508 endpoint = "futu_cancel_all_order",
509 env = %req.env,
510 market = %req.market,
511 acc_id = req.acc_id,
512 args_hash = %args_hash,
513 outcome = "request",
514 "cancel_all_order request received"
515 );
516 req.validate()?;
521 let caller_key_rec =
526 match self.require_caller_key_strict("futu_cancel_all_order", override_key) {
527 Ok(rec) => rec,
528 Err(reject_json) => return Err(reject_json),
529 };
530 let market_trimmed = req.market.trim();
531 let mutation_ctx = CheckCtx {
532 market: market_trimmed.to_ascii_uppercase(),
533 symbol: String::new(),
534 order_value: None,
535 trd_side: None,
536 acc_id: Some(req.acc_id), mutation_no_exposure: false,
538 currency: None,
539 };
540 if let Some(rej) = self.require_trading(
541 "futu_cancel_all_order",
542 &req.env,
543 Some(mutation_ctx),
544 override_key,
545 ) {
546 return Err(rej);
548 }
549 let client = self.client_or_err().await?;
550 let result = Self::wrap_result(
551 handlers::trade_write::cancel_all_order(&client, &req.env, req.acc_id, &req.market)
552 .await,
553 );
554 let startup_key = self.state.authed_key();
556 let outcome_key_id =
557 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
558 guard::emit_trade_outcome(
559 "futu_cancel_all_order",
560 outcome_key_id,
561 &args_hash,
562 Self::result_as_str(&result),
563 );
564 result
565 }
566}
567
568#[cfg(test)]
569#[path = "trade_write/tests.rs"]
570mod tests;
571
572include!(concat!(
573 env!("OUT_DIR"),
574 "/generated_mcp_routes_trade_write.rs"
575));