Skip to main content

futu_mcp/tool_args/
mod.rs

1//! MCP tool request/parameter schemas.
2//!
3//! Keep serde aliases, schema descriptions, and default helpers here so
4//! `tools.rs` can stay focused on auth + tool dispatch.
5//!
6//! v1.4.110 P1-1: 拆自 1785 LoC 单文件 tool_args.rs → tool_args/{qot,trd,push}.rs.
7//! 拆分轴: handler 域 (handlers/qot, handlers/trd, push subscription).
8//! 外部 consumer 用 `use crate::tool_args::*` glob 仍能拿到所有 struct.
9
10mod push;
11mod qot;
12mod trd;
13
14pub use push::*;
15pub use qot::*;
16pub use trd::*;
17
18use rmcp::schemars;
19use serde::{Deserialize, Serialize};
20
21use crate::tool_enums::ToolEnum;
22
23/// Empty argument object for MCP tools that take no business parameters.
24/// Unknown fields are rejected instead of being silently ignored.
25#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct NoArgs {}
28
29/// Official proto C2S JSON wrapper for endpoints whose ergonomic MCP surface is
30/// intentionally kept tied to generated protobuf shape.
31#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
32#[serde(deny_unknown_fields)]
33pub struct ProtoJsonReq {
34    #[schemars(
35        description = "Official generated C2S JSON. Field names use generated proto serde snake_case, e.g. multi_legs / combo_legs / order_type."
36    )]
37    pub c2s_json: String,
38
39    #[schemars(
40        description = "Optional per-call API key plaintext. HTTP scope mode still requires a valid Bearer on every /mcp request; this field overrides that identity for the tool handler. In stdio mode: tool argument > startup key."
41    )]
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub api_key: Option<String>,
44}
45
46// ========== 通用 deserializer (供 qot/trd/push 子模块 super::* glob 引用) ==========
47
48/// v1.4.42 (external reviewer v1.4.40 报告 P3.3 修): 让 `order_type` 类字段接受 integer OR
49/// string enum。LLM agent 习惯用 string 枚举(和 PlaceOrderReq / ModifyOrderReq
50/// 一致),旧 caller 用 int 不破坏。
51///
52/// 映射(对齐 Trd_Common.OrderType):
53/// - "NORMAL" / "LIMIT" → 1
54/// - "MARKET" → 2
55/// - "ABSOLUTE_LIMIT" → 5
56/// - "AUCTION" → 6
57/// - "AUCTION_LIMIT" → 7
58/// - "SPECIAL_LIMIT" → 8
59/// - 其他 string → 尝试 parse 成 int
60fn deser_int_or_order_type_str<'de, D>(deserializer: D) -> std::result::Result<i32, D::Error>
61where
62    D: serde::Deserializer<'de>,
63{
64    #[derive(Deserialize)]
65    #[serde(untagged)]
66    enum IntOrStr {
67        Int(i32),
68        Str(String),
69    }
70    match IntOrStr::deserialize(deserializer)? {
71        IntOrStr::Int(i) => Ok(i),
72        IntOrStr::Str(s) => match s.trim().to_ascii_uppercase().as_str() {
73            "NORMAL" | "LIMIT" => Ok(1),
74            "MARKET" => Ok(2),
75            "ABSOLUTE_LIMIT" => Ok(5),
76            "AUCTION" => Ok(6),
77            "AUCTION_LIMIT" => Ok(7),
78            "SPECIAL_LIMIT" => Ok(8),
79            // fallback: 尝试 parse 成 int(用户传 "3" 字符串也能用)
80            other => other.parse::<i32>().map_err(|_| {
81                serde::de::Error::custom(format!(
82                    "unknown order_type {other:?}: expect integer or one of \
83                     NORMAL|LIMIT|MARKET|ABSOLUTE_LIMIT|AUCTION|AUCTION_LIMIT|SPECIAL_LIMIT"
84                ))
85            }),
86        },
87    }
88}
89
90/// v1.4.110: trade write `order_id` accepts either numeric FTAPI `orderID`
91/// (integer or integer string), or backend/server `orderIDEx` strings such as
92/// `FU1C8AE09C51555000`.
93///
94/// Keep the raw string so handlers can route numeric values into `order_id` and
95/// FU/FH values into `order_id_ex`, matching C++ APIServer behavior.
96fn deser_order_id_raw_from_int_or_str<'de, D>(
97    deserializer: D,
98) -> std::result::Result<String, D::Error>
99where
100    D: serde::Deserializer<'de>,
101{
102    #[derive(Deserialize)]
103    #[serde(untagged)]
104    enum IntOrStr {
105        Int(u64),
106        Str(String),
107    }
108
109    match IntOrStr::deserialize(deserializer)? {
110        IntOrStr::Int(i) => Ok(i.to_string()),
111        IntOrStr::Str(s) => {
112            let trimmed = s.trim();
113            if trimmed.is_empty() {
114                return Err(serde::de::Error::custom(
115                    "invalid order_id string: must not be empty",
116                ));
117            }
118            Ok(trimmed.to_string())
119        }
120    }
121}
122
123/// v1.4.90 P0-E: CancelAllOrderReq.market 用,接 int OR string 但允许 missing /
124/// null / 空字符串 (`#[serde(default)]` 兜底). 空字符串 → 直接返空,
125/// runtime `validate()` 报"market is required"错; 非空 string/int 走标准
126/// `deser_trd_market_as_string` 路径.
127///
128/// 为什么不复用 `deser_trd_market_as_string`: 后者要求非空且必须是合法 enum,
129/// 但本字段保留 `#[serde(default)]` 让 schema 兼容 missing field, 且 runtime
130/// 自定义 error message ("market is required").
131fn deser_trd_market_string_allow_empty<'de, D>(
132    deserializer: D,
133) -> std::result::Result<String, D::Error>
134where
135    D: serde::Deserializer<'de>,
136{
137    let opt: Option<serde_json::Value> = Option::deserialize(deserializer)?;
138    match opt {
139        None | Some(serde_json::Value::Null) => Ok(String::new()),
140        Some(serde_json::Value::String(s)) if s.trim().is_empty() => Ok(String::new()),
141        Some(v) => {
142            // delegate to TrdMarketEnum 双接 path
143            let e = match v {
144                serde_json::Value::Number(n) => {
145                    let raw = n.as_i64().ok_or_else(|| {
146                        serde::de::Error::custom(format!("trd_market number invalid: {n}"))
147                    })?;
148                    let i = i32::try_from(raw).map_err(|_| {
149                        serde::de::Error::custom(format!(
150                            "trd_market number out of i32 range: {raw}"
151                        ))
152                    })?;
153                    crate::tool_enums::TrdMarketEnum::from_i32(i).ok_or_else(|| {
154                        serde::de::Error::custom(format!(
155                            "unknown trd_market int {i}: valid = {:?}",
156                            crate::tool_enums::TrdMarketEnum::all_int_values()
157                        ))
158                    })?
159                }
160                serde_json::Value::String(s) => {
161                    let t = s.trim();
162                    crate::tool_enums::TrdMarketEnum::from_str(t)
163                        .or_else(|| {
164                            t.parse::<i32>()
165                                .ok()
166                                .and_then(crate::tool_enums::TrdMarketEnum::from_i32)
167                        })
168                        .ok_or_else(|| {
169                            serde::de::Error::custom(format!(
170                                "unknown trd_market {s:?}: valid = {:?}",
171                                crate::tool_enums::TrdMarketEnum::all_string_values()
172                            ))
173                        })?
174                }
175                _ => {
176                    return Err(serde::de::Error::custom(format!(
177                        "trd_market must be int or string, got: {v}"
178                    )));
179                }
180            };
181            // 反查 canonical 大写 String
182            let i = e.as_i32();
183            let names = crate::tool_enums::TrdMarketEnum::all_string_values();
184            let ints = crate::tool_enums::TrdMarketEnum::all_int_values();
185            let idx = ints
186                .iter()
187                .position(|&v| v == i)
188                .ok_or_else(|| serde::de::Error::custom("trd_market i32 has no canonical"))?;
189            Ok(names[idx].to_string())
190        }
191    }
192}
193
194fn default_kl_type() -> String {
195    "day".to_string()
196}
197
198fn default_depth() -> i32 {
199    10
200}
201
202fn default_ticker_count() -> i32 {
203    100
204}
205
206fn default_plate_set() -> String {
207    "all".to_string()
208}
209
210fn default_env() -> String {
211    "real".to_string()
212}
213
214fn default_env_simulate() -> String {
215    "simulate".to_string()
216}
217
218fn default_order_type() -> String {
219    "NORMAL".to_string()
220}
221
222fn default_modify_op() -> String {
223    "NORMAL".to_string()
224}
225
226fn default_true() -> bool {
227    true
228}
229
230fn default_rehab_none() -> String {
231    "none".to_string()
232}
233
234/// v1.4.106 codex 0635 ζ36 F5: history-kline 省略 max_count 时 default 1000.
235/// 与 schema description "default 1000" 一致, 防 LLM context balloon.
236fn default_history_kline_max_count() -> Option<i32> {
237    Some(1000)
238}
239
240fn default_reference_type() -> String {
241    // v1.4.41: default 从 "option" 改成 "warrant"(真支持的值)
242    "warrant".to_string()
243}
244
245fn default_warrant_num() -> i32 {
246    20
247}
248
249fn default_user_security_group_type() -> i32 {
250    1
251}
252
253fn default_stock_filter_num() -> i32 {
254    50
255}
256
257fn default_is_first_push() -> bool {
258    true
259}
260
261fn default_is_reg_push() -> bool {
262    true
263}