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::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
5
6use crate::guard;
7use crate::handlers;
8use crate::tool_args::{ComboOrderProtoJsonReq, ProtoJsonReq};
9use crate::tool_auth::{http_bearer_token, outcome_key_id_from_snapshot};
10
11use super::FutuServer;
12
13impl FutuServer {
14    async fn futu_get_option_quote_impl(
15        &self,
16        Parameters(req): Parameters<ProtoJsonReq>,
17        req_ctx: RequestContext<RoleServer>,
18    ) -> std::result::Result<String, String> {
19        let c2s = match handlers::proto_json::parse_c2s_json::<futu_proto::qot_get_option_quote::C2s>(
20            "option-quote",
21            &req.c2s_json,
22        ) {
23            Ok(c2s) => c2s,
24            Err(err) => return Self::tool_err(err),
25        };
26        let client = self
27            .read_client_or_err(
28                "futu_get_option_quote",
29                &req_ctx,
30                req.api_key.as_deref(),
31                None,
32            )
33            .await?;
34        Self::wrap_result(handlers::proto_json::option_quote(&client, c2s).await)
35    }
36
37    async fn futu_get_option_strategy_impl(
38        &self,
39        Parameters(req): Parameters<ProtoJsonReq>,
40        req_ctx: RequestContext<RoleServer>,
41    ) -> std::result::Result<String, String> {
42        let c2s = match handlers::proto_json::parse_c2s_json::<
43            futu_proto::qot_get_option_strategy::C2s,
44        >("option-strategy", &req.c2s_json)
45        {
46            Ok(c2s) => c2s,
47            Err(err) => return Self::tool_err(err),
48        };
49        let client = self
50            .read_client_or_err(
51                "futu_get_option_strategy",
52                &req_ctx,
53                req.api_key.as_deref(),
54                None,
55            )
56            .await?;
57        Self::wrap_result(handlers::proto_json::option_strategy(&client, c2s).await)
58    }
59
60    async fn futu_get_option_strategy_analysis_impl(
61        &self,
62        Parameters(req): Parameters<ProtoJsonReq>,
63        req_ctx: RequestContext<RoleServer>,
64    ) -> std::result::Result<String, String> {
65        let c2s = match handlers::proto_json::parse_c2s_json::<
66            futu_proto::qot_get_option_strategy_analysis::C2s,
67        >("option-strategy-analysis", &req.c2s_json)
68        {
69            Ok(c2s) => c2s,
70            Err(err) => return Self::tool_err(err),
71        };
72        let client = self
73            .read_client_or_err(
74                "futu_get_option_strategy_analysis",
75                &req_ctx,
76                req.api_key.as_deref(),
77                None,
78            )
79            .await?;
80        Self::wrap_result(handlers::proto_json::option_strategy_analysis(&client, c2s).await)
81    }
82
83    async fn futu_get_option_strategy_spread_impl(
84        &self,
85        Parameters(req): Parameters<ProtoJsonReq>,
86        req_ctx: RequestContext<RoleServer>,
87    ) -> std::result::Result<String, String> {
88        let c2s = match handlers::proto_json::parse_c2s_json::<
89            futu_proto::qot_get_option_strategy_spread::C2s,
90        >("option-strategy-spread", &req.c2s_json)
91        {
92            Ok(c2s) => c2s,
93            Err(err) => return Self::tool_err(err),
94        };
95        let client = self
96            .read_client_or_err(
97                "futu_get_option_strategy_spread",
98                &req_ctx,
99                req.api_key.as_deref(),
100                None,
101            )
102            .await?;
103        Self::wrap_result(handlers::proto_json::option_strategy_spread(&client, c2s).await)
104    }
105
106    async fn futu_get_combo_max_trd_qtys_impl(
107        &self,
108        Parameters(req): Parameters<ProtoJsonReq>,
109        req_ctx: RequestContext<RoleServer>,
110    ) -> std::result::Result<String, String> {
111        let c2s = match handlers::proto_json::parse_combo_max_c2s_json(&req.c2s_json) {
112            Ok(c2s) => c2s,
113            Err(err) => return Self::tool_err(err),
114        };
115        let acc_id = match handlers::proto_json::combo_max_context(&c2s) {
116            Ok(acc_id) => acc_id,
117            Err(err) => return Self::tool_err(err),
118        };
119        let client = self
120            .read_client_or_err(
121                "futu_get_combo_max_trd_qtys",
122                &req_ctx,
123                req.api_key.as_deref(),
124                Some(acc_id),
125            )
126            .await?;
127        Self::wrap_result(handlers::proto_json::combo_max_trd_qtys(&client, c2s).await)
128    }
129
130    async fn futu_place_combo_order_impl(
131        &self,
132        Parameters(req): Parameters<ComboOrderProtoJsonReq>,
133        req_ctx: RequestContext<RoleServer>,
134    ) -> std::result::Result<String, String> {
135        let header_token = http_bearer_token(&req_ctx);
136        let override_key = req.api_key.as_deref().or(header_token.as_deref());
137        let args_hash = guard::args_short_hash(&req);
138
139        let c2s = match handlers::proto_json::parse_place_combo_c2s_json(&req.c2s_json) {
140            Ok(c2s) => c2s,
141            Err(err) => return Self::tool_err(err),
142        };
143        let combo_ctx = match handlers::proto_json::place_combo_context(&c2s) {
144            Ok(ctx) => ctx,
145            Err(err) => return Self::tool_err(err),
146        };
147
148        tracing::warn!(
149            target: futu_auth::audit::TARGET,
150            iface = "mcp",
151            endpoint = "futu_place_combo_order",
152            env = %combo_ctx.env,
153            market = %combo_ctx.market,
154            acc_id = combo_ctx.acc_id,
155            order_value = crate::state::audit_fmt::opt_f64(combo_ctx.order_value),
156            args_hash = %args_hash,
157            outcome = "request",
158            "place_combo_order request received"
159        );
160
161        let caller_key_rec =
162            match self.require_caller_key_strict("futu_place_combo_order", override_key) {
163                Ok(rec) => rec,
164                Err(reject_json) => return Err(reject_json),
165            };
166        if let Some(reject) = self.require_trading_scope_only(
167            "futu_place_combo_order",
168            combo_ctx.env,
169            caller_key_rec.as_ref(),
170        ) {
171            return Err(reject);
172        }
173
174        let ctx = CheckCtx {
175            market: combo_ctx.market.clone(),
176            symbol: String::new(),
177            order_value: combo_ctx.order_value,
178            trd_side: None,
179            acc_id: Some(combo_ctx.acc_id),
180            mutation_no_exposure: false,
181            currency: None,
182        };
183        if let Some(reject) = self.require_trading(
184            "futu_place_combo_order",
185            combo_ctx.env,
186            Some(ctx),
187            override_key,
188        ) {
189            return Err(reject);
190        }
191
192        let client = self.client_or_err().await?;
193        let result = Self::wrap_result(
194            handlers::proto_json::place_combo_order(&client, c2s, req.idempotency_key).await,
195        );
196        let startup_key = self.state.authed_key();
197        let outcome_key_id =
198            outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
199        guard::emit_trade_outcome(
200            "futu_place_combo_order",
201            outcome_key_id,
202            &args_hash,
203            Self::result_as_str(&result),
204        );
205        result
206    }
207}
208
209include!(concat!(env!("OUT_DIR"), "/generated_mcp_routes_combo.rs"));