Skip to main content

futu_mcp/tools/
combo.rs

1//! MCP proto-JSON tools for v10.7 combo-option endpoints.
2
3use futu_auth::CheckCtx;
4use rmcp::{
5    RoleServer, handler::server::wrapper::Parameters, service::RequestContext, tool, tool_router,
6};
7
8use crate::guard;
9use crate::handlers;
10use crate::tool_args::{ComboOrderProtoJsonReq, ProtoJsonReq};
11use crate::tool_auth::{http_bearer_token, outcome_key_id_from_snapshot};
12
13use super::FutuServer;
14
15#[tool_router(router = combo_tool_router, vis = "pub(crate)")]
16impl FutuServer {
17    #[tool(
18        description = "Get option quote data for one option leg or a combo-option leg set. Futu API v10.7: OpenQuoteContext.get_option_quote. `c2s_json` must be generated-proto JSON."
19    )]
20    async fn futu_get_option_quote(
21        &self,
22        Parameters(req): Parameters<ProtoJsonReq>,
23        req_ctx: RequestContext<RoleServer>,
24    ) -> std::result::Result<String, String> {
25        let c2s = match handlers::proto_json::parse_c2s_json::<futu_proto::qot_get_option_quote::C2s>(
26            "option-quote",
27            &req.c2s_json,
28        ) {
29            Ok(c2s) => c2s,
30            Err(err) => return Self::tool_err(err),
31        };
32        let client = self
33            .read_client_or_err(
34                "futu_get_option_quote",
35                &req_ctx,
36                req.api_key.as_deref(),
37                None,
38            )
39            .await?;
40        Self::wrap_result(handlers::proto_json::option_quote(&client, c2s).await)
41    }
42
43    #[tool(
44        description = "Get option strategy candidates for an underlying security. Futu API v10.7: OpenQuoteContext.get_option_strategy. `c2s_json` must be generated-proto JSON."
45    )]
46    async fn futu_get_option_strategy(
47        &self,
48        Parameters(req): Parameters<ProtoJsonReq>,
49        req_ctx: RequestContext<RoleServer>,
50    ) -> std::result::Result<String, String> {
51        let c2s = match handlers::proto_json::parse_c2s_json::<
52            futu_proto::qot_get_option_strategy::C2s,
53        >("option-strategy", &req.c2s_json)
54        {
55            Ok(c2s) => c2s,
56            Err(err) => return Self::tool_err(err),
57        };
58        let client = self
59            .read_client_or_err(
60                "futu_get_option_strategy",
61                &req_ctx,
62                req.api_key.as_deref(),
63                None,
64            )
65            .await?;
66        Self::wrap_result(handlers::proto_json::option_strategy(&client, c2s).await)
67    }
68
69    #[tool(
70        description = "Analyze payoff/risk metrics for a combo-option leg set. Futu API v10.7: OpenQuoteContext.get_option_strategy_analysis. `c2s_json` must be generated-proto JSON."
71    )]
72    async fn futu_get_option_strategy_analysis(
73        &self,
74        Parameters(req): Parameters<ProtoJsonReq>,
75        req_ctx: RequestContext<RoleServer>,
76    ) -> std::result::Result<String, String> {
77        let c2s = match handlers::proto_json::parse_c2s_json::<
78            futu_proto::qot_get_option_strategy_analysis::C2s,
79        >("option-strategy-analysis", &req.c2s_json)
80        {
81            Ok(c2s) => c2s,
82            Err(err) => return Self::tool_err(err),
83        };
84        let client = self
85            .read_client_or_err(
86                "futu_get_option_strategy_analysis",
87                &req_ctx,
88                req.api_key.as_deref(),
89                None,
90            )
91            .await?;
92        Self::wrap_result(handlers::proto_json::option_strategy_analysis(&client, c2s).await)
93    }
94
95    #[tool(
96        description = "Get available strike/expiry spread metadata for an option strategy. Futu API v10.7: OpenQuoteContext.get_option_strategy_spread. `c2s_json` must be generated-proto JSON."
97    )]
98    async fn futu_get_option_strategy_spread(
99        &self,
100        Parameters(req): Parameters<ProtoJsonReq>,
101        req_ctx: RequestContext<RoleServer>,
102    ) -> std::result::Result<String, String> {
103        let c2s = match handlers::proto_json::parse_c2s_json::<
104            futu_proto::qot_get_option_strategy_spread::C2s,
105        >("option-strategy-spread", &req.c2s_json)
106        {
107            Ok(c2s) => c2s,
108            Err(err) => return Self::tool_err(err),
109        };
110        let client = self
111            .read_client_or_err(
112                "futu_get_option_strategy_spread",
113                &req_ctx,
114                req.api_key.as_deref(),
115                None,
116            )
117            .await?;
118        Self::wrap_result(handlers::proto_json::option_strategy_spread(&client, c2s).await)
119    }
120
121    #[tool(
122        description = "Get maximum trade quantity for a combo-option order. Futu API v10.7: OpenSecTradeContext.get_combo_max_trd_qtys. `c2s_json` must include header.acc_id."
123    )]
124    async fn futu_get_combo_max_trd_qtys(
125        &self,
126        Parameters(req): Parameters<ProtoJsonReq>,
127        req_ctx: RequestContext<RoleServer>,
128    ) -> std::result::Result<String, String> {
129        let c2s = match handlers::proto_json::parse_combo_max_c2s_json(&req.c2s_json) {
130            Ok(c2s) => c2s,
131            Err(err) => return Self::tool_err(err),
132        };
133        let acc_id = match handlers::proto_json::combo_max_context(&c2s) {
134            Ok(acc_id) => acc_id,
135            Err(err) => return Self::tool_err(err),
136        };
137        let client = self
138            .read_client_or_err(
139                "futu_get_combo_max_trd_qtys",
140                &req_ctx,
141                req.api_key.as_deref(),
142                Some(acc_id),
143            )
144            .await?;
145        Self::wrap_result(handlers::proto_json::combo_max_trd_qtys(&client, c2s).await)
146    }
147
148    #[tool(
149        description = "⚠️ REAL MONEY when header.trd_env=1. Place a combo-option order. REQUIRES --enable-trading; real env additionally requires --allow-real-trading. `c2s_json` must include header.acc_id, header.trd_env, header.trd_market, combo_legs, qty, and order_type."
150    )]
151    async fn futu_place_combo_order(
152        &self,
153        Parameters(req): Parameters<ComboOrderProtoJsonReq>,
154        req_ctx: RequestContext<RoleServer>,
155    ) -> std::result::Result<String, String> {
156        let header_token = http_bearer_token(&req_ctx);
157        let override_key = req.api_key.as_deref().or(header_token.as_deref());
158        let args_hash = guard::args_short_hash(&req);
159
160        let c2s = match handlers::proto_json::parse_place_combo_c2s_json(&req.c2s_json) {
161            Ok(c2s) => c2s,
162            Err(err) => return Self::tool_err(err),
163        };
164        let combo_ctx = match handlers::proto_json::place_combo_context(&c2s) {
165            Ok(ctx) => ctx,
166            Err(err) => return Self::tool_err(err),
167        };
168
169        tracing::warn!(
170            target: futu_auth::audit::TARGET,
171            iface = "mcp",
172            endpoint = "futu_place_combo_order",
173            env = %combo_ctx.env,
174            market = %combo_ctx.market,
175            acc_id = combo_ctx.acc_id,
176            order_value = crate::state::audit_fmt::opt_f64(combo_ctx.order_value),
177            args_hash = %args_hash,
178            outcome = "request",
179            "place_combo_order request received"
180        );
181
182        let caller_key_rec =
183            match self.require_caller_key_strict("futu_place_combo_order", override_key) {
184                Ok(rec) => rec,
185                Err(reject_json) => return Err(reject_json),
186            };
187        if let Some(reject) = self.require_trading_scope_only(
188            "futu_place_combo_order",
189            combo_ctx.env,
190            caller_key_rec.as_ref(),
191        ) {
192            return Err(reject);
193        }
194
195        let ctx = CheckCtx {
196            market: combo_ctx.market.clone(),
197            symbol: String::new(),
198            order_value: combo_ctx.order_value,
199            trd_side: None,
200            acc_id: Some(combo_ctx.acc_id),
201            mutation_no_exposure: false,
202            currency: None,
203        };
204        if let Some(reject) = self.require_trading(
205            "futu_place_combo_order",
206            combo_ctx.env,
207            Some(ctx),
208            override_key,
209        ) {
210            return Err(reject);
211        }
212
213        let client = self.client_or_err().await?;
214        let result = Self::wrap_result(
215            handlers::proto_json::place_combo_order(&client, c2s, req.idempotency_key).await,
216        );
217        let startup_key = self.state.authed_key();
218        let outcome_key_id =
219            outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
220        guard::emit_trade_outcome(
221            "futu_place_combo_order",
222            outcome_key_id,
223            &args_hash,
224            Self::result_as_str(&result),
225        );
226        result
227    }
228}