1use 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 #[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 #[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 #[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_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 #[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 #[serde(
313 alias = "stocks",
314 alias = "code_list",
315 alias = "symbol_list",
316 alias = "security_list"
317 )]
318 pub symbols: Vec<String>,
319}