Skip to main content

futu_mcp/tool_enums/
mod.rs

1//! v1.4.84 §5 B2: 结构化 MCP tool enum 类型 + 泛型 int/string 双接 deserializer
2//!
3//! **背景**: v1.4.83 在 `tools.rs` 加了 `deser_int_or_order_type_str` 单一
4//! deserializer,让 OrderType 字段接受 int OR string enum。本模块把该 pattern
5//! **泛化**到 5 个 enum 类型:
6//!
7//! | Enum | proto | variants |
8//! |---|---|---|
9//! | [`MarketEnum`] | `Qot_Common.QotMarket` | 11 |
10//! | [`SubTypeEnum`] | `Qot_Common.SubType` | 17 |
11//! | [`OrderTypeEnum`] | `Trd_Common.OrderType` | 19 (含 v1.4.53 条件单) |
12//! | [`PriceReminderOpEnum`] | `Qot_SetPriceReminder.SetPriceReminderOp` | 6 |
13//!
14//! **设计要点**:
15//!
16//! - 所有 enum impl [`ToolEnum`] trait,统一 `from_i32 / from_str / as_i32 /
17//!   all_int_values / all_string_values` 接口
18//! - [`deser_int_or_enum_str`] 泛型 deserializer:`T: ToolEnum`,接受 JSON
19//!   integer OR string
20//! - Serialize 为 i32(backward-compat,旧 int-based 客户端不破坏)
21//! - String 名对齐 proto 枚举 suffix(`OrderBook` / `KL_Day` / `NORMAL` 等),
22//!   方便 LLM agent 对照 proto docs;同时接受合理 alias
23//!
24//! **当前接入状态**:
25//!
26//! 现有 MCP tool arg structs 已在 `tool_args/` 下按需接入这些 wrapper:
27//! `market` / `sub_type_list` / `trd_market` / `order_type` 等字段可接受
28//! int 或 string。handler runtime parser 也已完成主市场与高级订单类型收敛;
29//! fund market 仅在 read/view-only endpoint 暴露,trade-write runtime 会 fail closed。
30//!
31//! # dead_code allow 原因
32//!
33//! 部分 wrapper 只被特定 feature/test matrix 间接触发;保留集中 enum module
34//! 能让 schema/runtime drift guard 在一个位置覆盖所有 MCP enum parser。
35//!
36use serde::Deserialize;
37
38// Split: 为 tests.rs / 旧 mod.rs scope 提供的 alias
39// (cargo fix 误删过, #[allow] 防再删 — pitfall #65)
40#[allow(unused_imports)]
41use futu_proto::trd_common::OrderType as ProtoOrderType;
42#[allow(unused_imports)]
43use futu_proto::trd_common::TrdMarket as ProtoTrdMarket;
44
45// v1.4.93 C3 (Option D): runtime-delegate to prost-generated `from_str_name` so
46// that canonical proto names (e.g. `"TrdMarket_HK"` / `"OrderType_Stop"`) are
47// always accepted without re-listing them in hand-written `from_str` match arms.
48// Hand-written arms still cover **short-name aliases** (`"HK"` / `"MIT"`) that
49// LLM agents naturally produce. See pitfall #54 and the `option_d_*` tests.
50
51// ========== ToolEnum trait ==========
52
53/// v1.4.84 §5 B2: 统一 MCP tool enum 接口,让 [`deser_int_or_enum_str`] 可泛型化。
54pub trait ToolEnum: Sized + Copy {
55    /// 用于 error message 的人类可读类型名(如 `"market"` / `"order_type"`)
56    fn type_name() -> &'static str;
57
58    /// 按 proto int 值查 enum variant;未定义 → None
59    fn from_i32(v: i32) -> Option<Self>;
60
61    /// 按 string 名 / 别名查 enum variant;未定义 → None
62    ///
63    /// 实装应 trim + 大小写灵活匹配(canonical 名 + 常见 alias)
64    fn from_str(s: &str) -> Option<Self>;
65
66    /// 返 proto int 值(用于 serialize / handler 下游)
67    fn as_i32(self) -> i32;
68
69    /// 列举所有合法 int 值(用于 error message)
70    fn all_int_values() -> Vec<i32>;
71
72    /// 列举所有 canonical string 名(用于 error message / schema hints)
73    fn all_string_values() -> Vec<&'static str>;
74}
75
76// ========== 泛型双接 deserializer ==========
77
78/// v1.4.84 §5 B2: 泛型 int-or-string enum deserializer。
79///
80/// 对齐 v1.4.83 `tools.rs::deser_int_or_order_type_str` pattern,泛化到任意
81/// 实装 [`ToolEnum`] 的类型。
82///
83/// # Examples
84///
85/// ```ignore
86/// use crate::tool_enums::{MarketEnum, deser_int_or_enum_str};
87///
88/// #[derive(serde::Deserialize)]
89/// struct MyReq {
90///     #[serde(deserialize_with = "deser_int_or_enum_str::<MarketEnum>")]
91///     pub market: MarketEnum,
92/// }
93/// ```
94pub fn deser_int_or_enum_str<'de, D, E>(deserializer: D) -> Result<E, D::Error>
95where
96    D: serde::Deserializer<'de>,
97    E: ToolEnum,
98{
99    #[derive(Deserialize)]
100    #[serde(untagged)]
101    enum IntOrStr {
102        Int(i32),
103        Str(String),
104    }
105    match IntOrStr::deserialize(deserializer)? {
106        IntOrStr::Int(i) => E::from_i32(i).ok_or_else(|| {
107            serde::de::Error::custom(format!(
108                "unknown {} int value {i}: valid = {:?}",
109                E::type_name(),
110                E::all_int_values()
111            ))
112        }),
113        IntOrStr::Str(s) => {
114            // 先 trim,再 try enum lookup;失败 fallback 尝试 parse 成 int
115            let trimmed = s.trim();
116            if let Some(e) = E::from_str(trimmed) {
117                return Ok(e);
118            }
119            // fallback: 用户传 "1" 字符串也能用(和 v1.4.83 老 pattern 对齐)
120            if let Ok(i) = trimmed.parse::<i32>()
121                && let Some(e) = E::from_i32(i)
122            {
123                return Ok(e);
124            }
125            Err(serde::de::Error::custom(format!(
126                "unknown {} string {s:?}: valid = {:?} or integer {:?}",
127                E::type_name(),
128                E::all_string_values(),
129                E::all_int_values()
130            )))
131        }
132    }
133}
134
135// ========== MarketEnum (Qot_Common.QotMarket) ==========
136//
137// 对齐 `Qot_Common.QotMarket`,11 variants(跳 Unknown=0 / 废弃 HK_Future=2).
138//
139// String → variant 映射(见 `from_str` impl):
140//   HK=1 / US=11 / SH=21 (aka "CN") / SZ=22 / SG=31 / JP=41 / AU=51 /
141//   MY=61 / CA=71 / FX=81.
142//
143// **注**: "CN" 映射到 SH=21(沪股)是因为 proto 没有单一 CN 市场 id,
144// SH 是第一个 CN variant. 用户需要 SZ 应显式传 "SZ".
145//
146// ----------------------------------------------------------------------------
147// v1.4.84 §5 B2 field migration wrappers (见下方 deser_*_as_i32):
148//   tools.rs 里类型固定 i32 / Vec<i32> 的字段加 #[serde(deserialize_with =
149//   "tool_enums::deser_market_as_i32")] 即可接受 int OR string form.
150
151/// v1.4.84 §5 B2: MarketEnum 双接 (i32 or string) → i32.
152pub fn deser_market_as_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
153where
154    D: serde::Deserializer<'de>,
155{
156    let e: MarketEnum = deser_int_or_enum_str(deserializer)?;
157    Ok(e.as_i32())
158}
159
160/// v1.4.84 §5 B2: Optional<MarketEnum> 双接 → Option<i32>.
161pub fn deser_market_as_option_i32<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
162where
163    D: serde::Deserializer<'de>,
164{
165    let opt: Option<serde_json::Value> = Option::deserialize(deserializer)?;
166    match opt {
167        None | Some(serde_json::Value::Null) => Ok(None),
168        Some(v) => {
169            // 手工分 int/string 处理 (不能直接 deser_int_or_enum_str 因为
170            // Value 已经 parse 过).
171            let e: MarketEnum = match v {
172                serde_json::Value::Number(n) => {
173                    let i = n.as_i64().ok_or_else(|| {
174                        serde::de::Error::custom(format!("market number invalid: {n}"))
175                    })? as i32;
176                    MarketEnum::from_i32(i).ok_or_else(|| {
177                        serde::de::Error::custom(format!(
178                            "unknown market int {i}: valid = {:?}",
179                            MarketEnum::all_int_values()
180                        ))
181                    })?
182                }
183                serde_json::Value::String(s) => {
184                    let t = s.trim();
185                    MarketEnum::from_str(t)
186                        .or_else(|| t.parse::<i32>().ok().and_then(MarketEnum::from_i32))
187                        .ok_or_else(|| {
188                            serde::de::Error::custom(format!(
189                                "unknown market {s:?}: valid = {:?}",
190                                MarketEnum::all_string_values()
191                            ))
192                        })?
193                }
194                _ => {
195                    return Err(serde::de::Error::custom(format!(
196                        "market must be int or string, got: {v}"
197                    )));
198                }
199            };
200            Ok(Some(e.as_i32()))
201        }
202    }
203}
204
205/// v1.4.84 §5 B2: Vec<SubTypeEnum> 双接 (mixed int/string ok in same array)
206/// → Vec<i32>.
207pub fn deser_subtype_list_as_vec_i32<'de, D>(deserializer: D) -> Result<Vec<i32>, D::Error>
208where
209    D: serde::Deserializer<'de>,
210{
211    let raw: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
212    raw.into_iter()
213        .map(|v| {
214            let e: SubTypeEnum = match v {
215                serde_json::Value::Number(n) => {
216                    let i = n.as_i64().ok_or_else(|| {
217                        serde::de::Error::custom(format!("sub_type number invalid: {n}"))
218                    })? as i32;
219                    SubTypeEnum::from_i32(i).ok_or_else(|| {
220                        serde::de::Error::custom(format!(
221                            "unknown sub_type int {i}: valid = {:?}",
222                            SubTypeEnum::all_int_values()
223                        ))
224                    })?
225                }
226                serde_json::Value::String(s) => {
227                    let t = s.trim();
228                    SubTypeEnum::from_str(t)
229                        .or_else(|| t.parse::<i32>().ok().and_then(SubTypeEnum::from_i32))
230                        .ok_or_else(|| {
231                            serde::de::Error::custom(format!(
232                                "unknown sub_type {s:?}: valid = {:?}",
233                                SubTypeEnum::all_string_values()
234                            ))
235                        })?
236                }
237                _ => {
238                    return Err(serde::de::Error::custom(format!(
239                        "sub_type list element must be int or string, got: {v}"
240                    )));
241                }
242            };
243            Ok(e.as_i32())
244        })
245        .collect()
246}
247
248/// v1.4.84 §5 B2: PriceReminderOpEnum 双接 → i32.
249pub fn deser_price_reminder_op_as_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
250where
251    D: serde::Deserializer<'de>,
252{
253    let e: PriceReminderOpEnum = deser_int_or_enum_str(deserializer)?;
254    Ok(e.as_i32())
255}
256
257// ========== v1.4.90 P0-E + P1-G: String-returning wrappers ==========
258//
259// **背景** (P0-E int-string drift): tools.rs 里 11 个 trd `market: String`
260// 字段 + 1 个 PlaceOrderReq `order_type: String` 字段是 String 而非 i32 ——
261// 这些字段当前**只接 String**,不接 int. 本节加 _as_string wrappers 让这些
262// 已接入字段也能双接.
263//
264// **背景** (P1-G trd_market 缺 5 市场): v1.4.84 B2 没加 TrdMarketEnum (参与 B2
265// 的 5 enum 是 QotMarket / SubType / OrderType / KlType / PriceReminderOp).
266// 当时 TrdMarket 只列 HK/US/CN/HKCC; backend `Trd_Common.TrdMarket` proto 实际
267// 还有 SG=6 / AU=8 / JP=15 / MY=111 / CA=112 (P1-G 报告: routemap 已支持但
268// MCP/CLI 没列). 本节新增 [`TrdMarketEnum`] 9 variants, 对齐 proto.
269
270/// v1.4.90 P0-E + P1-G: TrdMarketEnum 双接 (int OR string) → 标准大写 String.
271///
272/// 用于 tools.rs 里 `market: String` 字段 (TrdAccReq / PlaceOrderReq /
273/// ModifyOrderReq / CancelOrderReq / MaxTrdQtysReq / OrderFeeReq /
274/// MarginRatioReq / HistoryQueryReq / AccCashFlowReq / CancelAllOrderReq).
275/// int 输入 → 转 canonical String; string 输入 → trim + uppercase + 通过
276/// `TrdMarketEnum::from_str` 验证 → 返 canonical.
277///
278/// **runtime 对齐状态**: `handlers/trade.rs` 与 `handlers/trade_write.rs`
279/// 已支持 HK/US/CN/HKCC/FUTURES/SG/AU/JP/MY/CA。HKFUND/USFUND 仅 read/view-only
280/// endpoint 接受;trade-write handler 会 fail closed,避免 fund-market 写路径误路由。
281pub fn deser_trd_market_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
282where
283    D: serde::Deserializer<'de>,
284{
285    let e: TrdMarketEnum = deser_int_or_enum_str(deserializer)?;
286    let i = e.as_i32();
287    let names = TrdMarketEnum::all_string_values();
288    let ints = TrdMarketEnum::all_int_values();
289    let idx = ints.iter().position(|&v| v == i).ok_or_else(|| {
290        serde::de::Error::custom(format!("trd_market i32 {i} has no canonical string"))
291    })?;
292    Ok(names[idx].to_string())
293}
294
295/// Optional TrdMarketEnum 双接 (int OR string) → Option<canonical String>.
296///
297/// 用于 legacy/compat 字段:调用 surface 仍可传旧 market 入参,但 runtime
298/// 不再信任它做账户 market 派生。这里保持旧客户端的 int/string 兼容与输入校验;
299/// 省略或 null 则返回 None。
300pub fn deser_trd_market_as_option_string<'de, D>(
301    deserializer: D,
302) -> Result<Option<String>, D::Error>
303where
304    D: serde::Deserializer<'de>,
305{
306    let opt: Option<serde_json::Value> = Option::deserialize(deserializer)?;
307    let Some(v) = opt else {
308        return Ok(None);
309    };
310    if v.is_null() {
311        return Ok(None);
312    }
313
314    let e = match v {
315        serde_json::Value::Number(n) => {
316            let i = n.as_i64().ok_or_else(|| {
317                serde::de::Error::custom(format!("trd_market number invalid: {n}"))
318            })? as i32;
319            TrdMarketEnum::from_i32(i).ok_or_else(|| {
320                serde::de::Error::custom(format!(
321                    "unknown trd_market int {i}: valid = {:?}",
322                    TrdMarketEnum::all_int_values()
323                ))
324            })?
325        }
326        serde_json::Value::String(s) => {
327            let trimmed = s.trim();
328            TrdMarketEnum::from_str(trimmed)
329                .or_else(|| {
330                    trimmed
331                        .parse::<i32>()
332                        .ok()
333                        .and_then(TrdMarketEnum::from_i32)
334                })
335                .ok_or_else(|| {
336                    serde::de::Error::custom(format!(
337                        "unknown trd_market string {s:?}: valid = {:?} or integer {:?}",
338                        TrdMarketEnum::all_string_values(),
339                        TrdMarketEnum::all_int_values()
340                    ))
341                })?
342        }
343        _ => {
344            return Err(serde::de::Error::custom(format!(
345                "trd_market must be int or string when present, got: {v}"
346            )));
347        }
348    };
349
350    let i = e.as_i32();
351    let names = TrdMarketEnum::all_string_values();
352    let ints = TrdMarketEnum::all_int_values();
353    let idx = ints.iter().position(|&v| v == i).ok_or_else(|| {
354        serde::de::Error::custom(format!("trd_market i32 {i} has no canonical string"))
355    })?;
356    Ok(Some(names[idx].to_string()))
357}
358
359/// v1.4.90 P0-E: OrderTypeEnum 双接 (int OR string) → 标准大写 String.
360///
361/// 用于 PlaceOrderReq.order_type: String. 让 LLM agent 传 STOP / STOP_LIMIT /
362/// MIT / LIT / TRAILING_STOP 等 v1.4.53 条件单 String 也能在 MCP 层接到 +
363/// 传 int (10/11/12/13/14/15/16/17/18/19) 也能解析.
364///
365/// **runtime 对齐状态**: `handlers/trade_write.rs::parse_order_type` 已支持
366/// 17 个公开订单类型,包括 STOP / STOP_LIMIT / MIT / LIT / trailing stop
367/// 以及 TWAP/VWAP。schema wrapper 与 runtime parser 不应再次分叉。
368pub fn deser_order_type_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
369where
370    D: serde::Deserializer<'de>,
371{
372    let e: OrderTypeEnum = deser_int_or_enum_str(deserializer)?;
373    let i = e.as_i32();
374    let names = OrderTypeEnum::all_string_values();
375    let ints = OrderTypeEnum::all_int_values();
376    let idx = ints.iter().position(|&v| v == i).ok_or_else(|| {
377        serde::de::Error::custom(format!("order_type i32 {i} has no canonical string"))
378    })?;
379    Ok(names[idx].to_string())
380}
381
382// Split: 1040 → 5 enum 子文件 + mod.rs head (ToolEnum trait + helpers)
383mod market_enum;
384mod order_type_enum;
385mod price_reminder_op_enum;
386mod subtype_enum;
387mod trd_market_enum;
388
389pub use market_enum::MarketEnum;
390pub use order_type_enum::OrderTypeEnum;
391pub use price_reminder_op_enum::PriceReminderOpEnum;
392pub use subtype_enum::SubTypeEnum;
393pub use trd_market_enum::TrdMarketEnum;
394
395#[cfg(test)]
396mod tests;