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;