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