1use bytes::Bytes;
4use prost::Message as _;
5
6use futu_command_spec::QotReadOperation;
7use futu_core::error::{FutuError, Result};
8use futu_domain_qot_option::{OptionRankPlan, RankFilter, RankFilterInterval, UnderlyingRankPlan};
9
10use crate::command_runtime::execute_qot_read_with_reserved;
11use crate::conn::BackendConn;
12use crate::proto_internal::{
13 ft_cmd_option_screener as option_screen, ft_cmd_screener as screen,
14 ft_cmd_underlying_screener as underlying_screen, option_move_reader as move_reader,
15 option_rank_reader as rank_reader,
16};
17
18#[derive(Debug)]
19pub struct UnderlyingRankBackendPage {
20 pub request_serial_no: u32,
21 pub trading_date: i64,
22 pub response: move_reader::GetUnderlyingRankRsp,
23}
24
25#[derive(Debug)]
26pub struct OptionRankBackendPage {
27 pub request_serial_no: u32,
28 pub trading_date: i64,
29 pub response: rank_reader::GetOptionRankRsp,
30}
31
32pub async fn pull_underlying_rank(
33 backend: &BackendConn,
34 plan: &UnderlyingRankPlan,
35) -> Result<UnderlyingRankBackendPage> {
36 let trading_date = match plan.trading_date.filter(|value| *value > 0) {
37 Some(value) => value,
38 None => {
39 let request = build_underlying_rank_date_request(plan);
40 let response = execute(
41 backend,
42 QotReadOperation::OptionUnderlyingRankDate,
43 request,
44 plan.quote_mkt_type,
45 )
46 .await?;
47 let response = move_reader::GetUnderlyingRankDateRsp::decode(response.body.as_ref())
48 .map_err(FutuError::Proto)?;
49 require_code(response.code, None, "option underlying rank date")?;
50 response.date_list.first().copied().ok_or_else(|| {
51 FutuError::Codec("option underlying rank date returned an empty date list".into())
52 })?
53 }
54 };
55 let response = execute(
56 backend,
57 QotReadOperation::OptionUnderlyingRank,
58 build_underlying_rank_request(plan, trading_date),
59 plan.quote_mkt_type,
60 )
61 .await?;
62 let request_serial_no = response.request_serial_no;
63 let decoded = move_reader::GetUnderlyingRankRsp::decode(response.body.as_ref())
64 .map_err(FutuError::Proto)?;
65 require_code(decoded.code, None, "option underlying rank")?;
66 Ok(UnderlyingRankBackendPage {
67 request_serial_no,
68 trading_date,
69 response: decoded,
70 })
71}
72
73pub async fn pull_option_rank(
74 backend: &BackendConn,
75 plan: &OptionRankPlan,
76) -> Result<OptionRankBackendPage> {
77 let trading_date = match plan.trading_date.filter(|value| *value > 0) {
78 Some(value) => value,
79 None => {
80 let response = execute(
81 backend,
82 QotReadOperation::OptionRankDate,
83 build_option_rank_date_request(plan),
84 plan.quote_mkt_type,
85 )
86 .await?;
87 let response = rank_reader::GetOptionRankDateRsp::decode(response.body.as_ref())
88 .map_err(FutuError::Proto)?;
89 require_code(response.code, response.message, "option rank date")?;
90 response.date_list.first().copied().ok_or_else(|| {
91 FutuError::Codec("option rank date returned an empty date list".into())
92 })?
93 }
94 };
95 let response = execute(
96 backend,
97 QotReadOperation::OptionRank,
98 build_option_rank_request(plan, trading_date),
99 plan.quote_mkt_type,
100 )
101 .await?;
102 let request_serial_no = response.request_serial_no;
103 let decoded =
104 rank_reader::GetOptionRankRsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
105 require_code(decoded.code, decoded.message.clone(), "option rank")?;
106 let final_trading_date = decoded
107 .trade_date
108 .filter(|value| *value != 0)
109 .unwrap_or(trading_date);
110 Ok(OptionRankBackendPage {
111 request_serial_no,
112 trading_date: final_trading_date,
113 response: decoded,
114 })
115}
116
117#[must_use]
118pub fn build_underlying_rank_date_request(
119 plan: &UnderlyingRankPlan,
120) -> move_reader::GetUnderlyingRankDateReq {
121 move_reader::GetUnderlyingRankDateReq {
122 market_type: Some(plan.backend_market),
123 underlying_type: Some(plan.underlying_type),
124 from: Some(0),
125 count: Some(1),
126 rank_type: Some(i64::from(plan.sort_field)),
127 }
128}
129
130#[must_use]
131pub fn build_underlying_rank_request(
132 plan: &UnderlyingRankPlan,
133 trading_date: i64,
134) -> move_reader::GetUnderlyingRankReq {
135 move_reader::GetUnderlyingRankReq {
136 market_type: Some(plan.backend_market),
137 underlying_type: Some(plan.underlying_type),
138 sort_field: Some(plan.sort_field),
139 is_asc: Some(plan.is_asc),
140 rank_date: Some(trading_date),
141 from: Some(plan.from),
142 count: Some(plan.count),
143 secondary_sort_field: None,
144 secondary_is_asc: None,
145 strategy: (!plan.filters.is_empty()).then(|| build_underlying_strategy(&plan.filters)),
146 }
147}
148
149#[must_use]
150pub fn build_option_rank_date_request(plan: &OptionRankPlan) -> rank_reader::GetOptionRankDateReq {
151 rank_reader::GetOptionRankDateReq {
152 market_category: Some(plan.market_category as u32),
153 rank_type: Some(option_sort_field(plan.sort_type)),
154 }
155}
156
157#[must_use]
158pub fn build_option_rank_request(
159 plan: &OptionRankPlan,
160 trading_date: i64,
161) -> rank_reader::GetOptionRankReq {
162 let strategy = option_screen::ScreenStrategy {
163 id: None,
164 name: None,
165 market_category_list: vec![screener_market_category(plan.market_category)],
166 filter_group_list: build_option_filter_groups(&plan.filters),
167 };
168 let field_filter = option_screen::OptionItem {
171 option_name: Some("0".into()),
172 option_type: Some(0),
173 volume: Some(0),
174 turnover: Some(0),
175 open_interest: Some(0),
176 open_interest_market_cap: Some(0),
177 oi_day_chg: Some(0),
178 oi_day_market_cap_chg: Some(0),
179 implied_volatility: Some(0),
180 price: Some(0),
181 chg_ratio: Some(0),
182 mid_price: Some(0),
183 bid_price: Some(0),
184 bid_volume: Some(0),
185 ask_price: Some(0),
186 ask_volume: Some(0),
187 delta: Some(0),
188 gamma: Some(0),
189 theta: Some(0),
190 vega: Some(0),
191 rho: Some(0),
192 ..Default::default()
193 };
194 rank_reader::GetOptionRankReq {
195 trade_date: Some(trading_date),
196 screen_req: Some(option_screen::OptionScreenerReq {
197 strategy: Some(strategy),
198 strategy_param: None,
199 field_filter: Some(field_filter),
200 sort_list: vec![option_screen::SortObj {
201 sort_field: Some(option_sort_field(plan.sort_type)),
202 is_asc: Some(i32::from(plan.is_asc)),
203 }],
204 pagination: Some(option_screen::OffsetPageObj {
205 from: Some(plan.from),
206 count: Some(plan.count),
207 }),
208 request_exact_data: Some(1),
209 }),
210 }
211}
212
213fn build_underlying_strategy(filters: &[RankFilter]) -> underlying_screen::ScreenStrategy {
214 underlying_screen::ScreenStrategy {
218 market_category_list: Vec::new(),
219 filter_group_list: filters
220 .iter()
221 .filter_map(|filter| {
222 if !filter.has_value {
223 return Some(underlying_screen::FilterGroup {
224 filter_list: Vec::new(),
225 });
226 }
227 let (indicator_type, multiplier, values, exact_value) = match filter.indicator_type
228 {
229 1 => (
230 101,
231 1.0,
232 filter
233 .security_stock_ids
234 .iter()
235 .map(|value| *value as i64)
236 .collect(),
237 true,
238 ),
239 2 => (105, 1.0, filter.value_list.clone(), true),
240 3 => (201, 1.0, Vec::new(), false),
241 4 => (202, 1.0, Vec::new(), false),
242 5 => (203, 1e5, Vec::new(), false),
243 6 => (204, 1e5, Vec::new(), false),
244 7 => (205, 1e5, Vec::new(), false),
245 8 => (206, 1e5, Vec::new(), false),
246 9 => (207, 1e5, Vec::new(), false),
247 10 => (208, 1e5, Vec::new(), false),
248 11 => (209, 1e5, Vec::new(), false),
249 12 => (210, 1e5, Vec::new(), false),
250 13 => (401, 1e3, Vec::new(), false),
251 _ => return None,
252 };
253 Some(underlying_screen::FilterGroup {
254 filter_list: vec![underlying_screen::Filter {
255 indicator_type: Some(indicator_type),
256 indicator_value: (exact_value || filter.interval.is_some())
257 .then(|| indicator_value(values, filter.interval, multiplier)),
258 }],
259 })
260 })
261 .collect(),
262 }
263}
264
265fn build_option_filter_groups(filters: &[RankFilter]) -> Vec<option_screen::FilterGroup> {
266 filters
270 .iter()
271 .filter_map(|filter| {
272 if !filter.has_value {
273 return None;
274 }
275 let (underlying_type, option_type, multiplier, values, exact_value) =
276 match filter.indicator_type {
277 1 => (
278 Some(107),
279 None,
280 1.0,
281 filter
282 .value_list
283 .iter()
284 .filter_map(|value| match value {
285 1 => Some(3),
286 2 => Some(4),
287 _ => None,
288 })
289 .collect(),
290 true,
291 ),
292 2 => (Some(401), None, 1e3, Vec::new(), false),
293 3 => (
294 Some(101),
295 None,
296 1.0,
297 filter
298 .security_stock_ids
299 .iter()
300 .map(|value| *value as i64)
301 .collect(),
302 true,
303 ),
304 4 => (Some(203), None, 1e5, Vec::new(), false),
305 5 => (Some(204), None, 1e5, Vec::new(), false),
306 6 => (Some(205), None, 1e5, Vec::new(), false),
307 7 => (Some(206), None, 1e5, Vec::new(), false),
308 8 => (None, Some(3001), 1e5, Vec::new(), false),
309 9 => (None, Some(1003), 1.0, filter.value_list.clone(), true),
310 10 => (None, Some(1002), 1.0, Vec::new(), false),
311 11 => (None, Some(2001), 1.0, filter.value_list.clone(), true),
312 12 => (None, Some(2011), 1.0, Vec::new(), false),
313 13 => (None, Some(2013), 1.0, Vec::new(), false),
314 14 => (None, Some(3004), 1e5, Vec::new(), false),
315 15 => (None, Some(3005), 1e5, Vec::new(), false),
316 16 => (None, Some(3007), 1e5, Vec::new(), false),
317 17 => (None, Some(3006), 1e5, Vec::new(), false),
318 18 => (None, Some(3008), 1e5, Vec::new(), false),
319 _ => return None,
320 };
321 let value = (exact_value || filter.interval.is_some())
322 .then(|| indicator_value(values, filter.interval, multiplier));
323 Some(option_screen::FilterGroup {
324 underlying_list: underlying_type.map_or_else(Vec::new, |indicator_type| {
325 vec![option_screen::UnderlyingIndicator {
326 indicator_type: Some(indicator_type),
327 indicator_value: value.clone(),
328 stock_strategy_list: Vec::new(),
329 plate_list: Vec::new(),
330 sub_indicator_list: Vec::new(),
331 stock_strategy_wch_list_stocks: Vec::new(),
332 }]
333 }),
334 option_list: option_type.map_or_else(Vec::new, |indicator_type| {
335 vec![option_screen::OptionIndicator {
336 indicator_type: Some(indicator_type),
337 indicator_value: value,
338 sub_indicator_list: Vec::new(),
339 }]
340 }),
341 combo_list: Vec::new(),
342 chain_list: Vec::new(),
343 })
344 })
345 .collect()
346}
347
348fn indicator_value(
349 values: Vec<i64>,
350 interval: Option<RankFilterInterval>,
351 multiplier: f64,
352) -> screen::IndicatorValue {
353 screen::IndicatorValue {
354 value_list: values,
355 value_interval: interval.map(|interval| screen::Interval {
356 min_value: interval.min.map(|(value, _)| (value * multiplier) as i64),
357 max_value: interval.max.map(|(value, _)| (value * multiplier) as i64),
358 exclude_min: interval.min.map(|(_, includes)| !includes),
359 exclude_max: interval.max.map(|(_, includes)| !includes),
360 unit: None,
361 }),
362 }
363}
364
365fn option_sort_field(sort_type: i32) -> option_screen::OptionItem {
366 let mut item = option_screen::OptionItem::default();
367 match sort_type {
370 1 => item.volume = Some(0),
371 2 => item.turnover = Some(0),
372 3 => item.open_interest = Some(0),
373 4 | 5 => item.oi_day_chg = Some(0),
374 6 => item.open_interest_market_cap = Some(0),
375 7 | 8 => item.oi_day_market_cap_chg = Some(0),
376 9 => item.chg_ratio = Some(0),
377 10 => item.implied_volatility = Some(0),
378 _ => {}
379 }
380 item
381}
382
383fn screener_market_category(category: i32) -> i32 {
384 match category {
387 1 => 0,
388 2 => 1,
389 4 => 3,
390 5 => 4,
391 _ => 0,
392 }
393}
394
395async fn execute<Req: prost::Message>(
396 backend: &BackendConn,
397 operation: QotReadOperation,
398 request: Req,
399 quote_mkt_type: u8,
400) -> Result<futu_command_runtime::CommandResponse> {
401 let mut reserved = [0_u8; 10];
404 reserved[0] = quote_mkt_type;
405 execute_qot_read_with_reserved(
406 backend,
407 operation,
408 Bytes::from(request.encode_to_vec()),
409 reserved,
410 )
411 .await
412}
413
414fn require_code(code: Option<i32>, message: Option<String>, label: &str) -> Result<()> {
415 match code {
416 Some(0) => Ok(()),
417 Some(code) => Err(FutuError::ServerError {
418 ret_type: code,
419 msg: message.unwrap_or_else(|| format!("{label} backend rejected request")),
420 }),
421 None => Err(FutuError::Codec(format!(
422 "{label} backend response missing code"
423 ))),
424 }
425}