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};
5use futu_surface_spec::input::validate_history_session_id;
6
7#[derive(Debug, Deserialize, schemars::JsonSchema)]
8#[serde(deny_unknown_fields)]
9pub struct HistoryKLineReq {
10    #[schemars(description = "Security symbol (MARKET.CODE); alias: code / stock")]
11    // v1.4.83 §5 Phase 3
12    #[serde(alias = "code", alias = "stock")]
13    pub symbol: String,
14    #[schemars(
15        description = "K-line type: day|week|month|quarter|year|1min|3min|5min|10min|15min|30min|60min|120min|180min|240min \
16                       (default day); alias: ktype / k_type"
17    )]
18    #[serde(default = "default_kl_type", alias = "ktype", alias = "k_type")]
19    pub kl_type: String,
20    #[schemars(description = "Rehab type: none|forward|backward (default none)")]
21    #[serde(default = "default_rehab_none")]
22    pub rehab_type: String,
23    #[schemars(description = "Start date yyyy-MM-dd; alias: begin_time / start_time / from")]
24    #[serde(alias = "begin_time", alias = "start_time", alias = "from")]
25    pub begin: String,
26    #[schemars(description = "End date yyyy-MM-dd; alias: end_time / to")]
27    #[serde(alias = "end_time", alias = "to")]
28    pub end: String,
29    #[schemars(
30        description = "Max number of candles to return (default 1000, range 1-1000). \
31                       If omitted, the gateway uses 1000; pass explicit 0 to request no limit \
32                       only when you can handle a large response. alias: num / count / req_count"
33    )]
34    // v1.4.106 codex 0635 ζ36 F5: schema 文案承诺 default 1000, 给一个真 default.
35    // 之前是 Option<i32> 不带 serde(default), MCP 用户省略 max_count 实际不是 1000
36    // 而是不传限制 → 响应规模超 schema 预期 → 上下文膨胀.
37    #[serde(
38        default = "default_history_kline_max_count",
39        alias = "num",
40        alias = "count",
41        alias = "req_count"
42    )]
43    pub max_count: Option<i32>,
44    #[schemars(description = "Optional Qot_Common.KLFields bitmask; alias: needKLFieldsFlag")]
45    #[serde(default, alias = "needKLFieldsFlag")]
46    pub need_kl_fields_flag: Option<i64>,
47    #[schemars(
48        description = "Fetch US extended-hours K-line data when supported; alias: extendedTime"
49    )]
50    #[serde(default, alias = "extendedTime")]
51    pub extended_time: Option<bool>,
52    #[schemars(
53        description = "Session: 0=NONE, 1=RTH, 2=ETH, 3=ALL. OVERNIGHT is not supported for this operation."
54    )]
55    #[serde(default)]
56    pub session: Option<i32>,
57    #[schemars(
58        description = "Pagination key from previous response, base64-encoded; alias: nextReqKey / next_key"
59    )]
60    #[serde(default, alias = "nextReqKey", alias = "next_key")]
61    pub next_req_key: Option<String>,
62}
63
64impl HistoryKLineReq {
65    pub fn validated_max_count(&self) -> Result<Option<i32>, String> {
66        futu_core::qot_page_bounds::validate_optional_max_count(
67            self.max_count,
68            1000,
69            "history_kline",
70        )
71        .map_err(|e| e.to_string())
72    }
73
74    pub fn validated_session(&self) -> Result<Option<i32>, String> {
75        validate_history_session_id(self.session)
76            .map_err(|error| format!("history_kline: invalid session: {error}"))
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_core::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_ipo_market_as_i32")]
224    pub market: i32,
225}
226
227impl IpoListReq {
228    pub fn validate(&self) -> Result<(), String> {
229        if futu_core::qot_endpoint_market::is_ipo_market(self.market) {
230            return Ok(());
231        }
232        Err(format!(
233            "futu_get_ipo_list market must be {}, got {}",
234            futu_core::qot_endpoint_market::QOT_IPO_MARKET_VALID_VALUES,
235            self.market,
236        ))
237    }
238}
239
240#[derive(Debug, Deserialize, schemars::JsonSchema)]
241#[serde(deny_unknown_fields)]
242pub struct IpoCalendarReq {
243    #[schemars(
244        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."
245    )]
246    #[serde(deserialize_with = "deser_ipo_market_as_i32")]
247    pub market: i32,
248    #[schemars(
249        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"
250    )]
251    #[serde(default, alias = "eventTypes")]
252    pub event_types: Vec<String>,
253    #[schemars(description = "Optional begin date YYYYMMDD. Alias: beginDate")]
254    #[serde(default, alias = "beginDate")]
255    pub begin_date: Option<String>,
256    #[schemars(description = "Optional end date YYYYMMDD. Alias: endDate")]
257    #[serde(default, alias = "endDate")]
258    pub end_date: Option<String>,
259}
260
261impl IpoCalendarReq {
262    pub fn validate(&self) -> Result<(), String> {
263        if !futu_core::qot_endpoint_market::is_ipo_market(self.market) {
264            return Err(format!(
265                "futu_get_ipo_calendar market must be {}, got {}",
266                futu_core::qot_endpoint_market::QOT_IPO_MARKET_VALID_VALUES,
267                self.market,
268            ));
269        }
270        for (field, value) in [
271            ("begin_date", self.begin_date.as_deref()),
272            ("end_date", self.end_date.as_deref()),
273        ] {
274            if let Some(value) = value
275                && !is_yyyymmdd(value)
276            {
277                return Err(format!(
278                    "{field} must be YYYYMMDD for futu_get_ipo_calendar"
279                ));
280            }
281        }
282        if let (Some(begin), Some(end)) = (&self.begin_date, &self.end_date)
283            && begin > end
284        {
285            return Err(
286                "begin_date must be earlier than or equal to end_date for futu_get_ipo_calendar"
287                    .to_string(),
288            );
289        }
290        if let Some(event) = self
291            .event_types
292            .iter()
293            .find(|event| !is_supported_ipo_event_type(event))
294        {
295            return Err(format!(
296                "unsupported event_type {event:?} for futu_get_ipo_calendar"
297            ));
298        }
299        Ok(())
300    }
301}
302
303#[derive(Debug, Deserialize, schemars::JsonSchema)]
304#[serde(deny_unknown_fields)]
305pub struct FutureInfoReq {
306    #[schemars(
307        description = "Array of future contract symbols in MARKET.CODE format \
308                       (e.g. [\"HK.HSImain\", \"US.MNQmain\"]). Alias: stocks / \
309                       code_list / symbol_list / security_list"
310    )]
311    // v1.4.84 §5 B1
312    #[serde(
313        alias = "stocks",
314        alias = "code_list",
315        alias = "symbol_list",
316        alias = "security_list"
317    )]
318    pub symbols: Vec<String>,
319}