1use bytes::Bytes;
4use futu_command_spec::QotReadOperation;
5use futu_core::error::{FutuError, Result};
6use futu_domain_qot_option_screen::{
7 EarningsScreenerPlan, OptionProductContractPlan, ProductFilterInput, SellerScreenerPlan,
8 ZeroDteScreenerPlan, api_expiration_to_internal,
9};
10
11use crate::command_runtime::execute_qot_read_with_reserved;
12use crate::conn::BackendConn;
13use crate::proto_internal::option_product_zone as proto;
14
15#[must_use]
16pub fn build_zero_dte_screener_request(
17 plan: &ZeroDteScreenerPlan,
18) -> proto::OptionZeroDteScreenerReq {
19 proto::OptionZeroDteScreenerReq {
20 strategy: Some(proto::ScreenStrategy {
21 market_type_list: vec![plan.backend_market],
22 filter_group_list: plan
23 .filters
24 .iter()
25 .filter_map(zero_dte_underlying_filter)
26 .collect(),
27 }),
28 sort_obj: Some(proto::ZeroDteSortObj {
29 sort_field: Some(zero_dte_sort_field(plan.sort_type)),
30 is_asc: Some(plan.is_asc),
31 }),
32 begin_index: plan.from,
33 count: Some(plan.count),
34 }
35}
36
37#[must_use]
38pub fn build_zero_dte_contract_request(
39 plan: &OptionProductContractPlan,
40) -> proto::OptionZeroDteContractListReq {
41 let chain = &plan.chain_info;
42 proto::OptionZeroDteContractListReq {
43 stock_id: Some(plan.owner_stock_id as i64),
44 strike_date_timestamp: Some(plan.strike_date_timestamp),
45 filter_group_list: plan
46 .filters
47 .iter()
48 .filter_map(zero_dte_contract_filter)
49 .collect(),
50 sort_obj: (plan.sort_type != 0).then(|| proto::ZeroDteContractObj {
51 sort_field: Some(zero_dte_contract_sort_field(plan.sort_type)),
52 is_asc: Some(plan.is_asc),
53 }),
54 option_chain: Some(proto::OptionChainType {
55 strike_date: chain.strike_date_timestamp,
58 product_code: chain.product_code.clone(),
59 hp_multiplier: chain.multiplier.map(|value| (value * 1e9) as i64),
60 hp_contract_share_size: chain.contract_share_size.map(|value| (value * 1e9) as i64),
61 expiration_type: chain.expiration_type.and_then(api_expiration_to_internal),
62 underlying_stock_id: chain.underlying_stock_id,
63 }),
64 }
65}
66
67#[must_use]
68pub fn build_earnings_screener_request(
69 plan: &EarningsScreenerPlan,
70) -> proto::ReportUnderlyingListReq {
71 let mut filter_group_list = Vec::new();
72 if plan.add_default_earnings_day_filter {
73 filter_group_list.push(underlying_interval_group(
74 301,
75 Some(0),
76 Some(365),
77 false,
78 false,
79 ));
80 }
81 filter_group_list.extend(plan.filters.iter().filter_map(earnings_filter));
82 proto::ReportUnderlyingListReq {
83 strategy: Some(proto::ScreenStrategy {
84 market_type_list: vec![plan.backend_market],
85 filter_group_list,
86 }),
87 sort_obj: vec![earnings_sort(plan.sort_type, plan.is_asc)],
88 begin_index: plan.from,
89 count: Some(plan.count),
90 is_card_scene: None,
91 }
92}
93
94#[must_use]
95pub fn build_seller_screener_request(plan: &SellerScreenerPlan) -> proto::OptionSellerScreenerReq {
96 let mut filter_group_list = seller_default_filters(plan.default_filter_mask);
97 filter_group_list.extend(plan.filters.iter().filter_map(seller_filter));
98 proto::OptionSellerScreenerReq {
99 option_id_list: Vec::new(),
100 seller_type: Some(plan.seller_type),
101 account_id: None,
102 strategy: Some(proto::ScreenStrategy {
103 market_type_list: vec![plan.backend_market],
104 filter_group_list,
105 }),
106 need_field: Some(proto::OptionItem {
107 option_id: Some(0),
111 option_name: Some(String::new()),
112 hp_strike_price: Some(0),
113 strike_date_timestamp: Some(0),
114 option_type: Some(0),
115 left_day: Some(0),
116 premium: Some(0),
117 otm_degree: Some(0),
118 implied_volatility: Some(0),
119 interval_return: Some(0),
120 sell_annualized_return: Some(0),
121 striked_interval_return: Some(0),
122 striked_annualized_return: Some(0),
123 itm_probability: Some(0),
124 stock_price: Some(0),
125 option_price: Some(0),
126 }),
127 sort_obj: (plan.sort_type != 0).then(|| proto::SellerSortObj {
128 sort_field: Some(seller_sort_field(plan.sort_type)),
129 is_asc: Some(plan.is_asc),
130 }),
131 }
132}
133
134pub async fn pull_zero_dte_screener(
135 backend: &BackendConn,
136 plan: &ZeroDteScreenerPlan,
137) -> Result<proto::OptionZeroDteScreenerRsp> {
138 pull(
139 backend,
140 QotReadOperation::OptionZeroDteScreener,
141 build_zero_dte_screener_request(plan),
142 plan.quote_mkt_type,
143 "option zero-DTE screener",
144 )
145 .await
146}
147
148pub async fn pull_zero_dte_contract(
149 backend: &BackendConn,
150 plan: &OptionProductContractPlan,
151) -> Result<proto::OptionZeroDteContractListRsp> {
152 pull(
153 backend,
154 QotReadOperation::OptionZeroDteContract,
155 build_zero_dte_contract_request(plan),
156 plan.quote_mkt_type,
157 "option zero-DTE contract",
158 )
159 .await
160}
161
162pub async fn pull_earnings_screener(
163 backend: &BackendConn,
164 plan: &EarningsScreenerPlan,
165) -> Result<proto::ReportUnderlyingListRsp> {
166 pull(
167 backend,
168 QotReadOperation::OptionEarningsScreener,
169 build_earnings_screener_request(plan),
170 plan.quote_mkt_type,
171 "option earnings screener",
172 )
173 .await
174}
175
176pub async fn pull_seller_screener(
177 backend: &BackendConn,
178 plan: &SellerScreenerPlan,
179) -> Result<proto::OptionSellerScreenerRsp> {
180 pull(
181 backend,
182 QotReadOperation::OptionSellerScreener,
183 build_seller_screener_request(plan),
184 plan.quote_mkt_type,
185 "option seller screener",
186 )
187 .await
188}
189
190async fn pull<Req, Rsp>(
191 backend: &BackendConn,
192 operation: QotReadOperation,
193 request: Req,
194 quote_mkt_type: u8,
195 label: &'static str,
196) -> Result<Rsp>
197where
198 Req: prost::Message,
199 Rsp: prost::Message + Default + ProductZoneResponse,
200{
201 let mut reserved = [0_u8; 10];
202 reserved[0] = quote_mkt_type;
206 let response = execute_qot_read_with_reserved(
207 backend,
208 operation,
209 Bytes::from(request.encode_to_vec()),
210 reserved,
211 )
212 .await?;
213 let decoded = Rsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
214 match decoded.ret_code() {
215 Some(0) => Ok(decoded),
216 Some(code) => Err(FutuError::ServerError {
217 ret_type: code,
218 msg: decoded
219 .err_msg()
220 .unwrap_or_else(|| format!("{label} backend rejected request")),
221 }),
222 None => Err(FutuError::Codec(format!(
223 "{label} backend response missing ret_code"
224 ))),
225 }
226}
227
228trait ProductZoneResponse {
229 fn ret_code(&self) -> Option<i32>;
230 fn err_msg(&self) -> Option<String>;
231}
232
233macro_rules! impl_response {
234 ($($type:ty),+ $(,)?) => {$(
235 impl ProductZoneResponse for $type {
236 fn ret_code(&self) -> Option<i32> { self.ret_code }
237 fn err_msg(&self) -> Option<String> { self.err_msg.clone() }
238 }
239 )+};
240}
241
242impl_response!(
243 proto::OptionZeroDteScreenerRsp,
244 proto::OptionZeroDteContractListRsp,
245 proto::ReportUnderlyingListRsp,
246 proto::OptionSellerScreenerRsp,
247);
248
249fn zero_dte_underlying_filter(filter: &ProductFilterInput) -> Option<proto::FilterGroup> {
250 let (kind, multiplier, exact_security, exact_values) = match filter.indicator_type {
254 1 => (101, 1.0, true, false),
255 2 => (308, 1.0, false, true),
256 3 => (201, 1.0, false, false),
257 4 => (202, 1.0, false, false),
258 5 => (203, 1e5, false, false),
259 6 => (204, 1e5, false, false),
260 7 => (205, 1e5, false, false),
261 8 => (206, 1e5, false, false),
262 9 => (402, 1e9, false, false),
263 10 => (403, 1e5, false, false),
264 _ => return None,
265 };
266 Some(underlying_group(indicator(
267 kind,
268 filter_value(filter, multiplier, exact_security, exact_values),
269 )))
270}
271
272fn zero_dte_contract_filter(filter: &ProductFilterInput) -> Option<proto::FilterGroup> {
273 let (kind, multiplier, exact_values) = match filter.indicator_type {
276 1 => (1003, 1.0, true),
277 2 => (2011, 1.0, false),
278 3 => (2013, 1.0, false),
279 4 => (3001, 1e5, false),
280 5 => (3004, 1e5, false),
281 6 => (3005, 1e5, false),
282 7 => (3007, 1e5, false),
283 8 => (3006, 1e5, false),
284 9 => (3008, 1e5, false),
285 10 => (2002, 1e9, false),
286 11 => (2010, 1e5, false),
287 12 => (3023, 1e9, false),
288 13 => (3011, 1e9, false),
292 14 => (3013, 1e9, false),
293 15 => (3014, 1e9, false),
294 _ => return None,
295 };
296 Some(option_group(indicator(
297 kind,
298 filter_value(filter, multiplier, false, exact_values),
299 )))
300}
301
302fn earnings_filter(filter: &ProductFilterInput) -> Option<proto::FilterGroup> {
303 let (kind, multiplier, exact_security, exact_values, expiration_values) =
306 match filter.indicator_type {
307 1 => (101, 1.0, true, false, false),
308 2 => (500, 1.0, false, true, false),
309 3 => (501, 1.0, true, false, false),
310 4 => (401, 1e3, false, false, false),
311 5 => (502, 1.0, false, false, true),
312 6 => (203, 1e5, false, false, false),
313 7 => (302, 1e5, false, false, false),
314 8 => (303, 1e5, false, false, false),
315 9 => (205, 1e5, false, false, false),
316 10 => (206, 1e5, false, false, false),
317 11 => (201, 1.0, false, false, false),
318 12 => (202, 1.0, false, false, false),
319 13 => (402, 1e9, false, false, false),
320 14 => (403, 1e5, false, false, false),
321 15 => (503, 1e5, false, false, false),
322 16 => (304, 1e5, false, false, false),
323 17 => (305, 1e5, false, false, false),
324 18 => (307, 1e5, false, false, false),
325 19 => (306, 1e5, false, false, false),
326 20 => (301, 1.0, false, false, false),
327 _ => return None,
328 };
329 let value = if expiration_values {
330 exact_expiration_value(filter)
331 } else {
332 filter_value(filter, multiplier, exact_security, exact_values)
333 };
334 Some(underlying_group(indicator(kind, value)))
335}
336
337fn seller_filter(filter: &ProductFilterInput) -> Option<proto::FilterGroup> {
338 let (kind, multiplier, side, exact_security, exact_values, expiration_values) =
341 match filter.indicator_type {
342 1 => (101, 1.0, false, true, false, false),
343 2 => (105, 1.0, false, false, true, false),
344 3 => (201, 1.0, false, false, false, false),
345 4 => (202, 1.0, false, false, false, false),
346 5 => (203, 1e5, false, false, false, false),
347 6 => (204, 1e5, false, false, false, false),
348 7 => (205, 1e5, false, false, false, false),
349 8 => (206, 1e5, false, false, false, false),
350 9 => (401, 1e3, false, false, false, false),
351 10 => (402, 1e9, false, false, false, false),
352 11 => (403, 1e5, false, false, false, false),
353 12 => (501, 1.0, false, false, true, false),
354 13 => (502, 1.0, false, false, false, true),
355 14 => (1002, 1.0, true, false, false, false),
356 15 => (1005, 1.0, true, false, false, true),
357 16 => (1007, 1.0, true, false, true, false),
358 17 => (2021, 1e9, true, false, false, false),
359 18 => (3021, 1e5, true, false, false, false),
360 19 => (3022, 1e5, true, false, false, false),
361 20 => (3018, 1e5, true, false, false, false),
362 21 => (3020, 1e5, true, false, false, false),
363 22 => (3001, 1e5, true, false, false, false),
364 23 => (2004, 1e9, true, false, false, false),
365 24 => (2005, 1e9, true, false, false, false),
366 25 => (2011, 1.0, true, false, false, false),
367 26 => (2013, 1.0, true, false, false, false),
368 _ => return None,
369 };
370 let value = if expiration_values {
371 exact_expiration_value(filter)
372 } else {
373 filter_value(filter, multiplier, exact_security, exact_values)
374 };
375 let indicator = indicator(kind, value);
376 Some(if side {
377 option_group(indicator)
378 } else {
379 underlying_group(indicator)
380 })
381}
382
383fn filter_value(
384 filter: &ProductFilterInput,
385 multiplier: f64,
386 exact_security: bool,
387 exact_values: bool,
388) -> Option<proto::IndicatorValue> {
389 if exact_security {
390 return Some(proto::IndicatorValue {
391 value_list: filter
392 .security_stock_ids
393 .iter()
394 .map(|value| *value as i64)
395 .collect(),
396 value_interval: None,
397 value_string_list: Vec::new(),
398 });
399 }
400 if exact_values {
401 return Some(proto::IndicatorValue {
402 value_list: filter.value_list.clone(),
403 value_interval: None,
404 value_string_list: Vec::new(),
405 });
406 }
407 filter.interval.map(|interval| proto::IndicatorValue {
408 value_list: Vec::new(),
409 value_interval: Some(proto::indicator_value::Interval {
410 min_value: interval.min.map(|bound| (bound.value * multiplier) as i64),
411 max_value: interval.max.map(|bound| (bound.value * multiplier) as i64),
412 exclude_min: interval.min.map(|bound| !bound.includes),
413 exclude_max: interval.max.map(|bound| !bound.includes),
414 }),
415 value_string_list: Vec::new(),
416 })
417}
418
419fn exact_expiration_value(filter: &ProductFilterInput) -> Option<proto::IndicatorValue> {
420 Some(proto::IndicatorValue {
421 value_list: filter
422 .value_list
423 .iter()
424 .filter_map(|value| i32::try_from(*value).ok())
425 .filter_map(api_expiration_to_internal)
426 .map(i64::from)
427 .collect(),
428 value_interval: None,
429 value_string_list: Vec::new(),
430 })
431}
432
433fn indicator(kind: i32, value: Option<proto::IndicatorValue>) -> proto::ScreenerIndicator {
434 proto::ScreenerIndicator {
435 indicator_type: Some(kind),
436 indicator_value: value,
437 }
438}
439
440fn underlying_group(indicator: proto::ScreenerIndicator) -> proto::FilterGroup {
441 proto::FilterGroup {
442 underlying_list: vec![indicator],
443 option_list: Vec::new(),
444 }
445}
446
447fn option_group(indicator: proto::ScreenerIndicator) -> proto::FilterGroup {
448 proto::FilterGroup {
449 underlying_list: Vec::new(),
450 option_list: vec![indicator],
451 }
452}
453
454fn underlying_interval_group(
455 kind: i32,
456 min: Option<i64>,
457 max: Option<i64>,
458 exclude_min: bool,
459 exclude_max: bool,
460) -> proto::FilterGroup {
461 underlying_group(indicator(
462 kind,
463 Some(proto::IndicatorValue {
464 value_list: Vec::new(),
465 value_interval: Some(proto::indicator_value::Interval {
466 min_value: min,
467 max_value: max,
468 exclude_min: min.map(|_| exclude_min),
469 exclude_max: max.map(|_| exclude_max),
470 }),
471 value_string_list: Vec::new(),
472 }),
473 ))
474}
475
476fn option_interval_group(kind: i32, min: i64) -> proto::FilterGroup {
477 option_group(indicator(
478 kind,
479 Some(proto::IndicatorValue {
480 value_list: Vec::new(),
481 value_interval: Some(proto::indicator_value::Interval {
482 min_value: Some(min),
483 max_value: None,
484 exclude_min: Some(true),
485 exclude_max: None,
486 }),
487 value_string_list: Vec::new(),
488 }),
489 ))
490}
491
492fn seller_default_filters(mask: u16) -> Vec<proto::FilterGroup> {
493 let definitions = [
497 (false, 401, 10_000_000_000_000_i64),
498 (false, 402, 1_000_000_000),
499 (false, 201, 0),
500 (false, 202, 0),
501 (true, 1002, 0),
502 (true, 2004, 10_000_000),
503 (true, 2005, 10_000_000),
504 (true, 3018, 0),
505 (true, 2011, 0),
506 (true, 2013, 0),
507 ];
508 definitions
509 .into_iter()
510 .enumerate()
511 .filter(|(bit, _)| mask & (1 << bit) != 0)
512 .map(|(_, (option_side, kind, min))| {
513 if option_side {
514 option_interval_group(kind, min)
515 } else {
516 underlying_interval_group(kind, Some(min), None, true, false)
517 }
518 })
519 .collect()
520}
521
522fn zero_dte_sort_field(sort_type: i32) -> proto::UnderlyingStatisticInfo {
523 let mut field = proto::UnderlyingStatisticInfo::default();
524 match sort_type {
525 2 => field.iv = Some(0),
526 3 => field.change_ratio = Some(0),
527 4 => field.open_interest = Some(0),
528 5 => field.market_cap = Some(0),
529 _ => field.volume = Some(0),
530 }
531 field
532}
533
534fn zero_dte_contract_sort_field(sort_type: i32) -> proto::ZeroDteOptionItem {
535 let mut field = proto::ZeroDteOptionItem::default();
536 match sort_type {
537 2 => field.open_interest = Some(0),
538 3 => field.implied_volatility = Some(0),
539 4 => field.delta = Some(0),
540 _ => field.volume = Some(0),
541 }
542 field
543}
544
545fn earnings_sort(sort_type: i32, is_asc: bool) -> proto::ReportSortObj {
546 if sort_type == 1 {
547 return proto::ReportSortObj {
548 underlying_info: None,
549 financial_item: Some(proto::FinancialItem::default()),
550 is_asc: Some(i32::from(is_asc)),
551 };
552 }
553 let mut field = proto::UnderlyingStatisticInfo::default();
554 match sort_type {
555 2 => field.volume = Some(0),
556 3 => field.iv = Some(0),
557 4 => field.market_cap = Some(0),
558 5 => field.change_ratio = Some(0),
559 6 => field.price = Some(0),
560 7 => field.iv_rank = Some(0),
561 8 => field.iv_percentile = Some(0),
562 9 => field.hv = Some(0),
563 10 => field.open_interest = Some(0),
564 11 => field.last_report_iv_crush = Some(0),
565 12 => field.history_report_iv_crush = Some(0),
566 13 => field.last_report_chg_ratio = Some(0),
567 14 => field.history_report_chg_ratio = Some(0),
568 15 => field.estimate_eps_yoy = Some(0),
569 16 => field.estimate_revenue_yoy = Some(0),
570 17 => field.expected_move_ratio_newest = Some(0),
571 _ => {}
572 }
573 proto::ReportSortObj {
574 underlying_info: Some(field),
575 financial_item: None,
576 is_asc: Some(i32::from(is_asc)),
577 }
578}
579
580fn seller_sort_field(sort_type: i32) -> proto::OptionItem {
581 let mut field = proto::OptionItem::default();
582 match sort_type {
583 2 => field.interval_return = Some(0),
584 3 => field.itm_probability = Some(0),
585 4 => field.premium = Some(0),
586 _ => field.sell_annualized_return = Some(0),
587 }
588 field
589}