1use 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 #[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 #[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 #[serde(alias = "code", alias = "stock")]
97 pub symbol: String,
98 #[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 #[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 #[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 #[serde(default, alias = "symbol", alias = "owner", alias = "code")]
190 pub owner_symbol: Option<String>,
191 #[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 #[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 #[serde(
311 alias = "stocks",
312 alias = "code_list",
313 alias = "symbol_list",
314 alias = "security_list"
315 )]
316 pub symbols: Vec<String>,
317}