Skip to main content

futu_mcp/tool_args/qot/
reference.rs

1//! MCP QOT request schemas split from the tool-args root.
2
3use super::*;
4use futu_qot::ipo_calendar::{is_supported_ipo_event_type, is_yyyymmdd};
5
6#[derive(Debug, Deserialize, schemars::JsonSchema)]
7#[serde(deny_unknown_fields)]
8pub struct HistoryKLineReq {
9    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock")]
10    // v1.4.83 §5 Phase 3
11    #[serde(alias = "code", alias = "stock")]
12    pub symbol: String,
13    #[schemars(
14        description = "K-line type: day|week|month|quarter|year|1min|3min|5min|10min|15min|30min|60min|120min|180min|240min \
15                       (default day); alias: ktype / k_type"
16    )]
17    #[serde(default = "default_kl_type", alias = "ktype", alias = "k_type")]
18    pub kl_type: String,
19    #[schemars(description = "Rehab type: none|forward|backward (default none)")]
20    #[serde(default = "default_rehab_none")]
21    pub rehab_type: String,
22    #[schemars(description = "Start date yyyy-MM-dd; alias: begin_time / start_time / from")]
23    #[serde(alias = "begin_time", alias = "start_time", alias = "from")]
24    pub begin: String,
25    #[schemars(description = "End date yyyy-MM-dd; alias: end_time / to")]
26    #[serde(alias = "end_time", alias = "to")]
27    pub end: String,
28    #[schemars(
29        description = "Max number of candles to return (default 1000, range 1-1000). \
30                       If omitted, the gateway uses 1000; pass explicit 0 to request no limit \
31                       only when you can handle a large response. alias: num / count / req_count"
32    )]
33    // v1.4.106 codex 0635 ζ36 F5: schema 文案承诺 default 1000, 给一个真 default.
34    // 之前是 Option<i32> 不带 serde(default), MCP 用户省略 max_count 实际不是 1000
35    // 而是不传限制 → 响应规模超 schema 预期 → 上下文膨胀.
36    #[serde(
37        default = "default_history_kline_max_count",
38        alias = "num",
39        alias = "count",
40        alias = "req_count"
41    )]
42    pub max_count: Option<i32>,
43    #[schemars(description = "Optional Qot_Common.KLFields bitmask; alias: needKLFieldsFlag")]
44    #[serde(default, alias = "needKLFieldsFlag")]
45    pub need_kl_fields_flag: Option<i64>,
46    #[schemars(
47        description = "Fetch US extended-hours K-line data when supported; alias: extendedTime"
48    )]
49    #[serde(default, alias = "extendedTime")]
50    pub extended_time: Option<bool>,
51    #[schemars(
52        description = "Common.Session: 0=NONE, 1=RTH, 2=ETH, 3=ALL. 4=OVERNIGHT is rejected like C++ OpenD."
53    )]
54    #[serde(default)]
55    pub session: Option<i32>,
56    #[schemars(
57        description = "Pagination key from previous response, base64-encoded; alias: nextReqKey / next_key"
58    )]
59    #[serde(default, alias = "nextReqKey", alias = "next_key")]
60    pub next_req_key: Option<String>,
61}
62
63impl HistoryKLineReq {
64    pub fn validated_max_count(&self) -> Result<Option<i32>, String> {
65        futu_qot::page_bounds::validate_optional_max_count(self.max_count, 1000, "history_kline")
66            .map_err(|e| e.to_string())
67    }
68
69    pub fn validated_session(&self) -> Result<Option<i32>, String> {
70        if self.session == Some(futu_qot::subscription_plan::SESSION_OVERNIGHT) {
71            return Err(
72                "history_kline: session=OVERNIGHT (4) is not supported; use ETH(2) or ALL(3)"
73                    .to_string(),
74            );
75        }
76        Ok(self.session)
77    }
78
79    pub fn decoded_next_req_key(&self) -> Result<Option<Vec<u8>>, String> {
80        let Some(key) = self.next_req_key.as_deref() else {
81            return Ok(None);
82        };
83        use base64::Engine as _;
84        base64::engine::general_purpose::STANDARD
85            .decode(key)
86            .map(Some)
87            .map_err(|e| format!("history_kline next_req_key must be base64: {e}"))
88    }
89}
90
91#[derive(Debug, Deserialize, schemars::JsonSchema)]
92#[serde(deny_unknown_fields)]
93pub struct ReferenceReq {
94    #[schemars(description = "Underlying symbol (e.g. HK.00700, US.AAPL); alias: code / stock")]
95    // v1.4.84 §5 B1
96    #[serde(alias = "code", alias = "stock")]
97    pub symbol: String,
98    // v1.4.41 (external reviewer v1.4.40 报告 P2.5 修): description 从 "warrant|future|option
99    // (default option)" 改成 "warrant|future (default warrant)"。真实 backend
100    // 不支持 reference_type=option(返 "unsupported reference type"),旧 schema
101    // 谎报误导 LLM agent。option 场景用 `futu_get_option_chain` 或 `futu_snapshot`。
102    #[schemars(
103        description = "Reference type: warrant|future (default warrant). Note: option is NOT supported — use futu_get_option_chain instead."
104    )]
105    #[serde(default = "default_reference_type")]
106    pub reference_type: String,
107}
108
109#[derive(Debug, Deserialize, schemars::JsonSchema)]
110#[serde(deny_unknown_fields)]
111pub struct OptionChainReq {
112    #[schemars(
113        description = "Underlying stock symbol (e.g. HK.00700, US.AAPL); alias: symbol / owner / code / stock"
114    )]
115    // v1.4.84 §5 B1
116    #[serde(alias = "symbol", alias = "owner", alias = "code", alias = "stock")]
117    pub owner_symbol: String,
118    #[schemars(
119        description = "Expiry range begin date yyyy-MM-dd; alias: begin / start_time / from"
120    )]
121    #[serde(alias = "begin", alias = "start_time", alias = "from")]
122    pub begin_time: String,
123    #[schemars(description = "Expiry range end date yyyy-MM-dd; alias: end / to")]
124    #[serde(alias = "end", alias = "to")]
125    pub end_time: String,
126    #[schemars(description = "Option type: all|call|put (default all)")]
127    pub option_type: Option<String>,
128
129    // v1.4.38 Phase 3: server-side Greek filter (via backend CMD 6736)
130    // 所有 filter 字段 optional;全 None 时不发送 filter 请求,保持 v1.4.37 行为
131    #[schemars(
132        description = "Optional Greek filter: only return options with delta in [min, max]; min must be <= max when both are provided. Typical ATM range: 0.3 to 0.7 for calls, -0.7 to -0.3 for puts."
133    )]
134    pub delta_min: Option<f64>,
135    #[schemars(description = "See delta_min; must be >= delta_min when both are provided.")]
136    pub delta_max: Option<f64>,
137    #[schemars(
138        description = "Implied volatility filter min (decimal, e.g. 0.3 = 30%); must be <= iv_max when both are provided."
139    )]
140    pub iv_min: Option<f64>,
141    #[schemars(description = "See iv_min; must be >= iv_min when both are provided.")]
142    pub iv_max: Option<f64>,
143    #[schemars(
144        description = "Open interest (contracts) filter min, integer; must be <= oi_max when both are provided."
145    )]
146    pub oi_min: Option<f64>,
147    #[schemars(description = "See oi_min; must be >= oi_min when both are provided.")]
148    pub oi_max: Option<f64>,
149    #[schemars(
150        description = "Gamma filter min (decimal); must be <= gamma_max when both are provided."
151    )]
152    pub gamma_min: Option<f64>,
153    #[schemars(description = "See gamma_min; must be >= gamma_min when both are provided.")]
154    pub gamma_max: Option<f64>,
155    #[schemars(
156        description = "Vega filter min (decimal); must be <= vega_max when both are provided."
157    )]
158    pub vega_min: Option<f64>,
159    #[schemars(description = "See vega_min; must be >= vega_min when both are provided.")]
160    pub vega_max: Option<f64>,
161    #[schemars(
162        description = "Theta filter min (decimal); must be <= theta_max when both are provided."
163    )]
164    pub theta_min: Option<f64>,
165    #[schemars(description = "See theta_min; must be >= theta_min when both are provided.")]
166    pub theta_max: Option<f64>,
167}
168
169impl OptionChainReq {
170    pub fn validate(&self) -> Result<(), String> {
171        let tool = "futu_get_option_chain";
172        validate_optional_f64_min_max(tool, "iv", self.iv_min, self.iv_max)?;
173        validate_optional_f64_min_max(tool, "delta", self.delta_min, self.delta_max)?;
174        validate_optional_f64_min_max(tool, "gamma", self.gamma_min, self.gamma_max)?;
175        validate_optional_f64_min_max(tool, "vega", self.vega_min, self.vega_max)?;
176        validate_optional_f64_min_max(tool, "theta", self.theta_min, self.theta_max)?;
177        validate_optional_f64_min_max(tool, "oi", self.oi_min, self.oi_max)?;
178        Ok(())
179    }
180}
181
182#[derive(Debug, Deserialize, schemars::JsonSchema)]
183#[serde(deny_unknown_fields)]
184pub struct WarrantReq {
185    #[schemars(
186        description = "Underlying stock symbol (e.g. HK.00700); None = whole-market warrants. Alias: symbol / owner / code"
187    )]
188    // v1.4.84 §5 B1
189    #[serde(default, alias = "symbol", alias = "owner", alias = "code")]
190    pub owner_symbol: Option<String>,
191    /// v1.4.106 codex 0635 ζ36 F1: 暴露 begin 让用户能拿下一页. 之前 wrapper
192    /// 硬编码 begin=0, 响应却含 last_page / all_count, 调用者看到 "还有下一页"
193    /// 但无法翻页. C++ backend 自始支持 data_from / data_max_count.
194    #[schemars(description = "Pagination begin index (default 0); alias: offset / skip")]
195    #[serde(default, alias = "offset", alias = "skip")]
196    pub begin: i32,
197    #[schemars(description = "Max rows (0-200, default 20); alias: count / max_count / req_count")]
198    #[serde(
199        default = "default_warrant_num",
200        alias = "count",
201        alias = "max_count",
202        alias = "req_count"
203    )]
204    pub num: i32,
205}
206
207impl WarrantReq {
208    pub fn validate(&self) -> Result<(), String> {
209        futu_qot::page_bounds::validate_begin_num(self.begin, self.num, 200, "warrant")
210            .map(|_| ())
211            .map_err(|err| err.to_string())
212    }
213}
214
215#[derive(Debug, Deserialize, schemars::JsonSchema)]
216#[serde(deny_unknown_fields)]
217pub struct IpoListReq {
218    #[schemars(
219        description = "Market code accepted by the IPO list backend. Accept int (1=HK, 2=HK_FUTURE, 11=US, 21=SH/CN, 22=SZ, 31=SG, 41=JP, 61=MY) \
220                       OR string (\"HK\" / \"HK_FUTURE\" / \"US\" / \"SH\" / \"SZ\" / \"CN\" / \"SG\" / \"JP\" / \"MY\")."
221    )]
222    // v1.4.84 §5 B2 field migration
223    #[serde(deserialize_with = "deser_hk_hkfuture_us_cn_market_as_i32")]
224    pub market: i32,
225}
226
227impl IpoListReq {
228    pub fn validate(&self) -> Result<(), String> {
229        if matches!(self.market, 1 | 2 | 11 | 21 | 22 | 31 | 41 | 61) {
230            return Ok(());
231        }
232        Err(format!(
233            "futu_get_ipo_list market must be 1, 2, 11, 21, 22, 31, 41, or 61 (HK/HK_Future/US/SH/SZ/SG/JP/MY), got {}",
234            self.market
235        ))
236    }
237}
238
239#[derive(Debug, Deserialize, schemars::JsonSchema)]
240#[serde(deny_unknown_fields)]
241pub struct IpoCalendarReq {
242    #[schemars(
243        description = "Market code accepted by the IPO calendar projection. Accept int (1=HK, 2=HK_FUTURE, 11=US, 21=SH/CN, 22=SZ, 31=SG, 41=JP, 61=MY) OR string."
244    )]
245    #[serde(deserialize_with = "deser_hk_hkfuture_us_cn_market_as_i32")]
246    pub market: i32,
247    #[schemars(
248        description = "IPO calendar event filter: list/apply/apply_start/apply_end/winning/approval/issue_confirm/price_confirm_start/price_confirm_end/inquiry_start/inquiry_end/draw. Alias: eventTypes"
249    )]
250    #[serde(default, alias = "eventTypes")]
251    pub event_types: Vec<String>,
252    #[schemars(description = "Optional begin date YYYYMMDD. Alias: beginDate")]
253    #[serde(default, alias = "beginDate")]
254    pub begin_date: Option<String>,
255    #[schemars(description = "Optional end date YYYYMMDD. Alias: endDate")]
256    #[serde(default, alias = "endDate")]
257    pub end_date: Option<String>,
258}
259
260impl IpoCalendarReq {
261    pub fn validate(&self) -> Result<(), String> {
262        if !matches!(self.market, 1 | 2 | 11 | 21 | 22 | 31 | 41 | 61) {
263            return Err(format!(
264                "futu_get_ipo_calendar market must be 1, 2, 11, 21, 22, 31, 41, or 61 (HK/HK_Future/US/SH/SZ/SG/JP/MY), got {}",
265                self.market
266            ));
267        }
268        for (field, value) in [
269            ("begin_date", self.begin_date.as_deref()),
270            ("end_date", self.end_date.as_deref()),
271        ] {
272            if let Some(value) = value
273                && !is_yyyymmdd(value)
274            {
275                return Err(format!(
276                    "{field} must be YYYYMMDD for futu_get_ipo_calendar"
277                ));
278            }
279        }
280        if let (Some(begin), Some(end)) = (&self.begin_date, &self.end_date)
281            && begin > end
282        {
283            return Err(
284                "begin_date must be earlier than or equal to end_date for futu_get_ipo_calendar"
285                    .to_string(),
286            );
287        }
288        if let Some(event) = self
289            .event_types
290            .iter()
291            .find(|event| !is_supported_ipo_event_type(event))
292        {
293            return Err(format!(
294                "unsupported event_type {event:?} for futu_get_ipo_calendar"
295            ));
296        }
297        Ok(())
298    }
299}
300
301#[derive(Debug, Deserialize, schemars::JsonSchema)]
302#[serde(deny_unknown_fields)]
303pub struct FutureInfoReq {
304    #[schemars(
305        description = "Array of future contract symbols in MARKET.CODE format \
306                       (e.g. [\"HK.HSImain\", \"US.MNQmain\"]). Alias: stocks / \
307                       code_list / symbol_list / security_list"
308    )]
309    // v1.4.84 §5 B1
310    #[serde(
311        alias = "stocks",
312        alias = "code_list",
313        alias = "symbol_list",
314        alias = "security_list"
315    )]
316    pub symbols: Vec<String>,
317}