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