Skip to main content

futu_mcp/tool_args/
qot.rs

1//! v1.4.110 P1-1: 拆自 `tool_args.rs` 按 handler 域分组.
2
3#![allow(unused_imports)]
4
5use rmcp::schemars;
6use serde::{Deserialize, Serialize};
7
8use crate::tool_enums;
9use crate::tool_enums::ToolEnum;
10
11use super::*;
12
13#[derive(Debug, Deserialize, schemars::JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct SymbolReq {
16    #[schemars(
17        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
18                              (alias: code / stock / security for SDK compat)"
19    )]
20    // v1.4.83 §5 Phase 3: 接受 code / stock / security 三种 alias (对齐
21    // py-futu-api 风格 + v1.4.82 A1/B1 REST adapter alias). 新代码推荐用
22    // `symbol` (Rust native snake_case).
23    #[serde(alias = "code", alias = "stock", alias = "security")]
24    pub symbol: String,
25}
26
27#[derive(Debug, Deserialize, schemars::JsonSchema)]
28#[serde(deny_unknown_fields)]
29pub struct SymbolListReq {
30    #[schemars(description = "Array of security symbols in MARKET.CODE format \
31                       (e.g. [\"HK.00700\", \"US.AAPL\"]). Field name is \
32                       `symbols` (Rust native snake_case); aliases: `stocks` \
33                       / `code_list` / `symbol_list` / `security_list` \
34                       for SDK compat.")]
35    // v1.4.83 §5 Phase 3: 接受 stocks / code_list / symbol_list /
36    // security_list 四种 alias (对齐 v1.4.82 B1 REST adapter alias).
37    #[serde(
38        alias = "stocks",
39        alias = "code_list",
40        alias = "symbol_list",
41        alias = "security_list"
42    )]
43    pub symbols: Vec<String>,
44}
45
46#[derive(Debug, Deserialize, schemars::JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct KLineReq {
49    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock")]
50    // v1.4.83 §5 Phase 3
51    #[serde(alias = "code", alias = "stock")]
52    pub symbol: String,
53    #[schemars(
54        description = "K-line type: day|week|month|quarter|year|1min|3min|5min|15min|30min|60min \
55                       (alias: ktype / k_type / kl_type for SDK compat)"
56    )]
57    #[serde(default = "default_kl_type", alias = "ktype", alias = "k_type")]
58    pub kl_type: String,
59    #[schemars(
60        description = "Number of candles to return (default 100); alias: num / max_count / req_count"
61    )]
62    #[serde(alias = "num", alias = "max_count", alias = "req_count")]
63    pub count: Option<i32>,
64    #[schemars(
65        description = "Start date yyyy-MM-dd (optional; default computed from count); alias: begin_time / from"
66    )]
67    #[serde(alias = "begin_time", alias = "from")]
68    pub begin: Option<String>,
69    #[schemars(
70        description = "End date yyyy-MM-dd (optional; default today); alias: end_time / to"
71    )]
72    #[serde(alias = "end_time", alias = "to")]
73    pub end: Option<String>,
74}
75
76#[derive(Debug, Deserialize, schemars::JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct OrderBookReq {
79    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock / security")]
80    // v1.4.84 §5 B1
81    #[serde(alias = "code", alias = "stock", alias = "security")]
82    pub symbol: String,
83    #[schemars(description = "Order book depth, 1-10 (default 10); alias: num")]
84    #[serde(default = "default_depth", alias = "num")]
85    pub depth: i32,
86}
87
88#[derive(Debug, Deserialize, schemars::JsonSchema)]
89#[serde(deny_unknown_fields)]
90pub struct TickerReq {
91    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock / security")]
92    // v1.4.84 §5 B1
93    #[serde(alias = "code", alias = "stock", alias = "security")]
94    pub symbol: String,
95    #[schemars(
96        description = "Number of ticks to fetch (default 100, max 1000); alias: num / max_count / req_count"
97    )]
98    #[serde(
99        default = "default_ticker_count",
100        alias = "num",
101        alias = "max_count",
102        alias = "req_count"
103    )]
104    pub count: i32,
105}
106
107#[derive(Debug, Deserialize, schemars::JsonSchema)]
108#[serde(deny_unknown_fields)]
109pub struct PlateListReq {
110    #[schemars(description = "Market: HK|HK_FUTURE|US|SH|SZ")]
111    pub market: String,
112    #[schemars(
113        description = "Plate set: all|industry|region|concept (default all); alias: plate_set_type"
114    )]
115    // v1.4.84 §5 B1
116    #[serde(default = "default_plate_set", alias = "plate_set_type")]
117    pub plate_set: String,
118}
119
120#[derive(Debug, Deserialize, schemars::JsonSchema)]
121#[serde(deny_unknown_fields)]
122pub struct PlateStocksReq {
123    #[schemars(
124        description = "Plate symbol, MARKET.CODE format (e.g. HK.LIST1001); alias: symbol / code / plate_code"
125    )]
126    // v1.4.84 §5 B1
127    #[serde(alias = "symbol", alias = "code", alias = "plate_code")]
128    pub plate: String,
129}
130
131#[derive(Debug, Deserialize, schemars::JsonSchema)]
132#[serde(deny_unknown_fields)]
133pub struct MarketStateReq {
134    #[schemars(
135        description = "Symbols list in MARKET.CODE format; alias: stocks / code_list / symbol_list / security_list"
136    )]
137    // v1.4.84 §5 B1
138    #[serde(
139        alias = "stocks",
140        alias = "code_list",
141        alias = "symbol_list",
142        alias = "security_list"
143    )]
144    pub symbols: Vec<String>,
145}
146
147#[derive(Debug, Deserialize, schemars::JsonSchema)]
148#[serde(deny_unknown_fields)]
149pub struct HistoryKLineReq {
150    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock")]
151    // v1.4.83 §5 Phase 3
152    #[serde(alias = "code", alias = "stock")]
153    pub symbol: String,
154    #[schemars(
155        description = "K-line type: day|week|month|quarter|year|1min|3min|5min|15min|30min|60min \
156                       (default day); alias: ktype / k_type"
157    )]
158    #[serde(default = "default_kl_type", alias = "ktype", alias = "k_type")]
159    pub kl_type: String,
160    #[schemars(description = "Rehab type: none|forward|backward (default none)")]
161    #[serde(default = "default_rehab_none")]
162    pub rehab_type: String,
163    #[schemars(description = "Start date yyyy-MM-dd; alias: begin_time / start_time / from")]
164    #[serde(alias = "begin_time", alias = "start_time", alias = "from")]
165    pub begin: String,
166    #[schemars(description = "End date yyyy-MM-dd; alias: end_time / to")]
167    #[serde(alias = "end_time", alias = "to")]
168    pub end: String,
169    #[schemars(
170        description = "Max number of candles to return (default 1000, range 1-1000). \
171                       If omitted, the gateway uses 1000; pass explicit 0 to request no limit \
172                       only when you can handle a large response. alias: num / count / req_count"
173    )]
174    // v1.4.106 codex 0635 ζ36 F5: schema 文案承诺 default 1000, 给一个真 default.
175    // 之前是 Option<i32> 不带 serde(default), MCP 用户省略 max_count 实际不是 1000
176    // 而是不传限制 → 响应规模超 schema 预期 → 上下文膨胀.
177    #[serde(
178        default = "default_history_kline_max_count",
179        alias = "num",
180        alias = "count",
181        alias = "req_count"
182    )]
183    pub max_count: Option<i32>,
184}
185
186#[derive(Debug, Deserialize, schemars::JsonSchema)]
187#[serde(deny_unknown_fields)]
188pub struct ReferenceReq {
189    #[schemars(description = "Underlying symbol (e.g. HK.00700, US.AAPL); alias: code / stock")]
190    // v1.4.84 §5 B1
191    #[serde(alias = "code", alias = "stock")]
192    pub symbol: String,
193    // v1.4.41 (eli v1.4.40 报告 P2.5 修): description 从 "warrant|future|option
194    // (default option)" 改成 "warrant|future (default warrant)"。真实 backend
195    // 不支持 reference_type=option(返 "unsupported reference type"),旧 schema
196    // 谎报误导 LLM agent。option 场景用 `futu_get_option_chain` 或 `futu_snapshot`。
197    #[schemars(
198        description = "Reference type: warrant|future (default warrant). Note: option is NOT supported — use futu_get_option_chain instead."
199    )]
200    #[serde(default = "default_reference_type")]
201    pub reference_type: String,
202}
203
204#[derive(Debug, Deserialize, schemars::JsonSchema)]
205#[serde(deny_unknown_fields)]
206pub struct OptionChainReq {
207    #[schemars(
208        description = "Underlying stock symbol (e.g. HK.00700, US.AAPL); alias: symbol / owner / code / stock"
209    )]
210    // v1.4.84 §5 B1
211    #[serde(alias = "symbol", alias = "owner", alias = "code", alias = "stock")]
212    pub owner_symbol: String,
213    #[schemars(
214        description = "Expiry range begin date yyyy-MM-dd; alias: begin / start_time / from"
215    )]
216    #[serde(alias = "begin", alias = "start_time", alias = "from")]
217    pub begin_time: String,
218    #[schemars(description = "Expiry range end date yyyy-MM-dd; alias: end / to")]
219    #[serde(alias = "end", alias = "to")]
220    pub end_time: String,
221    #[schemars(description = "Option type: all|call|put (default all)")]
222    pub option_type: Option<String>,
223
224    // v1.4.38 Phase 3: server-side Greek filter (via backend CMD 6736)
225    // 所有 filter 字段 optional;全 None 时不发送 filter 请求,保持 v1.4.37 行为
226    #[schemars(
227        description = "Optional Greek filter: only return options with delta in [min, max]. Typical ATM range: 0.3 to 0.7 for calls, -0.7 to -0.3 for puts."
228    )]
229    pub delta_min: Option<f64>,
230    #[schemars(description = "See delta_min.")]
231    pub delta_max: Option<f64>,
232    #[schemars(description = "Implied volatility filter min (decimal, e.g. 0.3 = 30%).")]
233    pub iv_min: Option<f64>,
234    #[schemars(description = "See iv_min.")]
235    pub iv_max: Option<f64>,
236    #[schemars(description = "Open interest (contracts) filter min, integer.")]
237    pub oi_min: Option<f64>,
238    #[schemars(description = "See oi_min.")]
239    pub oi_max: Option<f64>,
240    #[schemars(description = "Gamma filter min (decimal).")]
241    pub gamma_min: Option<f64>,
242    #[schemars(description = "See gamma_min.")]
243    pub gamma_max: Option<f64>,
244    #[schemars(description = "Vega filter min (decimal).")]
245    pub vega_min: Option<f64>,
246    #[schemars(description = "See vega_min.")]
247    pub vega_max: Option<f64>,
248    #[schemars(description = "Theta filter min (decimal).")]
249    pub theta_min: Option<f64>,
250    #[schemars(description = "See theta_min.")]
251    pub theta_max: Option<f64>,
252}
253
254#[derive(Debug, Deserialize, schemars::JsonSchema)]
255#[serde(deny_unknown_fields)]
256pub struct WarrantReq {
257    #[schemars(
258        description = "Underlying stock symbol (e.g. HK.00700); None = whole-market warrants. Alias: symbol / owner / code"
259    )]
260    // v1.4.84 §5 B1
261    #[serde(default, alias = "symbol", alias = "owner", alias = "code")]
262    pub owner_symbol: Option<String>,
263    /// v1.4.106 codex 0635 ζ36 F1: 暴露 begin 让用户能拿下一页. 之前 wrapper
264    /// 硬编码 begin=0, 响应却含 last_page / all_count, 调用者看到 "还有下一页"
265    /// 但无法翻页. C++ backend 自始支持 data_from / data_max_count.
266    #[schemars(description = "Pagination begin index (default 0); alias: offset / skip")]
267    #[serde(default, alias = "offset", alias = "skip")]
268    pub begin: i32,
269    #[schemars(description = "Max rows (1-200, default 20); alias: count / max_count / req_count")]
270    #[serde(
271        default = "default_warrant_num",
272        alias = "count",
273        alias = "max_count",
274        alias = "req_count"
275    )]
276    pub num: i32,
277}
278
279#[derive(Debug, Deserialize, schemars::JsonSchema)]
280#[serde(deny_unknown_fields)]
281pub struct IpoListReq {
282    #[schemars(
283        description = "Market code — **Qot_Common.QotMarket enum**. Accept int (1=HK, 11=US, 21=SH, 22=SZ) \
284                       OR string (\"HK\" / \"US\" / \"SH\" / \"SZ\" / ...). v1.4.84 §5 B2 双接."
285    )]
286    // v1.4.84 §5 B2 field migration
287    #[serde(deserialize_with = "tool_enums::deser_market_as_i32")]
288    pub market: i32,
289}
290
291#[derive(Debug, Deserialize, schemars::JsonSchema)]
292#[serde(deny_unknown_fields)]
293pub struct FutureInfoReq {
294    #[schemars(
295        description = "Array of future contract symbols in MARKET.CODE format \
296                       (e.g. [\"HK.HSImain\", \"US.MNQmain\"]). Alias: stocks / \
297                       code_list / symbol_list / security_list"
298    )]
299    // v1.4.84 §5 B1
300    #[serde(
301        alias = "stocks",
302        alias = "code_list",
303        alias = "symbol_list",
304        alias = "security_list"
305    )]
306    pub symbols: Vec<String>,
307}
308
309#[derive(Debug, Deserialize, schemars::JsonSchema)]
310#[serde(deny_unknown_fields)]
311pub struct UserSecurityGroupReq {
312    #[schemars(description = "Group type: 1=all, 2=custom, 3=system (default 1)")]
313    #[serde(default = "default_user_security_group_type")]
314    pub group_type: i32,
315}
316
317#[derive(Debug, Deserialize, schemars::JsonSchema)]
318#[serde(deny_unknown_fields)]
319pub struct StockFilterReq {
320    #[schemars(
321        description = "Market code — **Qot_Common.QotMarket enum**. Accept int (1=HK, 11=US, 21=SH, 22=SZ) \
322                       OR string (\"HK\" / \"US\" / ...). v1.4.84 §5 B2 双接."
323    )]
324    // v1.4.84 §5 B2 field migration
325    #[serde(deserialize_with = "tool_enums::deser_market_as_i32")]
326    pub market: i32,
327    #[schemars(description = "Pagination begin index (default 0); alias: offset / skip")]
328    // v1.4.84 §5 B1
329    #[serde(default, alias = "offset", alias = "skip")]
330    pub begin: i32,
331    #[schemars(description = "Max rows (1-200, default 50); alias: count / max_count / req_count")]
332    #[serde(
333        default = "default_stock_filter_num",
334        alias = "count",
335        alias = "max_count",
336        alias = "req_count"
337    )]
338    pub num: i32,
339}
340
341#[derive(Debug, Deserialize, schemars::JsonSchema)]
342#[serde(deny_unknown_fields)]
343pub struct TradingDaysReq {
344    #[schemars(
345        description = "Market code — **Qot_Common.TradeDateMarket enum** (i32, NOT QotMarket!): \
346                       1=HK, 2=US, 3=CN, 4=NorthboundSZ/SH, 5=SouthboundHK, 6=JP_Future, 7=SG_Future. \
347                       Different from QotMarket (ipo_list/stock_filter use 1=HK 11=US 21=SH 22=SZ). \
348                       v1.4.30 P3 documented inconsistency."
349    )]
350    pub market: i32,
351    #[schemars(description = "Begin date (yyyy-MM-dd); alias: begin / start_time / from")]
352    // v1.4.84 §5 B1
353    #[serde(alias = "begin", alias = "start_time", alias = "from")]
354    pub begin_time: String,
355    #[schemars(description = "End date (yyyy-MM-dd); alias: end / to")]
356    #[serde(alias = "end", alias = "to")]
357    pub end_time: String,
358}
359
360#[derive(Debug, Deserialize, schemars::JsonSchema)]
361#[serde(deny_unknown_fields)]
362pub struct SuspendReq {
363    #[schemars(description = "Array of security symbols in MARKET.CODE format \
364                       (e.g. [\"HK.00700\", \"HK.09988\"]). Alias: stocks / code_list / symbol_list / security_list")]
365    // v1.4.84 §5 B1
366    #[serde(
367        alias = "stocks",
368        alias = "code_list",
369        alias = "symbol_list",
370        alias = "security_list"
371    )]
372    pub symbols: Vec<String>,
373    #[schemars(description = "Begin date (yyyy-MM-dd); alias: begin / start_time / from")]
374    #[serde(alias = "begin", alias = "start_time", alias = "from")]
375    pub begin_time: String,
376    #[schemars(description = "End date (yyyy-MM-dd); alias: end / to")]
377    #[serde(alias = "end", alias = "to")]
378    pub end_time: String,
379}
380
381#[derive(Debug, Deserialize, schemars::JsonSchema)]
382#[serde(deny_unknown_fields)]
383pub struct UserSecurityReq {
384    #[schemars(
385        description = "Watchlist group name (use futu_get_user_security_group to list groups); alias: group / name"
386    )]
387    // v1.4.84 §5 B1
388    #[serde(alias = "group", alias = "name")]
389    pub group_name: String,
390}
391
392#[derive(Debug, Deserialize, schemars::JsonSchema)]
393#[serde(deny_unknown_fields)]
394pub struct HistoryKlQuotaReq {
395    #[schemars(
396        description = "Whether to fetch detailed per-symbol download history (default false)"
397    )]
398    #[serde(default)]
399    pub get_detail: bool,
400}
401
402#[derive(Debug, Deserialize, schemars::JsonSchema)]
403#[serde(deny_unknown_fields)]
404pub struct HoldingChangeReq {
405    #[schemars(
406        description = "Underlying stock symbol (e.g. HK.00700, US.AAPL); alias: code / stock"
407    )]
408    // v1.4.84 §5 B1
409    #[serde(alias = "code", alias = "stock")]
410    pub symbol: String,
411    #[schemars(
412        description = "Holder category: 1=Institution, 2=Fund, 3=Executive; alias: category"
413    )]
414    #[serde(alias = "category")]
415    pub holder_category: i32,
416    #[schemars(
417        description = "Begin time YYYY-MM-DD HH:MM:SS (optional); alias: begin / start_time / from"
418    )]
419    #[serde(default, alias = "begin", alias = "start_time", alias = "from")]
420    pub begin_time: Option<String>,
421    #[schemars(description = "End time YYYY-MM-DD HH:MM:SS (optional); alias: end / to")]
422    #[serde(default, alias = "end", alias = "to")]
423    pub end_time: Option<String>,
424}
425
426#[derive(Debug, Deserialize, schemars::JsonSchema)]
427#[serde(deny_unknown_fields)]
428pub struct ModifyUserSecurityReq {
429    #[schemars(description = "Watchlist group name; alias: group / name")]
430    // v1.4.84 §5 B1
431    #[serde(alias = "group", alias = "name")]
432    pub group_name: String,
433    #[schemars(
434        description = "Op: 1=AddInto, 2=Delete (from this group), 3=MoveOut; alias: op_type / operation"
435    )]
436    #[serde(alias = "op_type", alias = "operation")]
437    pub op: i32,
438    #[schemars(
439        description = "Security symbols to add/delete/move; alias: stocks / code_list / symbol_list / security_list"
440    )]
441    #[serde(
442        alias = "stocks",
443        alias = "code_list",
444        alias = "symbol_list",
445        alias = "security_list"
446    )]
447    pub symbols: Vec<String>,
448}
449
450#[derive(Debug, Deserialize, schemars::JsonSchema)]
451#[serde(deny_unknown_fields)]
452pub struct CodeChangeReq {
453    #[schemars(
454        description = "Security symbols to query (currently HK only); alias: stocks / code_list / symbol_list / security_list"
455    )]
456    // v1.4.84 §5 B1
457    #[serde(
458        alias = "stocks",
459        alias = "code_list",
460        alias = "symbol_list",
461        alias = "security_list"
462    )]
463    pub symbols: Vec<String>,
464}
465
466#[derive(Debug, Deserialize, schemars::JsonSchema)]
467#[serde(deny_unknown_fields)]
468pub struct SetPriceReminderReq {
469    #[schemars(description = "Security symbol (e.g. \"HK.00700\"). Field aliases: \
470                       `code` / `stock` (deprecated — prefer canonical `symbol`).")]
471    #[serde(alias = "code", alias = "stock")]
472    pub symbol: String,
473    #[schemars(
474        description = "Op: 1=Add / SetAdd, 2=Del / SetDel, 3=Enable / SetEnable, \
475                       4=Disable / SetDisable, 5=Modify, 6=DeleteAll / DelAll. \
476                       Accepts integer code OR string form (e.g. 1 or \"Add\"). \
477                       Aliases for the field name: `op_type` / `operation` \
478                       (deprecated — prefer canonical `op`)."
479    )]
480    #[serde(
481        alias = "op_type",
482        alias = "operation",
483        deserialize_with = "tool_enums::deser_price_reminder_op_as_i32"
484    )]
485    pub op: i32,
486    #[schemars(
487        description = "Reminder key (from get_price_reminder; required for modify/del/enable/disable)"
488    )]
489    #[serde(default)]
490    pub key: Option<i64>,
491    #[schemars(description = "Qot_Common::PriceReminderType: \
492                       1=PriceUp, 2=PriceDown, 3=ChangeRateUp, 4=ChangeRateDown, \
493                       5=5MinChangeRateUp, 6=5MinChangeRateDown, 7=VolumeUp, 8=TurnoverUp, \
494                       9=TurnoverRateUp, 10=BidPriceUp, 11=AskPriceDown, 12=BidVolUp, \
495                       13=AskVolUp, 14=3MinChangeRateUp, 15=3MinChangeRateDown.")]
496    #[serde(default)]
497    pub reminder_type: Option<i32>,
498    #[schemars(
499        description = "Qot_Common::PriceReminderFreq: 1=Always, 2=OncePerDay, 3=Once. \
500                       Required for op=1 (Add) — the gateway rejects Add without freq. \
501                       Optional for op=5 (Modify) / 2/3/4 (Del/Enable/Disable) where the \
502                       backend preserves the existing value when omitted."
503    )]
504    #[serde(default)]
505    pub freq: Option<i32>,
506    #[schemars(description = "Threshold value (required for Add/Modify)")]
507    #[serde(default)]
508    pub value: Option<f64>,
509    #[schemars(
510        description = "User note (optional). Maximum length: 40 half-width bytes \
511                       (~20 CN characters or 40 ASCII characters) using UTF-16 \
512                       half-/full-width counting (each ASCII code unit = 1 byte, \
513                       each non-ASCII code unit = 2 bytes)."
514    )]
515    #[serde(default)]
516    pub note: Option<String>,
517    /// Reminder session list controlling which trading sessions trigger the alert.
518    ///
519    /// `Qot_Common::PriceReminderMarketStatus`: 1=Open, 2=USPre, 3=USAfter, 4=USOverNight.
520    /// US stocks (with pre-/after-/overnight sessions) default to `[Open, USPre, USAfter]`
521    /// when this list is empty. Non-US securities ignore the list entirely.
522    #[schemars(
523        description = "Reminder session list (PriceReminderMarketStatus: 1=Open, 2=USPre, \
524                       3=USAfter, 4=USOverNight). For US stocks, an empty list defaults to \
525                       [Open, USPre, USAfter]; for non-US securities the list is cleared."
526    )]
527    #[serde(default)]
528    pub reminder_session_list: Vec<i32>,
529}
530
531impl SetPriceReminderReq {
532    /// Runtime validation for op-conditional required fields.
533    ///
534    /// Required fields by op:
535    /// - op=1 (Add): `reminder_type` + `freq` + `value`
536    /// - op=5 (Modify): `key` (other fields are optional and the backend
537    ///   preserves the existing value when omitted)
538    /// - op=2/3/4 (Del / Enable / Disable): `key`
539    /// - op=6 (DeleteAll): no extra required fields
540    ///
541    /// Returns `Err(String)` with a human-readable hint for the agent /
542    /// SDK user when validation fails.
543    pub fn validate(&self) -> Result<(), String> {
544        match self.op {
545            1 => {
546                if self.reminder_type.is_none() {
547                    return Err(
548                        "SetPriceReminderReq op=1 (Add): `reminder_type` is required \
549                         (PriceReminderType enum 1-15)"
550                            .to_string(),
551                    );
552                }
553                // v1.4.106 codex 0450 F2 (P2): freq schema/runtime sync —
554                // gateway 对齐 C++ NN_PriceReminderFreq_None check 在 Add 路径
555                // 强制要求 freq, MCP/REST schema 必须同步要求, 否则 caller 按
556                // schema 不传 freq 会触发 runtime reject (silent ship-blocker
557                // 见 pitfall #54 schema-only fix).
558                if self.freq.is_none() {
559                    return Err("SetPriceReminderReq op=1 (Add): `freq` is required \
560                         (PriceReminderFreq enum 1=Always / 2=OncePerDay / 3=Once)"
561                        .to_string());
562                }
563                if self.value.is_none() {
564                    return Err(
565                        "SetPriceReminderReq op=1 (Add): `value` is required (threshold \
566                         value for reminder_type)"
567                            .to_string(),
568                    );
569                }
570            }
571            2..=4 => {
572                if self.key.is_none() {
573                    return Err(format!(
574                        "SetPriceReminderReq op={} ({}): `key` is required (from \
575                         get_price_reminder response)",
576                        self.op,
577                        match self.op {
578                            2 => "Del",
579                            3 => "Enable",
580                            _ => "Disable",
581                        }
582                    ));
583                }
584            }
585            5 => {
586                if self.key.is_none() {
587                    return Err(
588                        "SetPriceReminderReq op=5 (Modify): `key` is required (from \
589                         get_price_reminder response)"
590                            .to_string(),
591                    );
592                }
593            }
594            6 => {} // DeleteAll: 无必填
595            _ => {
596                return Err(format!(
597                    "SetPriceReminderReq: unknown op={}, expected 1=Add|2=Del|3=Enable|\
598                     4=Disable|5=Modify|6=DeleteAll",
599                    self.op
600                ));
601            }
602        }
603        // v1.4.106 codex 0450 F5 (P3): strict reject invalid session list values
604        // (defense-in-depth — gateway also rejects but agent-side feedback is
605        // faster than wire round-trip). Deliberate deviation from C++ silent
606        // drop (see handler comment).
607        for &session in &self.reminder_session_list {
608            if !matches!(session, 1..=4) {
609                return Err(format!(
610                    "SetPriceReminderReq: invalid reminder_session_list entry \
611                     {session} (PriceReminderMarketStatus 1=Open / 2=USPre / \
612                     3=USAfter / 4=USOverNight)"
613                ));
614            }
615        }
616        Ok(())
617    }
618}
619
620#[derive(Debug, Deserialize, schemars::JsonSchema)]
621#[serde(deny_unknown_fields)]
622pub struct GetPriceReminderReq {
623    #[schemars(description = "Security symbol (MARKET.CODE, e.g. HK.00700). \
624                       **Exactly one of `symbol` or `market` must be set.** \
625                       Passing both: symbol wins. Passing neither returns an error. \
626                       v1.4.30 P3: schema-level oneOf not enforceable via serde; runtime check in handler. \
627                       Alias: code / stock")]
628    // v1.4.84 §5 B1
629    #[serde(default, alias = "code", alias = "stock")]
630    pub symbol: Option<String>,
631    #[schemars(
632        description = "Market code — **Qot_Common.QotMarket enum**. Accept int (1=HK, 11=US, 21=CN) \
633                       OR string (\"HK\" / \"US\" / \"CN\"). \
634                       **Exactly one of `symbol` or `market` required** (see symbol field doc). \
635                       v1.4.84 §5 B2 双接."
636    )]
637    // v1.4.84 §5 B2 field migration (Option variant)
638    #[serde(default, deserialize_with = "tool_enums::deser_market_as_option_i32")]
639    pub market: Option<i32>,
640}
641
642impl GetPriceReminderReq {
643    /// v1.4.84 §5 B4: runtime validate — symbol XOR market required.
644    ///
645    /// v1.4.30 P3: schema-level oneOf 不可能通过 serde 强制 (Option<T> 模型无法
646    /// 表达 "exactly one of"). runtime check 返清晰错误.
647    pub fn validate(&self) -> Result<(), String> {
648        if self.symbol.is_none() && self.market.is_none() {
649            return Err(
650                "GetPriceReminderReq: exactly one of `symbol` or `market` is required \
651                 (neither provided)"
652                    .to_string(),
653            );
654        }
655        // symbol + market 同时传: symbol wins (schema 已说明), 不 error
656        Ok(())
657    }
658}
659
660#[derive(Debug, Deserialize, schemars::JsonSchema)]
661#[serde(deny_unknown_fields)]
662pub struct OptionExpirationDateReq {
663    #[schemars(
664        description = "Underlying stock symbol (HK/US equities + HSI/HSCEI only); alias: symbol / owner / code / stock"
665    )]
666    // v1.4.84 §5 B1
667    #[serde(alias = "symbol", alias = "owner", alias = "code", alias = "stock")]
668    pub owner_symbol: String,
669    #[schemars(description = "For index options only: Qot_Common::IndexOptionType (optional)")]
670    #[serde(default)]
671    pub index_option_type: Option<i32>,
672}
673
674#[derive(Debug, Deserialize, schemars::JsonSchema)]
675#[serde(deny_unknown_fields)]
676pub struct BizGroupReq {
677    #[schemars(description = "Trade env: real / simulate (default real)")]
678    #[serde(default = "default_env", alias = "trd_env")]
679    pub env: String,
680    #[schemars(description = "Trading account ID (u64)")]
681    pub acc_id: u64,
682    #[schemars(
683        description = "Optional legacy market hint; accepted for backward compatibility but ignored. Daemon derives backend market from acc_id/account cache."
684    )]
685    #[serde(
686        default,
687        deserialize_with = "tool_enums::deser_trd_market_as_option_string"
688    )]
689    pub market: Option<String>,
690}
691
692#[derive(Debug, Deserialize, schemars::JsonSchema)]
693#[serde(deny_unknown_fields)]
694pub struct BondSymbolReq {
695    #[schemars(description = "Trade env: real / simulate (default real)")]
696    #[serde(default = "default_env", alias = "trd_env")]
697    pub env: String,
698    #[schemars(description = "Trading account ID (u64) for per-broker routing")]
699    pub acc_id: u64,
700    #[schemars(description = "Market: HK / US / SG (仅 3 市场有债券业务)")]
701    pub market: String,
702    #[schemars(description = "Bond symbol (债券代码, 如 HK1234 或 11000018)")]
703    pub symbol: String,
704}
705
706/// v1.4.98 T2-3 (mobile-source-audit Phase 2): TickerStatistic params.
707#[derive(Debug, Deserialize, schemars::JsonSchema)]
708#[serde(deny_unknown_fields)]
709pub struct TickerStatisticReq {
710    #[schemars(description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL")]
711    #[serde(alias = "code", alias = "stock", alias = "security")]
712    pub symbol: String,
713    #[schemars(
714        description = "Ticker type filter: 0=ALL, 1=BUY, 2=SELL, 3=BUY_AND_SELL, 4=NEUTRAL (default ALL)"
715    )]
716    #[serde(default)]
717    pub ticker_type: Option<i32>,
718    #[schemars(description = "Market session: 0=ALL, 1=BEFORE, 2=TRADING, 3=AFTER (default ALL)")]
719    #[serde(default)]
720    pub stat_type: Option<u32>,
721}
722
723/// v1.4.106 codex 0500 ζ23-redo: TickerStatistic Detail params (cmd 6366).
724///
725/// 配套 `futu_get_ticker_statistic` (Info, cmd 6365): 先 call Info 拿
726/// `ticker_time`, 再 call Detail 同 `ticker_time` 拿该时点的价位分布 list.
727#[derive(Debug, Deserialize, schemars::JsonSchema)]
728#[serde(deny_unknown_fields)]
729pub struct TickerStatisticDetailReq {
730    #[schemars(description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL")]
731    #[serde(alias = "code", alias = "stock", alias = "security")]
732    pub symbol: String,
733    #[schemars(
734        description = "Ticker type filter: 0=ALL, 1=BUY, 2=SELL, 3=BUY_AND_SELL, 4=NEUTRAL (default ALL)"
735    )]
736    #[serde(default)]
737    pub ticker_type: Option<i32>,
738    #[schemars(
739        description = "Ticker timestamp (ms) — usually from prior futu_get_ticker_statistic call. \
740            0 / omit = use backend latest available."
741    )]
742    #[serde(default)]
743    pub ticker_time: Option<u64>,
744    #[schemars(
745        description = "Filter type: 0=all price levels, 1..N=top N levels (backend max ~100)"
746    )]
747    #[serde(default)]
748    pub select_num: Option<u32>,
749    #[schemars(description = "Pagination start offset (default 0)")]
750    #[serde(default)]
751    pub data_from: Option<u32>,
752    #[schemars(description = "Pagination size, max items returned (default 20)")]
753    #[serde(default)]
754    pub data_max_count: Option<u32>,
755    #[schemars(description = "Market session: 0=ALL, 1=BEFORE, 2=TRADING, 3=AFTER (default ALL)")]
756    #[serde(default)]
757    pub stat_type: Option<u32>,
758}
759
760#[derive(Debug, Deserialize, schemars::JsonSchema)]
761#[serde(deny_unknown_fields)]
762pub struct QuoteRightsReq {
763    #[schemars(description = "If true, trigger request_highest_quote_right before querying")]
764    #[serde(default)]
765    pub refresh: Option<bool>,
766}