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