1use std::sync::Arc;
4
5use async_trait::async_trait;
6
7use futu_cache::login_cache::LoginCache;
8use futu_cache::qot_cache::{self, QotCache};
9use futu_cache::static_data::StaticDataCache;
10use futu_core::proto_id;
11use futu_server::conn::IncomingRequest;
12use futu_server::router::{RequestHandler, RequestRouter};
13use futu_server::subscription::SubscriptionManager;
14
15use crate::bridge::GatewayBridge;
16
17#[allow(dead_code)]
18fn hex_preview(data: &[u8], max: usize) -> String {
19 let n = data.len().min(max);
20 data[..n]
21 .iter()
22 .map(|b| format!("{b:02x}"))
23 .collect::<Vec<_>>()
24 .join(" ")
25}
26
27pub fn register_handlers(router: &Arc<RequestRouter>, bridge: &GatewayBridge) {
29 let cache = Arc::clone(&bridge.qot_cache);
30 let subs = Arc::clone(&bridge.subscriptions);
31 let static_cache = Arc::clone(&bridge.static_cache);
32
33 router.register(
35 proto_id::GET_GLOBAL_STATE,
36 Arc::new(GetGlobalStateHandler {
37 login_cache: Arc::clone(&bridge.login_cache),
38 backend: bridge.backend.clone(),
39 }),
40 );
41
42 router.register(
44 proto_id::QOT_SUB,
45 Arc::new(SubHandler {
46 subscriptions: subs.clone(),
47 backend: bridge.backend.clone(),
48 static_cache: static_cache.clone(),
49 }),
50 );
51 router.register(
52 proto_id::QOT_REG_QOT_PUSH,
53 Arc::new(RegQotPushHandler {
54 subscriptions: subs.clone(),
55 }),
56 );
57 router.register(
58 proto_id::QOT_GET_SUB_INFO,
59 Arc::new(GetSubInfoHandler {
60 subscriptions: subs.clone(),
61 }),
62 );
63
64 router.register(
66 proto_id::QOT_GET_BASIC_QOT,
67 Arc::new(GetBasicQotHandler {
68 cache: cache.clone(),
69 static_cache: static_cache.clone(),
70 }),
71 );
72 router.register(
73 proto_id::QOT_GET_KL,
74 Arc::new(GetKLHandler {
75 cache: cache.clone(),
76 }),
77 );
78 router.register(
79 proto_id::QOT_GET_ORDER_BOOK,
80 Arc::new(GetOrderBookHandler {
81 cache: cache.clone(),
82 }),
83 );
84 router.register(
85 proto_id::QOT_GET_BROKER,
86 Arc::new(GetBrokerHandler {
87 cache: cache.clone(),
88 static_cache: static_cache.clone(),
89 }),
90 );
91 router.register(
92 proto_id::QOT_GET_TICKER,
93 Arc::new(GetTickerHandler {
94 cache: cache.clone(),
95 }),
96 );
97 router.register(
98 proto_id::QOT_GET_RT,
99 Arc::new(GetRTHandler {
100 cache: cache.clone(),
101 }),
102 );
103 router.register(
104 proto_id::QOT_GET_BROKER,
105 Arc::new(GetBrokerHandler {
106 cache: cache.clone(),
107 static_cache: static_cache.clone(),
108 }),
109 );
110 router.register(
114 proto_id::QOT_GET_STATIC_INFO,
115 Arc::new(GetStaticInfoHandler {
116 cache: static_cache.clone(),
117 }),
118 );
119 router.register(
120 proto_id::QOT_GET_SECURITY_SNAPSHOT,
121 Arc::new(GetSecuritySnapshotHandler {
122 backend: bridge.backend.clone(),
123 static_cache: static_cache.clone(),
124 }),
125 );
126
127 router.register(
129 proto_id::QOT_GET_HISTORY_KL,
130 Arc::new(GetHistoryKLHandler {
131 backend: bridge.backend.clone(),
132 static_cache: static_cache.clone(),
133 }),
134 );
135 router.register(
136 proto_id::QOT_REQUEST_HISTORY_KL,
137 Arc::new(RequestHistoryKLHandler {
138 backend: bridge.backend.clone(),
139 static_cache: static_cache.clone(),
140 kl_quota_counter: bridge.kl_quota_counter.clone(),
141 }),
142 );
143 router.register(
144 proto_id::QOT_GET_HISTORY_KL_POINTS,
145 Arc::new(GetHistoryKLPointsHandler),
146 );
147 router.register(
148 proto_id::QOT_GET_TRADE_DATE,
149 Arc::new(GetTradeDateHandler {
150 backend: bridge.backend.clone(),
151 }),
152 );
153 router.register(
154 proto_id::QOT_GET_SUSPEND,
155 Arc::new(GetSuspendHandler {
156 suspend_cache: bridge.suspend_cache.clone(),
157 static_cache: static_cache.clone(),
158 }),
159 );
160 router.register(
161 proto_id::QOT_GET_REHAB,
162 Arc::new(GetRehabHandler {
163 backend: bridge.backend.clone(),
164 static_cache: static_cache.clone(),
165 }),
166 );
167 router.register(
168 proto_id::QOT_GET_PLATE_SET,
169 Arc::new(GetPlateSetHandler {
170 backend: bridge.backend.clone(),
171 static_cache: static_cache.clone(),
172 }),
173 );
174 router.register(
175 proto_id::QOT_GET_PLATE_SECURITY,
176 Arc::new(GetPlateSecurityHandler {
177 backend: bridge.backend.clone(),
178 static_cache: static_cache.clone(),
179 }),
180 );
181 router.register(
182 proto_id::QOT_GET_OWNER_PLATE,
183 Arc::new(GetOwnerPlateHandler {
184 backend: bridge.backend.clone(),
185 static_cache: static_cache.clone(),
186 }),
187 );
188 router.register(
189 proto_id::QOT_GET_REFERENCE,
190 Arc::new(GetReferenceHandler {
191 backend: bridge.backend.clone(),
192 static_cache: static_cache.clone(),
193 }),
194 );
195 router.register(
196 proto_id::QOT_GET_OPTION_CHAIN,
197 Arc::new(GetOptionChainHandler {
198 backend: bridge.backend.clone(),
199 static_cache: static_cache.clone(),
200 }),
201 );
202 router.register(
203 proto_id::QOT_GET_HOLDING_CHANGE_LIST,
204 Arc::new(GetHoldingChangeListHandler),
205 );
206
207 router.register(
209 proto_id::QOT_GET_WARRANT,
210 Arc::new(GetWarrantHandler {
211 backend: bridge.backend.clone(),
212 static_cache: static_cache.clone(),
213 }),
214 );
215 router.register(
216 proto_id::QOT_GET_CAPITAL_FLOW,
217 Arc::new(GetCapitalFlowHandler {
218 backend: bridge.backend.clone(),
219 static_cache: static_cache.clone(),
220 }),
221 );
222 router.register(
223 proto_id::QOT_GET_CAPITAL_DISTRIBUTION,
224 Arc::new(GetCapitalDistributionHandler {
225 backend: bridge.backend.clone(),
226 static_cache: static_cache.clone(),
227 }),
228 );
229 router.register(
230 proto_id::QOT_GET_USER_SECURITY,
231 Arc::new(GetUserSecurityHandler {
232 backend: bridge.backend.clone(),
233 static_cache: static_cache.clone(),
234 app_lang: bridge.app_lang,
235 }),
236 );
237 router.register(
238 proto_id::QOT_MODIFY_USER_SECURITY,
239 Arc::new(ModifyUserSecurityHandler {
240 backend: bridge.backend.clone(),
241 static_cache: static_cache.clone(),
242 }),
243 );
244 router.register(
245 proto_id::QOT_STOCK_FILTER,
246 Arc::new(StockFilterHandler {
247 backend: bridge.backend.clone(),
248 static_cache: static_cache.clone(),
249 }),
250 );
251 router.register(
252 proto_id::QOT_GET_CODE_CHANGE,
253 Arc::new(GetCodeChangeHandler {
254 code_change_cache: bridge.code_change_cache.clone(),
255 }),
256 );
257 router.register(
258 proto_id::QOT_GET_IPO_LIST,
259 Arc::new(GetIpoListHandler {
260 backend: bridge.backend.clone(),
261 static_cache: static_cache.clone(),
262 }),
263 );
264 router.register(
265 proto_id::QOT_GET_FUTURE_INFO,
266 Arc::new(GetFutureInfoHandler {
267 backend: bridge.backend.clone(),
268 static_cache: static_cache.clone(),
269 }),
270 );
271 router.register(
272 proto_id::QOT_REQUEST_TRADE_DATE,
273 Arc::new(RequestTradeDateHandler {
274 backend: bridge.backend.clone(),
275 }),
276 );
277 router.register(
278 proto_id::QOT_SET_PRICE_REMINDER,
279 Arc::new(SetPriceReminderHandler {
280 backend: bridge.backend.clone(),
281 static_cache: static_cache.clone(),
282 }),
283 );
284 router.register(
285 proto_id::QOT_GET_PRICE_REMINDER,
286 Arc::new(GetPriceReminderHandler {
287 backend: bridge.backend.clone(),
288 static_cache: static_cache.clone(),
289 }),
290 );
291 router.register(
292 proto_id::QOT_GET_USER_SECURITY_GROUP,
293 Arc::new(GetUserSecurityGroupHandler {
294 backend: bridge.backend.clone(),
295 app_lang: bridge.app_lang,
296 }),
297 );
298 router.register(
299 proto_id::QOT_GET_MARKET_STATE,
300 Arc::new(GetMarketStateHandler {
301 backend: bridge.backend.clone(),
302 static_cache: static_cache.clone(),
303 }),
304 );
305 router.register(
306 proto_id::QOT_GET_OPTION_EXPIRATION_DATE,
307 Arc::new(GetOptionExpirationDateHandler {
308 backend: bridge.backend.clone(),
309 static_cache: static_cache.clone(),
310 }),
311 );
312 router.register(
313 proto_id::QOT_REQUEST_HISTORY_KL_QUOTA,
314 Arc::new(RequestHistoryKLQuotaHandler),
315 );
316 router.register(
317 proto_id::QOT_REQUEST_REHAB,
318 Arc::new(RequestRehabHandler {
319 backend: bridge.backend.clone(),
320 static_cache: static_cache.clone(),
321 }),
322 );
323
324 router.register(
326 proto_id::GET_USED_QUOTA,
327 Arc::new(GetUsedQuotaHandler {
328 subscriptions: subs.clone(),
329 kl_quota_counter: bridge.kl_quota_counter.clone(),
330 }),
331 );
332
333 tracing::debug!("quote handlers registered (all implemented)");
334}
335
336struct SubHandler {
338 subscriptions: Arc<SubscriptionManager>,
339 backend: crate::bridge::SharedBackend,
340 static_cache: Arc<StaticDataCache>,
341}
342
343#[async_trait]
344impl RequestHandler for SubHandler {
345 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
346 let req: futu_proto::qot_sub::Request =
347 prost::Message::decode(request.body.as_ref()).ok()?;
348 let c2s = &req.c2s;
349
350 let mut backend_subs: Vec<(u64, i32, Vec<i32>)> = Vec::new();
352
353 for sec in &c2s.security_list {
354 let sec_key = format!("{}_{}", sec.market, sec.code);
355 for &sub_type in &c2s.sub_type_list {
356 if c2s.is_sub_or_un_sub {
357 self.subscriptions
358 .subscribe_qot(conn_id, &sec_key, sub_type);
359 } else {
360 self.subscriptions
361 .unsubscribe_qot(conn_id, &sec_key, sub_type);
362 }
363 }
364
365 if c2s.is_sub_or_un_sub {
367 if let Some(info) = self.static_cache.get_security_info(&sec_key) {
368 if info.stock_id > 0 {
369 backend_subs.push((info.stock_id, sec.market, c2s.sub_type_list.clone()));
370 }
371 }
372 }
373 }
374
375 if c2s.is_reg_or_un_reg_push.unwrap_or(false) {
376 self.subscriptions.subscribe_notify(conn_id);
377 }
378
379 if let Some(backend) = super::load_backend(&self.backend) {
381 if !backend_subs.is_empty() {
382 if let Err(e) =
383 futu_backend::quote_sub::subscribe_to_backend(&backend, &backend_subs).await
384 {
385 tracing::warn!(error = %e, "backend subscribe failed");
386 }
387 }
388 }
389
390 let resp = futu_proto::qot_sub::Response {
391 ret_type: 0,
392 ret_msg: None,
393 err_code: None,
394 s2c: Some(futu_proto::qot_sub::S2c {}),
395 };
396 Some(prost::Message::encode_to_vec(&resp))
397 }
398}
399
400struct RegQotPushHandler {
402 subscriptions: Arc<SubscriptionManager>,
403}
404
405#[async_trait]
406impl RequestHandler for RegQotPushHandler {
407 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
408 let req: futu_proto::qot_reg_qot_push::Request =
409 prost::Message::decode(request.body.as_ref()).ok()?;
410 let c2s = &req.c2s;
411 for sec in &c2s.security_list {
412 let sec_key = format!("{}_{}", sec.market, sec.code);
413 for &sub_type in &c2s.sub_type_list {
414 if c2s.is_reg_or_un_reg {
415 self.subscriptions
416 .subscribe_qot(conn_id, &sec_key, sub_type);
417 } else {
418 self.subscriptions
419 .unsubscribe_qot(conn_id, &sec_key, sub_type);
420 }
421 }
422 }
423 let resp = futu_proto::qot_reg_qot_push::Response {
424 ret_type: 0,
425 ret_msg: None,
426 err_code: None,
427 s2c: Some(futu_proto::qot_reg_qot_push::S2c {}),
428 };
429 Some(prost::Message::encode_to_vec(&resp))
430 }
431}
432
433struct GetSubInfoHandler {
435 subscriptions: Arc<SubscriptionManager>,
436}
437
438#[async_trait]
439impl RequestHandler for GetSubInfoHandler {
440 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
441 let req: futu_proto::qot_get_sub_info::Request =
442 prost::Message::decode(request.body.as_ref()).ok()?;
443 let is_req_all = req.c2s.is_req_all_conn.unwrap_or(false);
444
445 let used = self.subscriptions.get_total_used_quota();
446 let remain = futu_server::subscription::TOTAL_QUOTA.saturating_sub(used);
447
448 let conn_ids: Vec<u64> = if is_req_all {
450 self.subscriptions
451 .get_all_qot_conn_ids()
452 .into_iter()
453 .collect()
454 } else {
455 vec![conn_id]
456 };
457
458 let conn_sub_info_list: Vec<futu_proto::qot_common::ConnSubInfo> = conn_ids
459 .iter()
460 .map(|&cid| {
461 let subs = self.subscriptions.get_conn_qot_subs(cid);
462 let sub_info_list: Vec<futu_proto::qot_common::SubInfo> = subs
463 .iter()
464 .map(|(&sub_type, sec_keys)| {
465 let security_list: Vec<futu_proto::qot_common::Security> = sec_keys
466 .iter()
467 .filter_map(|sk| {
468 let parts: Vec<&str> = sk.splitn(2, '_').collect();
469 if parts.len() == 2 {
470 Some(futu_proto::qot_common::Security {
471 market: parts[0].parse().unwrap_or(0),
472 code: parts[1].to_string(),
473 })
474 } else {
475 None
476 }
477 })
478 .collect();
479 futu_proto::qot_common::SubInfo {
480 sub_type,
481 security_list,
482 }
483 })
484 .collect();
485 let conn_used = self.subscriptions.get_conn_used_quota(cid);
486 futu_proto::qot_common::ConnSubInfo {
487 sub_info_list,
488 used_quota: conn_used as i32,
489 is_own_conn_data: cid == conn_id,
490 }
491 })
492 .collect();
493
494 let resp = futu_proto::qot_get_sub_info::Response {
495 ret_type: 0,
496 ret_msg: None,
497 err_code: None,
498 s2c: Some(futu_proto::qot_get_sub_info::S2c {
499 conn_sub_info_list,
500 total_used_quota: used as i32,
501 remain_quota: remain as i32,
502 }),
503 };
504 Some(prost::Message::encode_to_vec(&resp))
505 }
506}
507
508struct GetBasicQotHandler {
510 cache: Arc<QotCache>,
511 static_cache: Arc<StaticDataCache>,
512}
513
514#[async_trait]
515impl RequestHandler for GetBasicQotHandler {
516 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
517 let req: futu_proto::qot_get_basic_qot::Request =
518 prost::Message::decode(request.body.as_ref()).ok()?;
519 let mut basic_qot_list = Vec::new();
520 for sec in &req.c2s.security_list {
521 let key = qot_cache::make_key(sec.market, &sec.code);
522 if let Some(c) = self.cache.get_basic_qot(&key) {
523 let static_info = self.static_cache.get_security_info(&key);
525 let name = static_info.as_ref().map(|s| s.name.clone());
526 let list_time = static_info
527 .as_ref()
528 .map(|s| s.list_time.clone())
529 .unwrap_or_default();
530
531 basic_qot_list.push(futu_proto::qot_common::BasicQot {
532 security: sec.clone(),
533 name,
534 is_suspended: c.is_suspended,
535 list_time,
536 price_spread: 0.0,
537 update_time: c.update_time.clone(),
538 high_price: c.high_price,
539 open_price: c.open_price,
540 low_price: c.low_price,
541 cur_price: c.cur_price,
542 last_close_price: c.last_close_price,
543 volume: c.volume,
544 turnover: c.turnover,
545 turnover_rate: c.turnover_rate,
546 amplitude: c.amplitude,
547 dark_status: None,
548 option_ex_data: None,
549 list_timestamp: None,
550 update_timestamp: Some(c.update_timestamp),
551 pre_market: None,
552 after_market: None,
553 sec_status: None,
554 future_ex_data: None,
555 warrant_ex_data: None,
556 overnight: None,
557 });
558 }
559 }
560 let resp = futu_proto::qot_get_basic_qot::Response {
561 ret_type: 0,
562 ret_msg: None,
563 err_code: None,
564 s2c: Some(futu_proto::qot_get_basic_qot::S2c { basic_qot_list }),
565 };
566 Some(prost::Message::encode_to_vec(&resp))
567 }
568}
569
570struct GetKLHandler {
572 cache: Arc<QotCache>,
573}
574
575#[async_trait]
576impl RequestHandler for GetKLHandler {
577 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
578 let req: futu_proto::qot_get_kl::Request =
579 prost::Message::decode(request.body.as_ref()).ok()?;
580 let c2s = &req.c2s;
581 let key = qot_cache::make_key(c2s.security.market, &c2s.security.code);
582 let kl_list = self
583 .cache
584 .get_klines(&key, c2s.kl_type)
585 .unwrap_or_default()
586 .into_iter()
587 .take(c2s.req_num as usize)
588 .map(|kl| futu_proto::qot_common::KLine {
589 time: kl.time,
590 is_blank: false,
591 high_price: Some(kl.high_price),
592 open_price: Some(kl.open_price),
593 low_price: Some(kl.low_price),
594 close_price: Some(kl.close_price),
595 last_close_price: None,
596 volume: Some(kl.volume),
597 turnover: Some(kl.turnover),
598 turnover_rate: None,
599 pe: None,
600 change_rate: None,
601 timestamp: None,
602 })
603 .collect();
604 let resp = futu_proto::qot_get_kl::Response {
605 ret_type: 0,
606 ret_msg: None,
607 err_code: None,
608 s2c: Some(futu_proto::qot_get_kl::S2c {
609 security: c2s.security.clone(),
610 name: None,
611 kl_list,
612 }),
613 };
614 Some(prost::Message::encode_to_vec(&resp))
615 }
616}
617
618struct GetOrderBookHandler {
620 cache: Arc<QotCache>,
621}
622
623#[async_trait]
624impl RequestHandler for GetOrderBookHandler {
625 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
626 let req: futu_proto::qot_get_order_book::Request =
627 prost::Message::decode(request.body.as_ref()).ok()?;
628 let c2s = &req.c2s;
629 let key = qot_cache::make_key(c2s.security.market, &c2s.security.code);
630 let ob = self.cache.order_books.get(&key);
631 let n = c2s.num as usize;
632 let (ask_list, bid_list) = match ob.as_deref() {
633 Some(ob) => (
634 ob.ask_list
635 .iter()
636 .take(n)
637 .map(|a| futu_proto::qot_common::OrderBook {
638 price: a.price,
639 volume: a.volume,
640 oreder_count: a.order_count,
641 detail_list: vec![],
642 })
643 .collect(),
644 ob.bid_list
645 .iter()
646 .take(n)
647 .map(|b| futu_proto::qot_common::OrderBook {
648 price: b.price,
649 volume: b.volume,
650 oreder_count: b.order_count,
651 detail_list: vec![],
652 })
653 .collect(),
654 ),
655 None => (vec![], vec![]),
656 };
657 let resp = futu_proto::qot_get_order_book::Response {
658 ret_type: 0,
659 ret_msg: None,
660 err_code: None,
661 s2c: Some(futu_proto::qot_get_order_book::S2c {
662 security: c2s.security.clone(),
663 name: None,
664 order_book_ask_list: ask_list,
665 order_book_bid_list: bid_list,
666 svr_recv_time_bid: None,
667 svr_recv_time_bid_timestamp: None,
668 svr_recv_time_ask: None,
669 svr_recv_time_ask_timestamp: None,
670 }),
671 };
672 Some(prost::Message::encode_to_vec(&resp))
673 }
674}
675
676struct GetTickerHandler {
678 cache: Arc<QotCache>,
679}
680
681#[async_trait]
682impl RequestHandler for GetTickerHandler {
683 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
684 let req: futu_proto::qot_get_ticker::Request =
685 prost::Message::decode(request.body.as_ref()).ok()?;
686 let c2s = &req.c2s;
687 let key = qot_cache::make_key(c2s.security.market, &c2s.security.code);
688 let n = c2s.max_ret_num as usize;
689 let tickers = self.cache.tickers.get(&key);
690 let ticker_list: Vec<futu_proto::qot_common::Ticker> = tickers
691 .as_deref()
692 .map(|t| {
693 t.iter()
694 .rev()
695 .take(n)
696 .rev()
697 .map(|t| futu_proto::qot_common::Ticker {
698 time: t.time.clone(),
699 sequence: 0,
700 dir: t.dir,
701 price: t.price,
702 volume: t.volume,
703 turnover: 0.0,
704 recv_time: None,
705 r#type: None,
706 type_sign: None,
707 push_data_type: None,
708 timestamp: None,
709 })
710 .collect()
711 })
712 .unwrap_or_default();
713 let resp = futu_proto::qot_get_ticker::Response {
714 ret_type: 0,
715 ret_msg: None,
716 err_code: None,
717 s2c: Some(futu_proto::qot_get_ticker::S2c {
718 security: c2s.security.clone(),
719 name: None,
720 ticker_list,
721 }),
722 };
723 Some(prost::Message::encode_to_vec(&resp))
724 }
725}
726
727struct GetRTHandler {
729 cache: Arc<QotCache>,
730}
731
732#[async_trait]
733impl RequestHandler for GetRTHandler {
734 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
735 let req: futu_proto::qot_get_rt::Request =
736 prost::Message::decode(request.body.as_ref()).ok()?;
737 let key = qot_cache::make_key(req.c2s.security.market, &req.c2s.security.code);
738 let rt_list = self
739 .cache
740 .rt_data
741 .get(&key)
742 .map(|v| {
743 v.iter()
744 .map(|rt| futu_proto::qot_common::TimeShare {
745 time: rt.time.clone(),
746 minute: rt.minute,
747 is_blank: false,
748 price: Some(rt.price),
749 last_close_price: if rt.last_close_price > 0.0 {
750 Some(rt.last_close_price)
751 } else {
752 None
753 },
754 avg_price: if rt.avg_price > 0.0 {
755 Some(rt.avg_price)
756 } else {
757 None
758 },
759 volume: Some(rt.volume),
760 turnover: Some(rt.turnover),
761 timestamp: Some(rt.timestamp),
762 })
763 .collect()
764 })
765 .unwrap_or_default();
766 let resp = futu_proto::qot_get_rt::Response {
767 ret_type: 0,
768 ret_msg: None,
769 err_code: None,
770 s2c: Some(futu_proto::qot_get_rt::S2c {
771 security: req.c2s.security.clone(),
772 name: None,
773 rt_list,
774 }),
775 };
776 Some(prost::Message::encode_to_vec(&resp))
777 }
778}
779
780struct GetStaticInfoHandler {
782 cache: Arc<StaticDataCache>,
783}
784
785#[async_trait]
786impl RequestHandler for GetStaticInfoHandler {
787 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
788 let req: futu_proto::qot_get_static_info::Request =
789 prost::Message::decode(request.body.as_ref()).ok()?;
790 let mut static_info_list = Vec::new();
791
792 if !req.c2s.security_list.is_empty() {
793 for sec in &req.c2s.security_list {
795 let key = qot_cache::make_key(sec.market, &sec.code);
796 if let Some(info) = self.cache.get_security_info(&key) {
797 static_info_list.push(make_static_info(sec.clone(), &info));
798 }
799 }
800 } else {
801 let filter_market = req.c2s.market;
803 let filter_sec_type = req.c2s.sec_type;
804 for entry in self.cache.securities.iter() {
805 let info = entry.value();
806 if info.no_search {
808 continue;
809 }
810 if let Some(m) = filter_market {
811 if info.market != m {
812 continue;
813 }
814 }
815 if let Some(st) = filter_sec_type {
816 if info.sec_type != st {
817 continue;
818 }
819 }
820 let sec = futu_proto::qot_common::Security {
821 market: info.market,
822 code: info.code.clone(),
823 };
824 static_info_list.push(make_static_info(sec, info));
825 }
826 }
827
828 let resp = futu_proto::qot_get_static_info::Response {
829 ret_type: 0,
830 ret_msg: None,
831 err_code: None,
832 s2c: Some(futu_proto::qot_get_static_info::S2c { static_info_list }),
833 };
834 Some(prost::Message::encode_to_vec(&resp))
835 }
836}
837
838fn make_static_info(
839 security: futu_proto::qot_common::Security,
840 info: &futu_cache::static_data::CachedSecurityInfo,
841) -> futu_proto::qot_common::SecurityStaticInfo {
842 futu_proto::qot_common::SecurityStaticInfo {
843 basic: futu_proto::qot_common::SecurityStaticBasic {
844 security,
845 id: info.stock_id as i64,
846 lot_size: info.lot_size,
847 sec_type: info.sec_type,
848 name: info.name.clone(),
849 list_time: info.list_time.clone(),
850 delisting: Some(info.delisting),
851 list_timestamp: None,
852 exch_type: if info.exch_type != 0 {
853 Some(info.exch_type)
854 } else {
855 None
856 },
857 },
858 warrant_ex_data: None,
859 option_ex_data: None,
860 future_ex_data: None,
861 }
862}
863
864struct GetSecuritySnapshotHandler {
866 backend: crate::bridge::SharedBackend,
867 static_cache: Arc<StaticDataCache>,
868}
869
870const CMD_PULL_SUB_DATA: u16 = 6824;
872
873#[derive(Default)]
875struct SnapshotData {
876 cur_price: i64,
878 last_close_price: i64,
879 timestamp: i64,
880 suspend_flag: bool,
882 sec_status: i32,
883 open_price: i64,
885 high_price: i64,
886 low_price: i64,
887 volume: i64,
888 turnover: i64,
889 turnover_ratio: i64,
890 amplitude: i64,
891 avg_price: i64,
892 bid_ask_ratio: i64,
893 volume_ratio: i64,
894 kcb_after_volume: i64,
896 kcb_after_turnover: i64,
897 kcb_has_after: bool,
898 bid_price: i64,
900 ask_price: i64,
901 bid_vol: i64,
902 ask_vol: i64,
903 history_highest_price: i64,
905 history_lowest_price: i64,
906 week52_highest_price: i64,
907 week52_lowest_price: i64,
908 close_price_5min: i64,
910 total_shares: i64,
912 total_market_cap: i64,
913 outstanding_shares: i64,
914 pe_lyr: i64,
915 pb_ratio: i64,
916 pe_ttm: i64,
917 eps_lyr: i64,
918 dividend_ttm: i64,
919 dividend_ratio_ttm: i64,
920 dividend_lfy: i64,
921 dividend_lfy_ratio: i64,
922 net_asset: i64,
924 net_asset_pershare: i64,
925 net_profit: i64,
926 ey_ratio: i32,
927 stock_specific: Option<StockSpecificData>,
929 pre_market: Option<PreAfterMarketItem>,
931 after_market: Option<PreAfterMarketItem>,
932 overnight: Option<PreAfterMarketItem>,
933}
934
935struct PreAfterMarketItem {
936 price: i64,
937 high_price: i64,
938 low_price: i64,
939 volume: i64,
940 turnover: i64,
941 change_val: i64,
942 change_ratio: i64,
943 amplitude: i64,
944}
945
946enum StockSpecificData {
947 Warrant(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::HkWarrantCbbc),
948 Option(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::Option),
949 Index(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::Index),
950 Plate(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::Plate),
951 Future(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::Future),
952 Trust(futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data::Trust),
953}
954
955fn dp(raw: i64, precision: u32) -> f64 {
957 raw as f64 / 10f64.powi(precision as i32)
958}
959
960fn parse_security_quote(
962 sq: &futu_backend::proto_internal::ft_cmd_stock_quote_sub_data::SecurityQuote,
963 sec_type: i32,
964 is_us: bool,
965) -> SnapshotData {
966 use futu_backend::proto_internal::ft_cmd_stock_quote_coverage_data as cov;
967 use prost::Message;
968 let mut s = SnapshotData::default();
969 for bq in &sq.bit_qta_list {
970 let bit = bq.bit.unwrap_or(999);
971 let data = &bq.data.as_deref().unwrap_or(&[]);
972 match bit {
973 0 => {
974 if let Ok(p) = cov::Price::decode(*data) {
976 s.cur_price = p.price_nominal.unwrap_or(0);
977 s.last_close_price = p.price_last_close.unwrap_or(0);
978 let exch_time_ms = p.exchange_data_time_ms.unwrap_or(0);
979 s.timestamp = exch_time_ms / 1000;
980 }
981 }
982 1 => {
983 if let Ok(st) = cov::StockState::decode(*data) {
985 let state_type = st.state_type.unwrap_or(0);
986 s.suspend_flag = state_type == 8; s.sec_status = stock_state_to_api_status(state_type);
988 }
989 }
990 2 => {
991 if let Ok(sp) = cov::StockTypeSpecific::decode(*data) {
993 match sec_type {
994 5 => {
995 if let Some(w) = sp.hk_warrant_cbbc {
996 s.stock_specific = Some(StockSpecificData::Warrant(w));
997 }
998 }
999 8 => {
1000 if let Some(o) = sp.option {
1001 s.stock_specific = Some(StockSpecificData::Option(o));
1002 }
1003 }
1004 6 => {
1005 if let Some(i) = sp.index {
1006 s.stock_specific = Some(StockSpecificData::Index(i));
1007 }
1008 }
1009 7 => {
1010 if let Some(p) = sp.plate {
1011 s.stock_specific = Some(StockSpecificData::Plate(p));
1012 }
1013 }
1014 10 => {
1015 if let Some(f) = sp.future {
1016 s.stock_specific = Some(StockSpecificData::Future(f));
1017 }
1018 }
1019 4 => {
1020 if let Some(t) = sp.trust {
1021 s.stock_specific = Some(StockSpecificData::Trust(t));
1022 }
1023 }
1024 _ => {}
1025 }
1026 }
1027 }
1028 4 => {
1029 if let Ok(ob) = cov::OrderBookSimple::decode(*data) {
1031 s.bid_price = ob.price_bid.unwrap_or(0);
1032 s.ask_price = ob.price_ask.unwrap_or(0);
1033 s.bid_vol = ob.volume_bid.unwrap_or(0);
1034 s.ask_vol = ob.volume_ask.unwrap_or(0);
1035 }
1036 }
1037 5 => {
1038 if let Ok(ds) = cov::DealStatistics::decode(*data) {
1040 s.open_price = ds.price_open.unwrap_or(0);
1041 s.high_price = ds.price_highest.unwrap_or(0);
1042 s.low_price = ds.price_lowest.unwrap_or(0);
1043 s.volume = ds.volume.unwrap_or(0);
1044 s.turnover = ds.turnover.unwrap_or(0);
1045 s.turnover_ratio = ds.ratio_turnover.unwrap_or(0);
1046 s.amplitude = ds.amplitude_price.unwrap_or(0);
1047 s.avg_price = ds.price_average.unwrap_or(0);
1048 s.bid_ask_ratio = ds.ratio_bid_ask.unwrap_or(0);
1049 s.volume_ratio = ds.ratio_volume.unwrap_or(0);
1050 if let Some(kcb) = &ds.kcb_stock_static {
1052 s.kcb_has_after = true;
1053 s.kcb_after_volume = kcb.volume.unwrap_or(0);
1054 s.kcb_after_turnover = kcb.turnover.unwrap_or(0);
1055 }
1056 }
1057 }
1058 6 => {
1059 if let Ok(hh) = cov::HistoryHighLowPrice::decode(*data) {
1061 s.history_highest_price = hh.price_highest_history.unwrap_or(0);
1062 s.history_lowest_price = hh.price_lowest_history.unwrap_or(0);
1063 s.week52_highest_price = hh.price_highest_52week.unwrap_or(0);
1064 s.week52_lowest_price = hh.price_lowest_52week.unwrap_or(0);
1065 }
1066 }
1067 7 => {
1068 if let Ok(hc) = cov::HistoryClosePrice::decode(*data) {
1070 s.close_price_5min = hc.price_close_5min.unwrap_or(0);
1071 }
1072 }
1073 8 => {
1074 if sec_type == 3 {
1076 if let Ok(fi) = cov::FinacialIndicator::decode(*data) {
1078 s.total_shares = fi.total_shares.unwrap_or(0);
1079 s.total_market_cap = fi.total_market_cap.unwrap_or(0);
1080 s.outstanding_shares = fi.outstanding_shares.unwrap_or(0);
1081 s.pe_lyr = fi.pe_lyr.unwrap_or(0);
1082 s.pb_ratio = fi.pb_ratio.unwrap_or(0);
1083 s.pe_ttm = fi.pe_ttm.unwrap_or(0);
1084 s.eps_lyr = fi.eps_lyr.unwrap_or(0);
1085 s.dividend_ttm = fi.dividend.unwrap_or(0);
1086 s.dividend_ratio_ttm = fi.dividend_ratio.unwrap_or(0);
1087 s.dividend_lfy = fi.dividend_lfy.unwrap_or(0);
1088 s.dividend_lfy_ratio = fi.dividend_lfy_ratio.unwrap_or(0);
1089 }
1090 }
1091 }
1092 18 => {
1093 if sec_type == 3 {
1095 if let Ok(sfi) = cov::StaticFinancialIndicator::decode(*data) {
1096 s.net_asset = sfi.net_asset.unwrap_or(0);
1097 s.net_asset_pershare = sfi.net_asset_pershare.unwrap_or(0);
1098 s.net_profit = sfi.net_profit_lyr.unwrap_or(0);
1099 s.ey_ratio = sfi.ey_ratio.unwrap_or(0);
1100 }
1101 }
1102 }
1103 13 => {
1104 if is_us {
1106 if let Ok(pa) = cov::UsPreMarketAfterHoursDetail::decode(*data) {
1107 if let Some(pm) = &pa.pre_market {
1108 s.pre_market = Some(PreAfterMarketItem {
1109 price: pm.price.unwrap_or(0),
1110 high_price: pm.price_highest.unwrap_or(0),
1111 low_price: pm.price_lowest.unwrap_or(0),
1112 volume: pm.volume.unwrap_or(0),
1113 turnover: pm.turnover.unwrap_or(0),
1114 change_val: pm.price_change.unwrap_or(0),
1115 change_ratio: pm.ratio_price_change.unwrap_or(0),
1116 amplitude: pm.amplitude_price.unwrap_or(0),
1117 });
1118 }
1119 if let Some(ah) = &pa.after_hours {
1120 s.after_market = Some(PreAfterMarketItem {
1121 price: ah.price.unwrap_or(0),
1122 high_price: ah.price_highest.unwrap_or(0),
1123 low_price: ah.price_lowest.unwrap_or(0),
1124 volume: ah.volume.unwrap_or(0),
1125 turnover: ah.turnover.unwrap_or(0),
1126 change_val: ah.price_change.unwrap_or(0),
1127 change_ratio: ah.ratio_price_change.unwrap_or(0),
1128 amplitude: ah.amplitude_price.unwrap_or(0),
1129 });
1130 }
1131 if let Some(on) = &pa.overnight {
1132 s.overnight = Some(PreAfterMarketItem {
1133 price: on.price.unwrap_or(0),
1134 high_price: on.price_highest.unwrap_or(0),
1135 low_price: on.price_lowest.unwrap_or(0),
1136 volume: on.volume.unwrap_or(0),
1137 turnover: on.turnover.unwrap_or(0),
1138 change_val: on.price_change.unwrap_or(0),
1139 change_ratio: on.ratio_price_change.unwrap_or(0),
1140 amplitude: on.amplitude_price.unwrap_or(0),
1141 });
1142 }
1143 }
1144 }
1145 }
1146 _ => {}
1147 }
1148 }
1149 s
1150}
1151
1152fn stock_state_to_api_status(state_type: i32) -> i32 {
1154 match state_type {
1155 0 => 0, 1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 8, 8 => 9, 9 => 10, 10 => 11, 11 => 12, 12 => 13, 13 => 14, 14 => 15, 15 => 16, 16 => 17, 17 => 18, 18 => 19, 19 => 20, 20 => 21, _ => 0,
1177 }
1178}
1179
1180fn build_pre_after_market(item: &PreAfterMarketItem) -> futu_proto::qot_common::PreAfterMarketData {
1182 futu_proto::qot_common::PreAfterMarketData {
1183 price: Some(dp(item.price, 9)),
1184 high_price: Some(dp(item.high_price, 9)),
1185 low_price: Some(dp(item.low_price, 9)),
1186 volume: Some(item.volume),
1187 turnover: Some(dp(item.turnover, 3)),
1188 change_val: Some(dp(item.change_val, 9)),
1189 change_rate: Some(dp(item.change_ratio, 3)),
1190 amplitude: Some(dp(item.amplitude, 4)),
1191 }
1192}
1193
1194#[async_trait]
1195impl RequestHandler for GetSecuritySnapshotHandler {
1196 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
1197 let req: futu_proto::qot_get_security_snapshot::Request =
1198 prost::Message::decode(request.body.as_ref()).ok()?;
1199
1200 let backend = match super::load_backend(&self.backend) {
1201 Some(b) => b,
1202 None => {
1203 return Some(super::make_error_response(-1, "no backend connection"));
1204 }
1205 };
1206
1207 struct SecEntry {
1209 sec: futu_proto::qot_common::Security,
1210 stock_id: u64,
1211 sec_type: i32,
1212 qot_market: i32,
1213 name: String,
1214 lot_size: i32,
1215 list_time: String,
1216 backend_mkt: u8, }
1218
1219 let mut entries = Vec::new();
1220 for sec in &req.c2s.security_list {
1221 let key = qot_cache::make_key(sec.market, &sec.code);
1222 let info = match self.static_cache.get_security_info(&key) {
1223 Some(i) if i.stock_id > 0 => i,
1224 _ => continue,
1225 };
1226 let backend_mkt = match futu_backend::stock_list::qot_market_to_backend(sec.market) {
1227 Some(m) => m as u8,
1228 None => continue,
1229 };
1230 entries.push(SecEntry {
1231 sec: sec.clone(),
1232 stock_id: info.stock_id,
1233 sec_type: info.sec_type,
1234 qot_market: sec.market,
1235 name: info.name.clone(),
1236 lot_size: info.lot_size,
1237 list_time: info.list_time.clone(),
1238 backend_mkt,
1239 });
1240 }
1241
1242 let mut market_groups: std::collections::HashMap<u8, Vec<usize>> =
1244 std::collections::HashMap::new();
1245 for (idx, entry) in entries.iter().enumerate() {
1246 market_groups
1247 .entry(entry.backend_mkt)
1248 .or_default()
1249 .push(idx);
1250 }
1251
1252 let mut stock_snapshots: std::collections::HashMap<u64, SnapshotData> =
1254 std::collections::HashMap::new();
1255
1256 for (mkt, indices) in &market_groups {
1257 let mut security_list = Vec::new();
1259 for &idx in indices {
1260 let entry = &entries[idx];
1261 let mut bit_info_list = Vec::new();
1262 for &bit in &[0u32, 1, 2, 5, 4, 8, 18, 7, 6, 13] {
1264 let prob = if bit == 6 { Some(2i64) } else { None }; bit_info_list.push(
1266 futu_backend::proto_internal::ft_cmd_stock_quote_sub_data::BitInfo {
1267 bit: Some(bit),
1268 prob,
1269 prob2: None,
1270 },
1271 );
1272 }
1273 security_list.push(
1274 futu_backend::proto_internal::ft_cmd_stock_quote_sub_data::SecuritySubscribe {
1275 security_id: Some(entry.stock_id),
1276 bit_info_list,
1277 },
1278 );
1279 }
1280
1281 let fetch_req = futu_backend::proto_internal::ft_cmd_stock_quote_fetch::FetchQuoteReq {
1282 security_list,
1283 reserved: Some(0),
1284 };
1285
1286 let reserved = futu_backend::stock_list::make_quote_reserved(
1287 match mkt {
1288 1 => futu_backend::stock_list::QuoteMktType::HK,
1289 2 => futu_backend::stock_list::QuoteMktType::US,
1290 3 => futu_backend::stock_list::QuoteMktType::SH,
1291 4 => futu_backend::stock_list::QuoteMktType::SZ,
1292 5 => futu_backend::stock_list::QuoteMktType::HKFuture,
1293 _ => futu_backend::stock_list::QuoteMktType::HK,
1294 },
1295 0, );
1297
1298 let body = prost::Message::encode_to_vec(&fetch_req);
1299 match backend
1300 .request_with_reserved(CMD_PULL_SUB_DATA, body, reserved)
1301 .await
1302 {
1303 Ok(resp) => {
1304 let fetch_rsp: futu_backend::proto_internal::ft_cmd_stock_quote_fetch::FetchQuoteRsp =
1305 match prost::Message::decode(resp.body.as_ref()) {
1306 Ok(r) => r,
1307 Err(e) => {
1308 tracing::warn!(conn_id, mkt, error = %e, "CMD6824 decode failed");
1309 continue;
1310 }
1311 };
1312 for sq in &fetch_rsp.security_qta_list {
1313 let stock_id = sq.security_id.unwrap_or(0);
1314 let (sec_type, is_us) = indices
1316 .iter()
1317 .find_map(|&i| {
1318 let e = &entries[i];
1319 if e.stock_id == stock_id {
1320 Some((e.sec_type, e.qot_market == 11))
1321 } else {
1322 None
1323 }
1324 })
1325 .unwrap_or((0, false));
1326 let sd = parse_security_quote(sq, sec_type, is_us);
1327 stock_snapshots.insert(stock_id, sd);
1328 }
1329 }
1330 Err(e) => {
1331 tracing::warn!(conn_id, mkt, error = %e, "CMD6824 request failed");
1332 }
1333 }
1334 }
1335
1336 let mut snapshot_list = Vec::new();
1338 for entry in &entries {
1339 let sd = stock_snapshots.get(&entry.stock_id);
1340 let basic = futu_proto::qot_get_security_snapshot::SnapshotBasicData {
1341 security: entry.sec.clone(),
1342 r#type: entry.sec_type,
1343 is_suspend: sd.map(|s| s.suspend_flag).unwrap_or(false),
1344 list_time: if entry.sec_type != 8 && entry.sec_type != 10 {
1345 entry.list_time.clone()
1346 } else {
1347 String::new()
1348 },
1349 lot_size: entry.lot_size,
1350 price_spread: 0.0,
1351 update_time: String::new(),
1352 high_price: sd.map(|s| dp(s.high_price, 9)).unwrap_or(0.0),
1353 open_price: sd.map(|s| dp(s.open_price, 9)).unwrap_or(0.0),
1354 low_price: sd.map(|s| dp(s.low_price, 9)).unwrap_or(0.0),
1355 last_close_price: sd.map(|s| dp(s.last_close_price, 9)).unwrap_or(0.0),
1356 cur_price: sd.map(|s| dp(s.cur_price, 9)).unwrap_or(0.0),
1357 volume: sd.map(|s| s.volume).unwrap_or(0),
1358 turnover: sd.map(|s| dp(s.turnover, 3)).unwrap_or(0.0),
1359 turnover_rate: sd.map(|s| dp(s.turnover_ratio, 3)).unwrap_or(0.0),
1360 name: Some(entry.name.clone()),
1361 list_timestamp: None,
1362 update_timestamp: sd.and_then(|s| {
1363 if s.timestamp > 0 {
1364 Some(s.timestamp as f64)
1365 } else {
1366 None
1367 }
1368 }),
1369 ask_price: sd.map(|s| dp(s.ask_price, 9)),
1370 bid_price: sd.map(|s| dp(s.bid_price, 9)),
1371 ask_vol: sd.map(|s| s.ask_vol),
1372 bid_vol: sd.map(|s| s.bid_vol),
1373 enable_margin: None,
1374 mortgage_ratio: None,
1375 long_margin_initial_ratio: None,
1376 enable_short_sell: None,
1377 short_sell_rate: None,
1378 short_available_volume: None,
1379 short_margin_initial_ratio: None,
1380 amplitude: sd.map(|s| dp(s.amplitude, 3)),
1381 avg_price: sd.map(|s| dp(s.avg_price, 9)),
1382 bid_ask_ratio: sd.map(|s| dp(s.bid_ask_ratio, 3)),
1383 volume_ratio: sd.map(|s| dp(s.volume_ratio, 3)),
1384 highest52_weeks_price: sd.map(|s| dp(s.week52_highest_price, 9)),
1385 lowest52_weeks_price: sd.map(|s| dp(s.week52_lowest_price, 9)),
1386 highest_history_price: sd.map(|s| dp(s.history_highest_price, 9)),
1387 lowest_history_price: sd.map(|s| dp(s.history_lowest_price, 9)),
1388 sec_status: sd.map(|s| s.sec_status),
1389 close_price5_minute: sd.map(|s| dp(s.close_price_5min, 9)),
1390 pre_market: sd.and_then(|s| s.pre_market.as_ref().map(build_pre_after_market)),
1391 after_market: {
1392 if let Some(s) = sd {
1394 if s.kcb_has_after {
1395 Some(futu_proto::qot_common::PreAfterMarketData {
1396 price: None,
1397 high_price: None,
1398 low_price: None,
1399 volume: Some(s.kcb_after_volume),
1400 turnover: Some(dp(s.kcb_after_turnover, 3)),
1401 change_val: None,
1402 change_rate: None,
1403 amplitude: None,
1404 })
1405 } else {
1406 s.after_market.as_ref().map(build_pre_after_market)
1407 }
1408 } else {
1409 None
1410 }
1411 },
1412 overnight: sd.and_then(|s| s.overnight.as_ref().map(build_pre_after_market)),
1413 };
1414
1415 let mut equity_ex = None;
1417 let mut warrant_ex = None;
1418 let mut option_ex = None;
1419 let mut index_ex = None;
1420 let mut plate_ex = None;
1421 let mut future_ex = None;
1422 let mut trust_ex = None;
1423
1424 if let Some(s) = sd {
1425 match entry.sec_type {
1426 3 => {
1427 let cur_price_f = dp(s.cur_price, 9);
1429 equity_ex = Some(
1430 futu_proto::qot_get_security_snapshot::EquitySnapshotExData {
1431 issued_shares: s.total_shares,
1432 issued_market_val: dp(s.total_market_cap, 3),
1433 net_asset: dp(s.net_asset, 3),
1434 net_profit: dp(s.net_profit, 3),
1435 earnings_pershare: dp(s.eps_lyr, 9),
1436 outstanding_shares: s.outstanding_shares,
1437 outstanding_market_val: s.outstanding_shares as f64 * cur_price_f,
1438 net_asset_pershare: dp(s.net_asset_pershare, 9),
1439 ey_rate: dp(s.ey_ratio as i64, 3),
1440 pe_ttm_rate: dp(s.pe_ttm, 3),
1441 pb_rate: dp(s.pb_ratio, 3),
1442 pe_rate: dp(s.pe_lyr, 3),
1443 dividend_ttm: Some(dp(s.dividend_ttm, 9)),
1444 dividend_ratio_ttm: Some(dp(s.dividend_ratio_ttm, 2)), dividend_lfy: Some(dp(s.dividend_lfy, 9)),
1446 dividend_lfy_ratio: Some(dp(s.dividend_lfy_ratio, 3)), },
1448 );
1449 }
1450 5 => {
1451 if let Some(StockSpecificData::Warrant(ref w)) = s.stock_specific {
1453 warrant_ex = Some(
1454 futu_proto::qot_get_security_snapshot::WarrantSnapshotExData {
1455 conversion_price: Some(dp(w.price_entitlement.unwrap_or(0), 9)),
1456 warrant_type: w.r#type.unwrap_or(0),
1457 strike_price: dp(w.price_strike.unwrap_or(0), 9),
1458 maturity_time: String::new(),
1459 end_trade_time: String::new(),
1460 owner: futu_proto::qot_common::Security {
1461 market: entry.qot_market,
1462 code: String::new(),
1463 },
1464 recovery_price: dp(w.price_call.unwrap_or(0), 9),
1465 street_volumn: w.volume_street.unwrap_or(0),
1466 issue_volumn: w.issued_shares.unwrap_or(0),
1467 street_rate: dp(w.ratio_street.unwrap_or(0), 5),
1468 delta: dp(w.delta.unwrap_or(0), 3),
1469 implied_volatility: dp(w.implied_volatility.unwrap_or(0), 3),
1470 premium: dp(w.premium.unwrap_or(0), 5),
1471 maturity_timestamp: w.expiry_date_time_s.map(|t| t as f64),
1472 end_trade_timestamp: w
1473 .last_trading_date_time_s
1474 .map(|t| t as f64),
1475 leverage: Some(dp(w.leverage.unwrap_or(0), 3)),
1476 ipop: Some(dp(w.ratio_itm_otm.unwrap_or(0), 5)),
1477 break_even_point: Some(dp(
1478 w.price_break_even_point.unwrap_or(0),
1479 9,
1480 )),
1481 conversion_rate: dp(w.ratio_entitlement.unwrap_or(0), 3),
1482 price_recovery_ratio: Some(dp(
1483 w.ratio_price_call.unwrap_or(0),
1484 5,
1485 )),
1486 score: Some(dp(w.score_faxing.unwrap_or(0) as i64, 0)),
1487 upper_strike_price: Some(dp(w.upper_price.unwrap_or(0), 9)),
1488 lower_strike_price: Some(dp(w.lower_price.unwrap_or(0), 9)),
1489 in_line_price_status: w.in_or_out.map(|v| {
1490 if v == 0 {
1491 1
1492 } else {
1493 2
1494 }
1495 }),
1496 issuer_code: None,
1497 },
1498 );
1499 }
1500 }
1501 8 => {
1502 if let Some(StockSpecificData::Option(ref o)) = s.stock_specific {
1504 let greek = o.greek.as_ref();
1505 option_ex = Some(
1506 futu_proto::qot_get_security_snapshot::OptionSnapshotExData {
1507 r#type: 0,
1508 owner: futu_proto::qot_common::Security {
1509 market: entry.qot_market,
1510 code: String::new(),
1511 },
1512 strike_time: String::new(),
1513 strike_price: dp(o.price_strike.unwrap_or(0), 9),
1514 contract_size: o.contract_size.unwrap_or(0) as i32,
1515 open_interest: o.open_interest.unwrap_or(0) as i32,
1516 implied_volatility: dp(o.implied_volatility.unwrap_or(0), 5),
1517 premium: dp(o.premium.unwrap_or(0), 5),
1518 delta: greek
1519 .map(|g| dp(g.hp_delta.unwrap_or(0), 9))
1520 .unwrap_or(0.0),
1521 gamma: greek
1522 .map(|g| dp(g.hp_gamma.unwrap_or(0), 9))
1523 .unwrap_or(0.0),
1524 vega: greek
1525 .map(|g| dp(g.hp_vega.unwrap_or(0), 9))
1526 .unwrap_or(0.0),
1527 theta: greek
1528 .map(|g| dp(g.hp_theta.unwrap_or(0), 9))
1529 .unwrap_or(0.0),
1530 rho: greek.map(|g| dp(g.hp_rho.unwrap_or(0), 9)).unwrap_or(0.0),
1531 strike_timestamp: None,
1532 index_option_type: None,
1533 net_open_interest: Some(o.open_interest_net.unwrap_or(0) as i32),
1534 expiry_date_distance: Some(
1535 o.distance_due_date.unwrap_or(0) as i32
1536 ),
1537 contract_nominal_value: Some(dp(
1538 o.contract_nominal_ammount.unwrap_or(0) as i64,
1539 9,
1540 )),
1541 owner_lot_multiplier: Some(
1542 o.positive_number_of_hand.unwrap_or(0) as f64,
1543 ),
1544 option_area_type: Some(o.option_type.unwrap_or(0) + 1),
1545 contract_multiplier: o.hp_multiplier.map(|m| dp(m as i64, 9)),
1546 contract_size_float: o
1547 .hp_contract_size
1548 .map(|c| dp(c as i64, 9)),
1549 },
1550 );
1551 }
1552 }
1553 6 => {
1554 if let Some(StockSpecificData::Index(ref i)) = s.stock_specific {
1556 index_ex =
1557 Some(futu_proto::qot_get_security_snapshot::IndexSnapshotExData {
1558 raise_count: i.raise_count.unwrap_or(0) as i32,
1559 fall_count: i.fall_count.unwrap_or(0) as i32,
1560 equal_count: i.equal_count.unwrap_or(0) as i32,
1561 });
1562 }
1563 }
1564 7 => {
1565 if let Some(StockSpecificData::Plate(ref p)) = s.stock_specific {
1567 plate_ex =
1568 Some(futu_proto::qot_get_security_snapshot::PlateSnapshotExData {
1569 raise_count: p.raise_count.unwrap_or(0) as i32,
1570 fall_count: p.fall_count.unwrap_or(0) as i32,
1571 equal_count: p.equal_count.unwrap_or(0) as i32,
1572 });
1573 }
1574 }
1575 10 => {
1576 if let Some(StockSpecificData::Future(ref f)) = s.stock_specific {
1578 future_ex = Some(
1579 futu_proto::qot_get_security_snapshot::FutureSnapshotExData {
1580 last_settle_price: dp(f.last_settlement_price.unwrap_or(0), 9),
1581 position: f.hold_vol.unwrap_or(0) as i32,
1582 position_change: f.diff_hold_vol.unwrap_or(0) as i32,
1583 last_trade_time: String::new(),
1584 last_trade_timestamp: None,
1585 is_main_contract: f.main_flag.unwrap_or(0) == 1,
1586 },
1587 );
1588 }
1589 }
1590 4 => {
1591 if let Some(StockSpecificData::Trust(ref t)) = s.stock_specific {
1593 trust_ex =
1594 Some(futu_proto::qot_get_security_snapshot::TrustSnapshotExData {
1595 dividend_yield: dp(t.dividend_yield.unwrap_or(0), 5),
1596 aum: t.aum.unwrap_or(0) as f64,
1597 outstanding_units: t.outstanding_units.unwrap_or(0) as i64,
1598 net_asset_value: dp(t.net_asset_value.unwrap_or(0) as i64, 9),
1599 premium: dp(t.premium.unwrap_or(0), 5),
1600 asset_class: t.asset_class.unwrap_or(0) as i32,
1601 });
1602 }
1603 }
1604 _ => {}
1605 }
1606 }
1607
1608 snapshot_list.push(futu_proto::qot_get_security_snapshot::Snapshot {
1609 basic,
1610 equity_ex_data: equity_ex,
1611 warrant_ex_data: warrant_ex,
1612 option_ex_data: option_ex,
1613 index_ex_data: index_ex,
1614 plate_ex_data: plate_ex,
1615 future_ex_data: future_ex,
1616 trust_ex_data: trust_ex,
1617 });
1618 }
1619
1620 tracing::debug!(
1621 conn_id,
1622 count = snapshot_list.len(),
1623 "GetSecuritySnapshot via CMD6824"
1624 );
1625
1626 let resp = futu_proto::qot_get_security_snapshot::Response {
1627 ret_type: 0,
1628 ret_msg: None,
1629 err_code: None,
1630 s2c: Some(futu_proto::qot_get_security_snapshot::S2c { snapshot_list }),
1631 };
1632 Some(prost::Message::encode_to_vec(&resp))
1633 }
1634}
1635
1636struct GetGlobalStateHandler {
1638 login_cache: Arc<LoginCache>,
1639 backend: crate::bridge::SharedBackend,
1640}
1641
1642#[async_trait]
1643impl RequestHandler for GetGlobalStateHandler {
1644 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
1645 let _req: futu_proto::get_global_state::Request =
1646 prost::Message::decode(request.body.as_ref()).ok()?;
1647
1648 let login_state = self.login_cache.get_login_state();
1649 let is_logged_in = login_state
1650 .as_ref()
1651 .map(|s| s.is_logged_in)
1652 .unwrap_or(false);
1653 let now = std::time::SystemTime::now()
1654 .duration_since(std::time::UNIX_EPOCH)
1655 .unwrap_or_default()
1656 .as_secs() as i64;
1657
1658 use futu_backend::stock_list::{pull_single_market_status, QuoteMktType};
1660 let mut market_hk: i32 = 0; let mut market_us: i32 = 0;
1662 let mut market_sh: i32 = 0;
1663 let mut market_sz: i32 = 0;
1664 let mut market_hk_future: i32 = 0;
1665 let mut market_us_future: Option<i32> = None;
1666 let mut market_sg_future: Option<i32> = None;
1667 let mut market_jp_future: Option<i32> = None;
1668
1669 if let Some(backend) = super::load_backend(&self.backend) {
1670 let queries = [
1671 (QuoteMktType::HK, "HK"),
1672 (QuoteMktType::US, "US"),
1673 (QuoteMktType::SH, "SH"),
1674 (QuoteMktType::SZ, "SZ"),
1675 (QuoteMktType::HKFuture, "HKFuture"),
1676 (QuoteMktType::USFuture, "USFuture"),
1677 (QuoteMktType::SGFuture, "SGFuture"),
1678 (QuoteMktType::JPFuture, "JPFuture"),
1679 ];
1680 for (mkt, name) in &queries {
1681 match pull_single_market_status(&backend, *mkt).await {
1682 Ok(statuses) if !statuses.is_empty() => {
1683 let state = statuses[0].status as i32;
1684 match *name {
1685 "HK" => market_hk = state,
1686 "US" => market_us = state,
1687 "SH" => market_sh = state,
1688 "SZ" => market_sz = state,
1689 "HKFuture" => market_hk_future = state,
1690 "USFuture" => market_us_future = Some(state),
1691 "SGFuture" => market_sg_future = Some(state),
1692 "JPFuture" => market_jp_future = Some(state),
1693 _ => {}
1694 }
1695 }
1696 Ok(_) => {
1697 tracing::debug!(market = name, "no market status data");
1698 }
1699 Err(e) => {
1700 tracing::debug!(market = name, error = %e, "market status query failed");
1701 }
1702 }
1703 }
1704 }
1705
1706 let resp = futu_proto::get_global_state::Response {
1707 ret_type: 0,
1708 ret_msg: None,
1709 err_code: None,
1710 s2c: Some(futu_proto::get_global_state::S2c {
1711 market_hk,
1712 market_us,
1713 market_sh,
1714 market_sz,
1715 market_hk_future,
1716 market_us_future,
1717 market_sg_future,
1718 market_jp_future,
1719 qot_logined: is_logged_in,
1720 trd_logined: is_logged_in,
1721 server_ver: 1002,
1722 server_build_no: 1,
1723 time: now,
1724 local_time: Some(now as f64),
1725 program_status: None,
1726 qot_svr_ip_addr: login_state.as_ref().map(|s| s.server_addr.clone()),
1727 trd_svr_ip_addr: login_state.as_ref().map(|s| s.server_addr.clone()),
1728 conn_id: Some(conn_id),
1729 }),
1730 };
1731 Some(prost::Message::encode_to_vec(&resp))
1732 }
1733}
1734
1735struct RequestHistoryKLHandler {
1737 backend: crate::bridge::SharedBackend,
1738 static_cache: Arc<StaticDataCache>,
1739 kl_quota_counter: Arc<std::sync::atomic::AtomicU32>,
1740}
1741
1742fn ftapi_kl_type_to_backend(kl_type: i32) -> Option<u32> {
1745 match kl_type {
1746 1 => Some(1), 2 => Some(2), 3 => Some(3), 4 => Some(4), 5 => Some(5), 6 => Some(6), 7 => Some(7), 8 => Some(8), 9 => Some(9), 10 => Some(10), 11 => Some(11), _ => None,
1758 }
1759}
1760
1761fn date_str_to_timestamp(s: &str, market: i32) -> Option<u64> {
1765 let date_part = s.split(' ').next().unwrap_or(s);
1767 let parts: Vec<&str> = date_part.split('-').collect();
1768 if parts.len() != 3 {
1769 return None;
1770 }
1771 let year: i32 = parts[0].parse().ok()?;
1772 let month: u32 = parts[1].parse().ok()?;
1773 let day: u32 = parts[2].parse().ok()?;
1774
1775 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
1776 return None;
1777 }
1778
1779 let mut total_days: i64 = 0;
1780 for y in 1970..year {
1781 total_days += if is_leap_year(y) { 366 } else { 365 };
1782 }
1783 let month_days = [
1784 31,
1785 if is_leap_year(year) { 29 } else { 28 },
1786 31,
1787 30,
1788 31,
1789 30,
1790 31,
1791 31,
1792 30,
1793 31,
1794 30,
1795 31,
1796 ];
1797 for &md in month_days.iter().take(month as usize - 1) {
1798 total_days += md as i64;
1799 }
1800 total_days += (day as i64) - 1;
1801
1802 if total_days < 0 {
1803 return None;
1804 }
1805 let utc_secs = total_days * 86400;
1806
1807 let tz_offset_hours: i64 = market_timezone_offset(market);
1809 let ts = utc_secs - tz_offset_hours * 3600;
1810 if ts < 0 {
1811 return None;
1812 }
1813 Some(ts as u64)
1814}
1815
1816fn market_timezone_offset(market: i32) -> i64 {
1818 match market {
1819 1 | 2 => 8,
1821 11 => -5,
1823 21 | 22 => 8,
1825 31 => 8,
1827 41 => 9,
1829 51 => 10,
1831 _ => 8,
1833 }
1834}
1835
1836fn is_leap_year(y: i32) -> bool {
1837 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
1838}
1839
1840fn timestamp_to_datetime_str(ts: u64) -> String {
1842 let (year, month, day, hour, minute, second) = timestamp_to_components(ts + 8 * 3600);
1844 format!(
1845 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
1846 year, month, day, hour, minute, second
1847 )
1848}
1849
1850fn timestamp_to_date_str(ts: u64) -> String {
1851 let (year, month, day, _, _, _) = timestamp_to_components(ts + 8 * 3600);
1852 format!("{:04}-{:02}-{:02}", year, month, day)
1853}
1854
1855fn parse_date_to_timestamp(s: &str) -> Option<i64> {
1857 let parts: Vec<&str> = s.split('-').collect();
1858 if parts.len() != 3 {
1859 return None;
1860 }
1861 let y: i64 = parts[0].parse().ok()?;
1862 let m: i64 = parts[1].parse().ok()?;
1863 let d: i64 = parts[2].parse().ok()?;
1864 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
1865 return None;
1866 }
1867 let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
1869 let days = 365 * y2 + y2 / 4 - y2 / 100 + y2 / 400 + (m2 * 306 + 5) / 10 + d - 1 - 719468;
1870 Some(days * 86400 - 8 * 3600) }
1872
1873fn backend_expiration_to_api(backend_type: u32) -> i32 {
1876 match backend_type {
1877 0 => 2, 1 => 1, 2 => 3, 3 => 4, 11 => 11, 12 => 12, 13 => 13, 14 => 14, 15 => 15, _ => 0, }
1888}
1889
1890fn timestamp_to_components(ts: u64) -> (i32, u32, u32, u32, u32, u32) {
1892 let secs = ts as i64;
1893 let second = ((secs % 60) + 60) as u32 % 60;
1894 let minute = (((secs / 60) % 60) + 60) as u32 % 60;
1895 let hour = (((secs / 3600) % 24) + 24) as u32 % 24;
1896
1897 let mut days = secs / 86400;
1898 let mut year = 1970_i32;
1899
1900 loop {
1901 let days_in_year: i64 = if is_leap_year(year) { 366 } else { 365 };
1902 if days < days_in_year {
1903 break;
1904 }
1905 days -= days_in_year;
1906 year += 1;
1907 }
1908
1909 let month_days = [
1910 31,
1911 if is_leap_year(year) { 29 } else { 28 },
1912 31,
1913 30,
1914 31,
1915 30,
1916 31,
1917 31,
1918 30,
1919 31,
1920 30,
1921 31,
1922 ];
1923 let mut month = 1_u32;
1924 for &md in &month_days {
1925 if days < md as i64 {
1926 break;
1927 }
1928 days -= md as i64;
1929 month += 1;
1930 }
1931 let day = days as u32 + 1;
1932
1933 (year, month, day, hour, minute, second)
1934}
1935
1936fn kline_item_to_ftapi(
1938 item: &futu_backend::proto_internal::ft_cmd_kline::KlineItem,
1939) -> futu_proto::qot_common::KLine {
1940 let close_raw = item.close_price.unwrap_or(0);
1941 let open_raw = item.open_price.unwrap_or(0);
1942 let high_raw = item.highest_price.unwrap_or(0);
1943 let low_raw = item.lowest_price.unwrap_or(0);
1944 let last_close_raw = item.last_close_price.unwrap_or(0);
1945
1946 let divisor = 1_000_000_000.0_f64;
1947
1948 let close_price = close_raw as f64 / divisor;
1949 let open_price = open_raw as f64 / divisor;
1950 let high_price = high_raw as f64 / divisor;
1951 let low_price = low_raw as f64 / divisor;
1952 let last_close_price = last_close_raw as f64 / divisor;
1953
1954 let turnover = item.turnover.unwrap_or(0) as f64 / 1_000.0;
1955 let turnover_rate = item.turnover_rate.unwrap_or(0) as f64 / 100_000.0;
1956 let pe = item.pe.unwrap_or(0) as f64 / 1_000.0;
1957 let volume = item.volume.unwrap_or(0) as i64;
1958 let timestamp = item.time.unwrap_or(0) as f64;
1959
1960 let change_rate = if last_close_raw != 0 {
1962 (close_raw - last_close_raw) as f64 / last_close_raw as f64 * 100.0
1963 } else {
1964 0.0
1965 };
1966
1967 let is_blank = item.close_price.is_none();
1969
1970 let ts = item.time.unwrap_or(0);
1972 let tz_offset = item.time_zone.unwrap_or(0) as i64;
1973 let time_str = format_timestamp_with_tz(ts, tz_offset);
1974
1975 if is_blank {
1976 return futu_proto::qot_common::KLine {
1978 time: time_str,
1979 is_blank: true,
1980 high_price: None,
1981 open_price: None,
1982 low_price: None,
1983 close_price: None,
1984 last_close_price: None,
1985 volume: None,
1986 turnover: None,
1987 turnover_rate: None,
1988 pe: None,
1989 change_rate: None,
1990 timestamp: Some(timestamp),
1991 };
1992 }
1993
1994 futu_proto::qot_common::KLine {
1995 time: time_str,
1996 is_blank: false,
1997 high_price: Some(high_price),
1998 open_price: Some(open_price),
1999 low_price: Some(low_price),
2000 close_price: Some(close_price),
2001 last_close_price: Some(last_close_price),
2002 volume: Some(volume),
2003 turnover: Some(turnover),
2004 turnover_rate: Some(turnover_rate),
2005 pe: Some(pe),
2006 change_rate: Some(change_rate),
2007 timestamp: Some(timestamp),
2008 }
2009}
2010
2011fn format_timestamp_with_tz(ts: u64, tz_hours: i64) -> String {
2013 let local_ts = ts as i64 + tz_hours * 3600;
2014 format_timestamp(local_ts as u64)
2015}
2016
2017fn format_timestamp(ts: u64) -> String {
2019 let secs = ts as i64;
2020 let days = secs.div_euclid(86400);
2021 let day_secs = secs.rem_euclid(86400);
2022 let hours = day_secs / 3600;
2023 let minutes = (day_secs % 3600) / 60;
2024 let seconds = day_secs % 60;
2025
2026 let mut remaining_days = days;
2028 let mut year = 1970_i32;
2029
2030 loop {
2031 let days_in_year: i64 = if is_leap_year(year) { 366 } else { 365 };
2032 if remaining_days < days_in_year {
2033 break;
2034 }
2035 remaining_days -= days_in_year;
2036 year += 1;
2037 }
2038
2039 let month_days = [
2040 31,
2041 if is_leap_year(year) { 29 } else { 28 },
2042 31,
2043 30,
2044 31,
2045 30,
2046 31,
2047 31,
2048 30,
2049 31,
2050 30,
2051 31,
2052 ];
2053 let mut month = 0_usize;
2054 while month < 12 && remaining_days >= month_days[month] as i64 {
2055 remaining_days -= month_days[month] as i64;
2056 month += 1;
2057 }
2058 let day = remaining_days + 1;
2059
2060 format!(
2061 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
2062 year,
2063 month + 1,
2064 day,
2065 hours,
2066 minutes,
2067 seconds
2068 )
2069}
2070
2071#[async_trait]
2072impl RequestHandler for RequestHistoryKLHandler {
2073 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2074 let req: futu_proto::qot_request_history_kl::Request =
2075 prost::Message::decode(request.body.as_ref()).ok()?;
2076 let c2s = &req.c2s;
2077
2078 let backend = match super::load_backend(&self.backend) {
2079 Some(b) => b,
2080 None => {
2081 tracing::warn!(conn_id, "RequestHistoryKL: no backend connection");
2082 return Some(super::make_error_response(-1, "no backend connection"));
2083 }
2084 };
2085
2086 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
2088 let stock_id = match self.static_cache.get_security_info(&sec_key) {
2089 Some(info) if info.stock_id > 0 => info.stock_id,
2090 _ => {
2091 tracing::warn!(conn_id, sec_key, "RequestHistoryKL: stock_id not found");
2092 return Some(super::make_error_response(
2093 -1,
2094 "security not found in cache",
2095 ));
2096 }
2097 };
2098
2099 let backend_kl_type = match ftapi_kl_type_to_backend(c2s.kl_type) {
2101 Some(v) => v,
2102 None => {
2103 tracing::warn!(
2104 conn_id,
2105 kl_type = c2s.kl_type,
2106 "RequestHistoryKL: invalid kl_type"
2107 );
2108 return Some(super::make_error_response(-1, "invalid kl_type"));
2109 }
2110 };
2111
2112 let exright_type = c2s.rehab_type as u32;
2114
2115 let qot_market = c2s.security.market;
2117 let begin_ts = date_str_to_timestamp(&c2s.begin_time, qot_market).unwrap_or(0);
2118 let end_ts = if c2s.end_time.is_empty() {
2120 u64::MAX
2121 } else {
2122 date_str_to_timestamp(&c2s.end_time, qot_market)
2124 .map(|t| t + 86399)
2125 .unwrap_or(u64::MAX)
2126 };
2127
2128 let max_kl_num = c2s.max_ack_kl_num.unwrap_or(0) as usize;
2131
2132 let kline_req = futu_backend::proto_internal::ft_cmd_kline::KlineReq {
2134 security_id: Some(stock_id),
2135 kline_type: Some(backend_kl_type),
2136 exright_type: Some(exright_type),
2137 data_set_type: Some(0), data_range_type: Some(1), begin_time: Some(begin_ts),
2140 end_time: Some(end_ts),
2141 item_count: None,
2142 end_time_offset: None,
2143 };
2144
2145 let body = prost::Message::encode_to_vec(&kline_req);
2146
2147 tracing::debug!(
2148 conn_id,
2149 stock_id,
2150 kl_type = backend_kl_type,
2151 exright = exright_type,
2152 body_len = body.len(),
2153 "sending CMD6161 KlineReq"
2154 );
2155
2156 let resp_frame = match backend.request(6161, body).await {
2158 Ok(f) => f,
2159 Err(e) => {
2160 tracing::error!(conn_id, error = %e, "CMD6161 request failed");
2161 return Some(super::make_error_response(-1, "backend request failed"));
2162 }
2163 };
2164
2165 let kline_rsp: futu_backend::proto_internal::ft_cmd_kline::KlineRsp =
2167 match prost::Message::decode(resp_frame.body.as_ref()) {
2168 Ok(r) => r,
2169 Err(e) => {
2170 tracing::error!(conn_id, error = %e, body_len = resp_frame.body.len(), "CMD6161 decode failed");
2171 return Some(super::make_error_response(
2172 -1,
2173 "backend response decode failed",
2174 ));
2175 }
2176 };
2177
2178 let result = kline_rsp.result.unwrap_or(-1);
2179 if result != 0 {
2180 tracing::warn!(conn_id, result, "CMD6161 returned error");
2181 return Some(super::make_error_response(
2182 -1,
2183 "backend kline request failed",
2184 ));
2185 }
2186
2187 let mut kl_list: Vec<futu_proto::qot_common::KLine> = kline_rsp
2190 .kline_item_list
2191 .iter()
2192 .map(kline_item_to_ftapi)
2193 .collect();
2194 if max_kl_num > 0 && kl_list.len() > max_kl_num {
2195 kl_list.truncate(max_kl_num);
2196 }
2197
2198 tracing::debug!(
2199 conn_id,
2200 count = kl_list.len(),
2201 "RequestHistoryKL returning klines"
2202 );
2203
2204 let stock_name = self
2206 .static_cache
2207 .get_security_info(&sec_key)
2208 .map(|info| info.name.clone());
2209
2210 self.kl_quota_counter
2212 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2213
2214 let resp = futu_proto::qot_request_history_kl::Response {
2215 ret_type: 0,
2216 ret_msg: None,
2217 err_code: None,
2218 s2c: Some(futu_proto::qot_request_history_kl::S2c {
2219 security: c2s.security.clone(),
2220 name: stock_name,
2221 kl_list,
2222 next_req_key: None,
2223 }),
2224 };
2225 Some(prost::Message::encode_to_vec(&resp))
2226 }
2227}
2228
2229struct RequestTradeDateHandler {
2231 backend: crate::bridge::SharedBackend,
2232}
2233
2234fn ftapi_market_to_backend(market: i32) -> Option<u32> {
2236 match market {
2237 1 => Some(1), 11 => Some(10), 21 => Some(30), 22 => Some(31), _ => None,
2242 }
2243}
2244
2245fn date_str_to_yyyymmdd(s: &str) -> Option<u32> {
2247 let date_part = s.split(' ').next().unwrap_or(s);
2249 let parts: Vec<&str> = date_part.split('-').collect();
2250 if parts.len() != 3 {
2251 return None;
2252 }
2253 let year: u32 = parts[0].parse().ok()?;
2254 let month: u32 = parts[1].parse().ok()?;
2255 let day: u32 = parts[2].parse().ok()?;
2256 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
2257 return None;
2258 }
2259 Some(year * 10000 + month * 100 + day)
2260}
2261
2262fn yyyymmdd_to_date_str(v: u32) -> String {
2264 let year = v / 10000;
2265 let month = (v % 10000) / 100;
2266 let day = v % 100;
2267 format!("{:04}-{:02}-{:02}", year, month, day)
2268}
2269
2270fn backend_trading_type_to_ftapi(trading_type: u32) -> i32 {
2273 match trading_type {
2274 4 => 0, 0 => 1, 1 => 2, 2 => 3, _ => 0, }
2280}
2281
2282#[async_trait]
2283impl RequestHandler for RequestTradeDateHandler {
2284 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2285 let req: futu_proto::qot_request_trade_date::Request =
2286 prost::Message::decode(request.body.as_ref()).ok()?;
2287 let c2s = &req.c2s;
2288
2289 let backend = match super::load_backend(&self.backend) {
2290 Some(b) => b,
2291 None => {
2292 tracing::warn!(conn_id, "RequestTradeDate: no backend connection");
2293 return Some(super::make_error_response(-1, "no backend connection"));
2294 }
2295 };
2296
2297 let backend_market = match ftapi_market_to_backend(c2s.market) {
2298 Some(m) => m,
2299 None => {
2300 tracing::warn!(
2301 conn_id,
2302 market = c2s.market,
2303 "RequestTradeDate: unsupported market"
2304 );
2305 return Some(super::make_error_response(-1, "unsupported market"));
2306 }
2307 };
2308
2309 let begin_key = match date_str_to_yyyymmdd(&c2s.begin_time) {
2310 Some(v) => v,
2311 None => {
2312 tracing::warn!(conn_id, begin = %c2s.begin_time, "RequestTradeDate: invalid begin_time");
2313 return Some(super::make_error_response(-1, "invalid begin_time"));
2314 }
2315 };
2316
2317 let end_key = match date_str_to_yyyymmdd(&c2s.end_time) {
2318 Some(v) => v,
2319 None => {
2320 tracing::warn!(conn_id, end = %c2s.end_time, "RequestTradeDate: invalid end_time");
2321 return Some(super::make_error_response(-1, "invalid end_time"));
2322 }
2323 };
2324
2325 let range_req = futu_backend::proto_internal::market_trading_day::RangeTradingDayReq {
2326 begin_date_key: Some(begin_key),
2327 end_date_key: Some(end_key),
2328 market_id: Some(backend_market),
2329 };
2330
2331 let body = prost::Message::encode_to_vec(&range_req);
2332
2333 tracing::debug!(
2334 conn_id,
2335 backend_market,
2336 begin_key,
2337 end_key,
2338 "sending CMD6733 RangeTradingDayReq"
2339 );
2340
2341 let resp_frame = match backend.request(6733, body).await {
2342 Ok(f) => f,
2343 Err(e) => {
2344 tracing::error!(conn_id, error = %e, "CMD6733 request failed");
2345 return Some(super::make_error_response(-1, "backend request failed"));
2346 }
2347 };
2348
2349 let range_rsp: futu_backend::proto_internal::market_trading_day::RangeTradingDayRsp =
2350 match prost::Message::decode(resp_frame.body.as_ref()) {
2351 Ok(r) => r,
2352 Err(e) => {
2353 tracing::error!(conn_id, error = %e, "CMD6733 decode failed");
2354 return Some(super::make_error_response(
2355 -1,
2356 "backend response decode failed",
2357 ));
2358 }
2359 };
2360
2361 let code = range_rsp.code.unwrap_or(-1);
2362 if code != 0 {
2363 tracing::warn!(conn_id, code, "CMD6733 returned error");
2364 return Some(super::make_error_response(
2365 -1,
2366 "backend trade date request failed",
2367 ));
2368 }
2369
2370 let trade_date_list: Vec<futu_proto::qot_request_trade_date::TradeDate> = range_rsp
2372 .day_infos
2373 .iter()
2374 .map(|day| {
2375 let time_date = day.time_date.unwrap_or(0);
2376 let time_str = yyyymmdd_to_date_str(time_date);
2377 let trading_type = day.trading_type.unwrap_or(4); let trade_date_type = backend_trading_type_to_ftapi(trading_type);
2379 let ts = day.date_key.unwrap_or(0) as f64;
2381
2382 futu_proto::qot_request_trade_date::TradeDate {
2383 time: time_str,
2384 timestamp: Some(ts),
2385 trade_date_type: Some(trade_date_type),
2386 }
2387 })
2388 .collect();
2389
2390 tracing::debug!(
2391 conn_id,
2392 count = trade_date_list.len(),
2393 "RequestTradeDate returning trade dates"
2394 );
2395
2396 let resp = futu_proto::qot_request_trade_date::Response {
2397 ret_type: 0,
2398 ret_msg: None,
2399 err_code: None,
2400 s2c: Some(futu_proto::qot_request_trade_date::S2c { trade_date_list }),
2401 };
2402 Some(prost::Message::encode_to_vec(&resp))
2403 }
2404}
2405
2406struct GetRehabHandler {
2408 backend: crate::bridge::SharedBackend,
2409 static_cache: Arc<StaticDataCache>,
2410}
2411
2412#[async_trait]
2413impl RequestHandler for GetRehabHandler {
2414 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2415 let req: futu_proto::qot_request_rehab::Request =
2416 prost::Message::decode(request.body.as_ref()).ok()?;
2417 let c2s = &req.c2s;
2418
2419 let backend = match super::load_backend(&self.backend) {
2420 Some(b) => b,
2421 None => {
2422 tracing::warn!(conn_id, "GetRehab: no backend connection");
2423 return Some(super::make_error_response(-1, "no backend connection"));
2424 }
2425 };
2426
2427 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
2429 let stock_id = match self.static_cache.get_security_info(&sec_key) {
2430 Some(info) if info.stock_id > 0 => info.stock_id,
2431 _ => {
2432 tracing::warn!(conn_id, sec_key, "GetRehab: stock_id not found");
2433 return Some(super::make_error_response(
2434 -1,
2435 "security not found in cache",
2436 ));
2437 }
2438 };
2439
2440 let exright_req = futu_backend::proto_internal::ft_cmd6811::ExrightFactorReq {
2441 stock_id: Some(stock_id),
2442 sequence: Some(0),
2443 };
2444
2445 let body = prost::Message::encode_to_vec(&exright_req);
2446
2447 tracing::debug!(conn_id, stock_id, "sending CMD6811 ExrightFactorReq");
2448
2449 let resp_frame = match backend.request(6811, body).await {
2450 Ok(f) => f,
2451 Err(e) => {
2452 tracing::error!(conn_id, error = %e, "CMD6811 request failed");
2453 return Some(super::make_error_response(-1, "backend request failed"));
2454 }
2455 };
2456
2457 let exright_rsp: futu_backend::proto_internal::ft_cmd6811::ExrightFactorRsp =
2458 match prost::Message::decode(resp_frame.body.as_ref()) {
2459 Ok(r) => r,
2460 Err(e) => {
2461 tracing::error!(conn_id, error = %e, "CMD6811 decode failed");
2462 return Some(super::make_error_response(
2463 -1,
2464 "backend response decode failed",
2465 ));
2466 }
2467 };
2468
2469 let result = exright_rsp.result.unwrap_or(-1);
2470 if result != 0 && result != 1 {
2472 tracing::warn!(conn_id, result, "CMD6811 returned error");
2473 return Some(super::make_error_response(
2474 -1,
2475 "backend rehab request failed",
2476 ));
2477 }
2478
2479 let rehab_list: Vec<futu_proto::qot_common::Rehab> = exright_rsp
2481 .exs
2482 .iter()
2483 .map(|item| {
2484 let ex_ts = item.ex_time.unwrap_or(0);
2485 let time_str = timestamp_to_date_str(ex_ts as u64);
2486
2487 let mut flag: i64 = 0;
2489 if item.split_base.is_some() {
2490 flag |= 1;
2491 }
2492 if item.join_base.is_some() {
2493 flag |= 2;
2494 }
2495 if item.bonus_base.is_some() {
2496 flag |= 4;
2497 }
2498 if item.transfer_base.is_some() {
2499 flag |= 8;
2500 }
2501 if item.allot_base.is_some() {
2502 flag |= 16;
2503 }
2504 if item.add_base.is_some() {
2505 flag |= 32;
2506 }
2507 if item.dividend_amount.is_some() {
2508 flag |= 64;
2509 }
2510 if item.sp_dividend_amount.is_some() {
2511 flag |= 128;
2512 }
2513
2514 let divisor = 100_000.0;
2515
2516 futu_proto::qot_common::Rehab {
2517 time: time_str,
2518 company_act_flag: flag,
2519 fwd_factor_a: item.origin_fwd_a.unwrap_or(100_000.0) / divisor,
2520 fwd_factor_b: item.origin_fwd_b.unwrap_or(0.0) / divisor,
2521 bwd_factor_a: item.origin_bwd_a.unwrap_or(100_000.0) / divisor,
2522 bwd_factor_b: item.origin_bwd_b.unwrap_or(0.0) / divisor,
2523 split_base: item.split_base.map(|v| v as i32),
2524 split_ert: item.split_ert.map(|v| v as i32),
2525 join_base: item.join_base.map(|v| v as i32),
2526 join_ert: item.join_ert.map(|v| v as i32),
2527 bonus_base: item.bonus_base.map(|v| v as i32),
2528 bonus_ert: item.bonus_ert.map(|v| v as i32),
2529 transfer_base: item.transfer_base.map(|v| v as i32),
2530 transfer_ert: item.transfer_ert.map(|v| v as i32),
2531 allot_base: item.allot_base.map(|v| v as i32),
2532 allot_ert: item.allot_ert.map(|v| v as i32),
2533 allot_price: item.allot_price.map(|v| v as f64 / divisor),
2534 add_base: item.add_base.map(|v| v as i32),
2535 add_ert: item.add_ert.map(|v| v as i32),
2536 add_price: item.add_price.map(|v| v as f64 / divisor),
2537 dividend: item.dividend_amount.map(|v| v as f64 / divisor),
2538 sp_dividend: item.sp_dividend_amount.map(|v| v as f64 / divisor),
2539 timestamp: Some(ex_ts as f64),
2540 spin_off_base: item.spin_off_base.map(|v| v as f64),
2541 spin_off_ert: item.spin_off_ert.map(|v| v as f64),
2542 }
2543 })
2544 .collect();
2545
2546 tracing::debug!(
2547 conn_id,
2548 count = rehab_list.len(),
2549 "GetRehab returning rehab items"
2550 );
2551
2552 let resp = futu_proto::qot_request_rehab::Response {
2553 ret_type: 0,
2554 ret_msg: None,
2555 err_code: None,
2556 s2c: Some(futu_proto::qot_request_rehab::S2c { rehab_list }),
2557 };
2558 Some(prost::Message::encode_to_vec(&resp))
2559 }
2560}
2561
2562struct GetPlateSetHandler {
2564 backend: crate::bridge::SharedBackend,
2565 static_cache: Arc<StaticDataCache>,
2566}
2567
2568fn map_plate_set_id(market: i32, plate_set_type: i32) -> Option<u64> {
2570 match (market, plate_set_type) {
2571 (1, 0) => Some(9700009), (1, 1) => Some(9700000), (1, 2) => Some(9700008), (1, 3) => Some(9700001), (1, 4) => Some(9700008), (11, 0) => Some(9700309), (11, 1) => Some(9700300), (11, 2) => Some(9700308), (11, 3) => Some(9700301), (11, 4) => Some(9700308), (21 | 22, 0) => Some(9700609), (21 | 22, 1) => Some(9700600), (21 | 22, 2) => Some(9700602), (21 | 22, 3) => Some(9700601), (21 | 22, 4) => Some(9700608), _ => None,
2590 }
2591}
2592
2593#[async_trait]
2594impl RequestHandler for GetPlateSetHandler {
2595 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2596 let req: futu_proto::qot_get_plate_set::Request =
2597 prost::Message::decode(request.body.as_ref()).ok()?;
2598 let c2s = &req.c2s;
2599
2600 let backend = match super::load_backend(&self.backend) {
2601 Some(b) => b,
2602 None => {
2603 tracing::warn!(conn_id, "GetPlateSet: no backend connection");
2604 return Some(super::make_error_response(-1, "no backend connection"));
2605 }
2606 };
2607
2608 let plate_set_id = match map_plate_set_id(c2s.market, c2s.plate_set_type) {
2609 Some(id) => id,
2610 None => {
2611 tracing::warn!(
2612 conn_id,
2613 market = c2s.market,
2614 plate_set_type = c2s.plate_set_type,
2615 "GetPlateSet: unsupported market/plateSetType"
2616 );
2617 return Some(super::make_error_response(
2618 -1,
2619 "unsupported market or plateSetType",
2620 ));
2621 }
2622 };
2623
2624 let plate_req = futu_backend::proto_internal::ft_cmd_plate::PlateListIDsReq {
2627 plate_id: plate_set_id,
2628 sort_type: 1, sort_id: 100, data_from: Some(0),
2631 data_max_count: Some(99999),
2632 check_code: Some(0),
2633 };
2634 let body = prost::Message::encode_to_vec(&plate_req);
2635
2636 tracing::debug!(
2637 conn_id,
2638 plate_set_id,
2639 "sending CMD6600 PlateListIDsReq for plate set"
2640 );
2641
2642 let resp_frame = match backend.request(6600, body).await {
2643 Ok(f) => f,
2644 Err(e) => {
2645 tracing::error!(conn_id, error = %e, "CMD6600 request failed");
2646 return Some(super::make_error_response(-1, "backend request failed"));
2647 }
2648 };
2649
2650 let plate_rsp: futu_backend::proto_internal::ft_cmd_plate::PlateListIDsRsp =
2651 match prost::Message::decode(resp_frame.body.as_ref()) {
2652 Ok(r) => r,
2653 Err(e) => {
2654 tracing::error!(conn_id, error = %e, "CMD6600 decode failed");
2655 return Some(super::make_error_response(
2656 -1,
2657 "backend response decode failed",
2658 ));
2659 }
2660 };
2661
2662 if plate_rsp.result != 0 {
2663 tracing::warn!(
2664 conn_id,
2665 result = plate_rsp.result,
2666 "CMD6600 returned error for plate set"
2667 );
2668 return Some(super::make_error_response(
2669 -1,
2670 "backend plate set request failed",
2671 ));
2672 }
2673
2674 let plate_info_list: Vec<futu_proto::qot_common::PlateInfo> = plate_rsp
2678 .arry_items
2679 .iter()
2680 .filter_map(|&pid| {
2681 let key = self.static_cache.id_to_key.get(&pid);
2683 if key.is_none() {
2684 tracing::warn!(
2685 conn_id,
2686 plate_id = pid,
2687 "plate_id not found in id_to_key cache"
2688 );
2689 return None;
2690 }
2691 let key = key.unwrap();
2692 let info = self.static_cache.get_security_info(&key)?;
2693
2694 Some(futu_proto::qot_common::PlateInfo {
2695 plate: futu_proto::qot_common::Security {
2696 market: info.market,
2697 code: info.code.clone(),
2698 },
2699 name: info.name.clone(),
2700 plate_type: None,
2701 })
2702 })
2703 .collect();
2704
2705 tracing::debug!(
2706 conn_id,
2707 count = plate_info_list.len(),
2708 "GetPlateSet returning plates"
2709 );
2710
2711 let resp = futu_proto::qot_get_plate_set::Response {
2712 ret_type: 0,
2713 ret_msg: None,
2714 err_code: None,
2715 s2c: Some(futu_proto::qot_get_plate_set::S2c { plate_info_list }),
2716 };
2717 Some(prost::Message::encode_to_vec(&resp))
2718 }
2719}
2720
2721struct GetPlateSecurityHandler {
2723 backend: crate::bridge::SharedBackend,
2724 static_cache: Arc<StaticDataCache>,
2725}
2726
2727#[async_trait]
2728impl RequestHandler for GetPlateSecurityHandler {
2729 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2730 let req: futu_proto::qot_get_plate_security::Request =
2731 prost::Message::decode(request.body.as_ref()).ok()?;
2732 let c2s = &req.c2s;
2733
2734 let backend = match super::load_backend(&self.backend) {
2735 Some(b) => b,
2736 None => {
2737 tracing::warn!(conn_id, "GetPlateSecurity: no backend connection");
2738 return Some(super::make_error_response(-1, "no backend connection"));
2739 }
2740 };
2741
2742 let key = futu_cache::qot_cache::make_key(c2s.plate.market, &c2s.plate.code);
2746 let plate_id: u64 = match self.static_cache.get_security_info(&key) {
2747 Some(info) => info.stock_id,
2748 None => {
2749 tracing::warn!(
2750 conn_id,
2751 market = c2s.plate.market,
2752 code = %c2s.plate.code,
2753 "GetPlateSecurity: plate not found in static cache"
2754 );
2755 return Some(super::make_error_response(-1, "invalid plate code"));
2756 }
2757 };
2758
2759 let sort_type = if c2s.ascend.unwrap_or(true) { 0 } else { 1 };
2761 let sort_id = c2s.sort_field.unwrap_or(0);
2762
2763 let plate_req = futu_backend::proto_internal::ft_cmd_plate::PlateListIDsReq {
2765 plate_id,
2766 sort_type,
2767 sort_id,
2768 data_from: None,
2769 data_max_count: None,
2770 check_code: None,
2771 };
2772 let body = prost::Message::encode_to_vec(&plate_req);
2773
2774 tracing::debug!(conn_id, plate_id, "sending CMD6600 PlateListIDsReq");
2775
2776 let resp_frame = match backend.request(6600, body).await {
2777 Ok(f) => f,
2778 Err(e) => {
2779 tracing::error!(conn_id, error = %e, "CMD6600 request failed");
2780 return Some(super::make_error_response(-1, "backend request failed"));
2781 }
2782 };
2783
2784 let plate_rsp: futu_backend::proto_internal::ft_cmd_plate::PlateListIDsRsp =
2785 match prost::Message::decode(resp_frame.body.as_ref()) {
2786 Ok(r) => r,
2787 Err(e) => {
2788 tracing::error!(conn_id, error = %e, "CMD6600 decode failed");
2789 return Some(super::make_error_response(
2790 -1,
2791 "backend response decode failed",
2792 ));
2793 }
2794 };
2795
2796 if plate_rsp.result != 0 {
2797 tracing::warn!(conn_id, result = plate_rsp.result, "CMD6600 returned error");
2798 return Some(super::make_error_response(
2799 -1,
2800 "backend plate request failed",
2801 ));
2802 }
2803
2804 let static_info_list: Vec<futu_proto::qot_common::SecurityStaticInfo> = plate_rsp
2806 .arry_items
2807 .iter()
2808 .filter_map(|&stock_id| {
2809 let key = self.static_cache.id_to_key.get(&stock_id)?;
2810 let info = self.static_cache.get_security_info(key.value())?;
2811 Some(futu_proto::qot_common::SecurityStaticInfo {
2812 basic: futu_proto::qot_common::SecurityStaticBasic {
2813 security: futu_proto::qot_common::Security {
2814 market: info.market,
2815 code: info.code.clone(),
2816 },
2817 id: info.stock_id as i64,
2818 lot_size: info.lot_size,
2819 sec_type: info.sec_type,
2820 name: info.name.clone(),
2821 list_time: info.list_time.clone(),
2822 delisting: None,
2823 list_timestamp: None,
2824 exch_type: None,
2825 },
2826 warrant_ex_data: None,
2827 option_ex_data: None,
2828 future_ex_data: None,
2829 })
2830 })
2831 .collect();
2832
2833 tracing::debug!(
2834 conn_id,
2835 count = static_info_list.len(),
2836 "GetPlateSecurity returning securities"
2837 );
2838
2839 let resp = futu_proto::qot_get_plate_security::Response {
2840 ret_type: 0,
2841 ret_msg: None,
2842 err_code: None,
2843 s2c: Some(futu_proto::qot_get_plate_security::S2c { static_info_list }),
2844 };
2845 Some(prost::Message::encode_to_vec(&resp))
2846 }
2847}
2848
2849struct GetOwnerPlateHandler {
2851 backend: crate::bridge::SharedBackend,
2852 static_cache: Arc<StaticDataCache>,
2853}
2854
2855#[async_trait]
2856impl RequestHandler for GetOwnerPlateHandler {
2857 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
2858 let req: futu_proto::qot_get_owner_plate::Request =
2859 prost::Message::decode(request.body.as_ref()).ok()?;
2860 let c2s = &req.c2s;
2861
2862 let backend = match super::load_backend(&self.backend) {
2863 Some(b) => b,
2864 None => {
2865 tracing::warn!(conn_id, "GetOwnerPlate: no backend connection");
2866 return Some(super::make_error_response(-1, "no backend connection"));
2867 }
2868 };
2869
2870 let mut stock_ids: Vec<u64> = Vec::new();
2872 let mut sec_info_map: Vec<(futu_proto::qot_common::Security, u64)> = Vec::new();
2873
2874 for sec in &c2s.security_list {
2875 let sec_key = format!("{}_{}", sec.market, sec.code);
2876 if let Some(info) = self.static_cache.get_security_info(&sec_key) {
2877 if info.stock_id > 0 {
2878 stock_ids.push(info.stock_id);
2879 sec_info_map.push((sec.clone(), info.stock_id));
2880 }
2881 }
2882 }
2883
2884 if stock_ids.is_empty() {
2885 let resp = futu_proto::qot_get_owner_plate::Response {
2887 ret_type: 0,
2888 ret_msg: None,
2889 err_code: None,
2890 s2c: Some(futu_proto::qot_get_owner_plate::S2c {
2891 owner_plate_list: vec![],
2892 }),
2893 };
2894 return Some(prost::Message::encode_to_vec(&resp));
2895 }
2896
2897 let owner_req = futu_backend::proto_internal::ft_cmd_plate::OwnerPlateReq {
2899 stock_id_list: stock_ids,
2900 owner_plate_type: 0, need_plate_quote: None,
2902 };
2903 let body = prost::Message::encode_to_vec(&owner_req);
2904
2905 tracing::debug!(
2906 conn_id,
2907 count = sec_info_map.len(),
2908 "sending CMD6608 OwnerPlateReq"
2909 );
2910
2911 let resp_frame = match backend.request(6608, body).await {
2912 Ok(f) => f,
2913 Err(e) => {
2914 tracing::error!(conn_id, error = %e, "CMD6608 request failed");
2915 return Some(super::make_error_response(-1, "backend request failed"));
2916 }
2917 };
2918
2919 let owner_rsp: futu_backend::proto_internal::ft_cmd_plate::OwnerPlateRsp =
2920 match prost::Message::decode(resp_frame.body.as_ref()) {
2921 Ok(r) => r,
2922 Err(e) => {
2923 tracing::error!(conn_id, error = %e, "CMD6608 decode failed");
2924 return Some(super::make_error_response(
2925 -1,
2926 "backend response decode failed",
2927 ));
2928 }
2929 };
2930
2931 if owner_rsp.result != 0 {
2932 tracing::warn!(conn_id, result = owner_rsp.result, "CMD6608 returned error");
2933 return Some(super::make_error_response(
2934 -1,
2935 "backend owner plate request failed",
2936 ));
2937 }
2938
2939 let owner_plate_list: Vec<futu_proto::qot_get_owner_plate::SecurityOwnerPlate> = owner_rsp
2941 .stock_info_list
2942 .iter()
2943 .filter_map(|stock_info| {
2944 let (sec, _) = sec_info_map
2946 .iter()
2947 .find(|(_, sid)| *sid == stock_info.stock_id)?;
2948 let sec_key = format!("{}_{}", sec.market, sec.code);
2949 let cached_info = self.static_cache.get_security_info(&sec_key);
2950
2951 let plate_info_list: Vec<futu_proto::qot_common::PlateInfo> = stock_info
2952 .plate_info_list
2953 .iter()
2954 .filter_map(|pi| {
2955 let key = self.static_cache.id_to_key.get(&pi.plate_id)?;
2957 let plate_info = self.static_cache.get_security_info(&key)?;
2958
2959 Some(futu_proto::qot_common::PlateInfo {
2960 plate: futu_proto::qot_common::Security {
2961 market: plate_info.market,
2962 code: plate_info.code.clone(),
2963 },
2964 name: plate_info.name.clone(),
2965 plate_type: pi.owner_set_id.map(|set_id| {
2966 match set_id {
2969 9700000 | 9700300 | 9700600 => 1, 9700602 => 2, 9700001 | 9700301 | 9700601 => 3, _ => 4, }
2974 }),
2975 })
2976 })
2977 .collect();
2978
2979 Some(futu_proto::qot_get_owner_plate::SecurityOwnerPlate {
2980 security: sec.clone(),
2981 name: cached_info.map(|i| i.name),
2982 plate_info_list,
2983 })
2984 })
2985 .collect();
2986
2987 tracing::debug!(
2988 conn_id,
2989 count = owner_plate_list.len(),
2990 "GetOwnerPlate returning results"
2991 );
2992
2993 let resp = futu_proto::qot_get_owner_plate::Response {
2994 ret_type: 0,
2995 ret_msg: None,
2996 err_code: None,
2997 s2c: Some(futu_proto::qot_get_owner_plate::S2c { owner_plate_list }),
2998 };
2999 Some(prost::Message::encode_to_vec(&resp))
3000 }
3001}
3002
3003struct GetMarketStateHandler {
3005 backend: crate::bridge::SharedBackend,
3006 static_cache: Arc<StaticDataCache>,
3007}
3008
3009#[async_trait]
3010impl RequestHandler for GetMarketStateHandler {
3011 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3012 let req: futu_proto::qot_get_market_state::Request =
3013 prost::Message::decode(request.body.as_ref()).ok()?;
3014
3015 let backend = match super::load_backend(&self.backend) {
3016 Some(b) => b,
3017 None => {
3018 tracing::warn!(conn_id, "GetMarketState: no backend connection");
3019 return Some(super::make_error_response(-1, "no backend connection"));
3020 }
3021 };
3022
3023 let mut markets_needed: std::collections::HashSet<i32> = std::collections::HashSet::new();
3025 for sec in &req.c2s.security_list {
3026 markets_needed.insert(sec.market);
3027 }
3028
3029 let mut market_status_map: std::collections::HashMap<i32, u32> =
3033 std::collections::HashMap::new();
3034 for &qot_market in &markets_needed {
3035 if let Some(mkt) = futu_backend::stock_list::qot_market_to_backend(qot_market) {
3036 match futu_backend::stock_list::pull_single_market_status(&backend, mkt).await {
3037 Ok(statuses) => {
3038 if let Some(first) = statuses.first() {
3041 market_status_map.insert(qot_market, first.status);
3042 tracing::debug!(
3043 conn_id,
3044 qot_market,
3045 status = first.status,
3046 text = %first.status_text,
3047 "GetMarketState: backend status"
3048 );
3049 }
3050 }
3051 Err(e) => {
3052 tracing::warn!(
3053 conn_id,
3054 qot_market,
3055 error = %e,
3056 "GetMarketState: backend request failed, using default"
3057 );
3058 }
3059 }
3060 }
3061 }
3062
3063 let market_info_list: Vec<futu_proto::qot_get_market_state::MarketInfo> = req
3064 .c2s
3065 .security_list
3066 .iter()
3067 .map(|sec| {
3068 let sec_key = format!("{}_{}", sec.market, sec.code);
3069 let name = self
3070 .static_cache
3071 .get_security_info(&sec_key)
3072 .map(|info| info.name)
3073 .unwrap_or_default();
3074
3075 let market_state = market_status_map.get(&sec.market).copied().unwrap_or(0) as i32;
3077
3078 futu_proto::qot_get_market_state::MarketInfo {
3079 security: sec.clone(),
3080 name,
3081 market_state,
3082 }
3083 })
3084 .collect();
3085
3086 tracing::debug!(conn_id, count = market_info_list.len(), "GetMarketState");
3087
3088 let resp = futu_proto::qot_get_market_state::Response {
3089 ret_type: 0,
3090 ret_msg: None,
3091 err_code: None,
3092 s2c: Some(futu_proto::qot_get_market_state::S2c { market_info_list }),
3093 };
3094 Some(prost::Message::encode_to_vec(&resp))
3095 }
3096}
3097
3098struct GetOptionExpirationDateHandler {
3100 backend: crate::bridge::SharedBackend,
3101 static_cache: Arc<StaticDataCache>,
3102}
3103
3104#[async_trait]
3105impl RequestHandler for GetOptionExpirationDateHandler {
3106 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3107 let req: futu_proto::qot_get_option_expiration_date::Request =
3108 prost::Message::decode(request.body.as_ref()).ok()?;
3109 let c2s = &req.c2s;
3110
3111 let backend = match super::load_backend(&self.backend) {
3112 Some(b) => b,
3113 None => {
3114 tracing::warn!(conn_id, "GetOptionExpirationDate: no backend connection");
3115 return Some(super::make_error_response(-1, "no backend connection"));
3116 }
3117 };
3118
3119 let sec_key = format!("{}_{}", c2s.owner.market, c2s.owner.code);
3121 let stock_id = match self.static_cache.get_security_info(&sec_key) {
3122 Some(info) if info.stock_id > 0 => info.stock_id,
3123 _ => {
3124 tracing::warn!(
3125 conn_id,
3126 sec_key,
3127 "GetOptionExpirationDate: stock_id not found"
3128 );
3129 return Some(super::make_error_response(
3130 -1,
3131 "security not found in cache",
3132 ));
3133 }
3134 };
3135
3136 let strike_req = futu_backend::proto_internal::ftcmd_option_chain::StrikeDateReq {
3138 stock_id: Some(stock_id),
3139 };
3140 let body = prost::Message::encode_to_vec(&strike_req);
3141
3142 tracing::debug!(conn_id, stock_id, "sending CMD6311 StrikeDateReq");
3143
3144 let resp_frame = match backend.request(6311, body).await {
3145 Ok(f) => f,
3146 Err(e) => {
3147 tracing::error!(conn_id, error = %e, "CMD6311 request failed");
3148 return Some(super::make_error_response(-1, "backend request failed"));
3149 }
3150 };
3151
3152 let strike_rsp: futu_backend::proto_internal::ftcmd_option_chain::StrikeDateRsp =
3153 match prost::Message::decode(resp_frame.body.as_ref()) {
3154 Ok(r) => r,
3155 Err(e) => {
3156 tracing::error!(conn_id, error = %e, "CMD6311 decode failed");
3157 return Some(super::make_error_response(
3158 -1,
3159 "backend response decode failed",
3160 ));
3161 }
3162 };
3163
3164 if strike_rsp.ret.unwrap_or(1) != 0 {
3165 tracing::warn!(conn_id, ret = ?strike_rsp.ret, "CMD6311 returned error");
3166 return Some(super::make_error_response(
3167 -1,
3168 "backend strike date request failed",
3169 ));
3170 }
3171
3172 let now_ts = std::time::SystemTime::now()
3174 .duration_since(std::time::UNIX_EPOCH)
3175 .map(|d| d.as_secs())
3176 .unwrap_or(0);
3177
3178 let date_list: Vec<futu_proto::qot_get_option_expiration_date::OptionExpirationDate> =
3180 if !strike_rsp.strike_dates.is_empty() {
3181 strike_rsp
3182 .strike_dates
3183 .iter()
3184 .map(|item| {
3185 let ts = item.strike_date.unwrap_or(0) as u64;
3186 let strike_time_str = timestamp_to_date_str(ts);
3187 let left_day = item.left_day.unwrap_or(0);
3188 let cycle = item.expiration.map(backend_expiration_to_api);
3189
3190 futu_proto::qot_get_option_expiration_date::OptionExpirationDate {
3191 strike_time: Some(strike_time_str),
3192 strike_timestamp: Some(ts as f64),
3193 option_expiry_date_distance: left_day,
3194 cycle,
3195 }
3196 })
3197 .collect::<Vec<_>>()
3198 } else {
3199 strike_rsp
3200 .strike_date_list
3201 .iter()
3202 .enumerate()
3203 .map(|(i, &ts)| {
3204 let strike_time_str = timestamp_to_date_str(ts as u64);
3205 let left_day = strike_rsp
3206 .left_day
3207 .get(i)
3208 .copied()
3209 .unwrap_or((((ts as i64) - now_ts as i64) / 86400) as i32);
3210 let cycle = strike_rsp
3211 .expiration_cycle
3212 .get(i)
3213 .map(|&e| backend_expiration_to_api(e));
3214
3215 futu_proto::qot_get_option_expiration_date::OptionExpirationDate {
3216 strike_time: Some(strike_time_str),
3217 strike_timestamp: Some(ts as f64),
3218 option_expiry_date_distance: left_day,
3219 cycle,
3220 }
3221 })
3222 .collect()
3223 };
3224
3225 tracing::debug!(
3226 conn_id,
3227 count = date_list.len(),
3228 "GetOptionExpirationDate returning"
3229 );
3230
3231 let resp = futu_proto::qot_get_option_expiration_date::Response {
3232 ret_type: 0,
3233 ret_msg: None,
3234 err_code: None,
3235 s2c: Some(futu_proto::qot_get_option_expiration_date::S2c { date_list }),
3236 };
3237 Some(prost::Message::encode_to_vec(&resp))
3238 }
3239}
3240
3241struct RequestHistoryKLQuotaHandler;
3243
3244#[async_trait]
3245impl RequestHandler for RequestHistoryKLQuotaHandler {
3246 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3247 let _req: futu_proto::qot_request_history_kl_quota::Request =
3249 prost::Message::decode(request.body.as_ref()).ok()?;
3250
3251 tracing::debug!(conn_id, "RequestHistoryKLQuota: returning unlimited quota");
3252
3253 let resp = futu_proto::qot_request_history_kl_quota::Response {
3255 ret_type: 0,
3256 ret_msg: None,
3257 err_code: None,
3258 s2c: Some(futu_proto::qot_request_history_kl_quota::S2c {
3259 used_quota: 0,
3260 remain_quota: 10000,
3261 detail_list: Vec::new(),
3262 }),
3263 };
3264 Some(prost::Message::encode_to_vec(&resp))
3265 }
3266}
3267
3268struct RequestRehabHandler {
3270 backend: crate::bridge::SharedBackend,
3271 static_cache: Arc<StaticDataCache>,
3272}
3273
3274#[async_trait]
3275impl RequestHandler for RequestRehabHandler {
3276 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3277 let req: futu_proto::qot_request_rehab::Request =
3278 prost::Message::decode(request.body.as_ref()).ok()?;
3279 let c2s = &req.c2s;
3280
3281 let backend = match super::load_backend(&self.backend) {
3282 Some(b) => b,
3283 None => {
3284 tracing::warn!(conn_id, "RequestRehab: no backend connection");
3285 return Some(super::make_error_response(-1, "no backend connection"));
3286 }
3287 };
3288
3289 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
3290 let stock_id = match self.static_cache.get_security_info(&sec_key) {
3291 Some(info) if info.stock_id > 0 => info.stock_id,
3292 _ => {
3293 tracing::warn!(conn_id, sec_key, "RequestRehab: stock_id not found");
3294 return Some(super::make_error_response(
3295 -1,
3296 "security not found in cache",
3297 ));
3298 }
3299 };
3300
3301 let exright_req = futu_backend::proto_internal::ft_cmd6811::ExrightFactorReq {
3302 stock_id: Some(stock_id),
3303 sequence: Some(0),
3304 };
3305 let body = prost::Message::encode_to_vec(&exright_req);
3306
3307 tracing::debug!(conn_id, stock_id, "RequestRehab: sending CMD6811");
3308
3309 let resp_frame = match backend.request(6811, body).await {
3310 Ok(f) => f,
3311 Err(e) => {
3312 tracing::error!(conn_id, error = %e, "RequestRehab CMD6811 request failed");
3313 return Some(super::make_error_response(-1, "backend request failed"));
3314 }
3315 };
3316
3317 let exright_rsp: futu_backend::proto_internal::ft_cmd6811::ExrightFactorRsp =
3318 match prost::Message::decode(resp_frame.body.as_ref()) {
3319 Ok(r) => r,
3320 Err(e) => {
3321 tracing::error!(conn_id, error = %e, "RequestRehab CMD6811 decode failed");
3322 return Some(super::make_error_response(
3323 -1,
3324 "backend response decode failed",
3325 ));
3326 }
3327 };
3328
3329 let result = exright_rsp.result.unwrap_or(-1);
3330 if result != 0 && result != 1 {
3331 tracing::warn!(conn_id, result, "RequestRehab CMD6811 returned error");
3332 return Some(super::make_error_response(
3333 -1,
3334 "backend rehab request failed",
3335 ));
3336 }
3337
3338 let rehab_list: Vec<futu_proto::qot_common::Rehab> = exright_rsp
3339 .exs
3340 .iter()
3341 .map(|item| {
3342 let ex_ts = item.ex_time.unwrap_or(0);
3343 let time_str = timestamp_to_date_str(ex_ts as u64);
3344
3345 let mut flag: i64 = 0;
3346 if item.split_base.is_some() {
3347 flag |= 1;
3348 }
3349 if item.join_base.is_some() {
3350 flag |= 2;
3351 }
3352 if item.bonus_base.is_some() {
3353 flag |= 4;
3354 }
3355 if item.transfer_base.is_some() {
3356 flag |= 8;
3357 }
3358 if item.allot_base.is_some() {
3359 flag |= 16;
3360 }
3361 if item.add_base.is_some() {
3362 flag |= 32;
3363 }
3364 if item.dividend_amount.is_some() {
3365 flag |= 64;
3366 }
3367 if item.sp_dividend_amount.is_some() {
3368 flag |= 128;
3369 }
3370
3371 let divisor = 100_000.0;
3372
3373 futu_proto::qot_common::Rehab {
3374 time: time_str,
3375 company_act_flag: flag,
3376 fwd_factor_a: item.origin_fwd_a.unwrap_or(100_000.0) / divisor,
3377 fwd_factor_b: item.origin_fwd_b.unwrap_or(0.0) / divisor,
3378 bwd_factor_a: item.origin_bwd_a.unwrap_or(100_000.0) / divisor,
3379 bwd_factor_b: item.origin_bwd_b.unwrap_or(0.0) / divisor,
3380 split_base: item.split_base.map(|v| v as i32),
3381 split_ert: item.split_ert.map(|v| v as i32),
3382 join_base: item.join_base.map(|v| v as i32),
3383 join_ert: item.join_ert.map(|v| v as i32),
3384 bonus_base: item.bonus_base.map(|v| v as i32),
3385 bonus_ert: item.bonus_ert.map(|v| v as i32),
3386 transfer_base: item.transfer_base.map(|v| v as i32),
3387 transfer_ert: item.transfer_ert.map(|v| v as i32),
3388 allot_base: item.allot_base.map(|v| v as i32),
3389 allot_ert: item.allot_ert.map(|v| v as i32),
3390 allot_price: item.allot_price.map(|v| v as f64 / divisor),
3391 add_base: item.add_base.map(|v| v as i32),
3392 add_ert: item.add_ert.map(|v| v as i32),
3393 add_price: item.add_price.map(|v| v as f64 / divisor),
3394 dividend: item.dividend_amount.map(|v| v as f64 / divisor),
3395 sp_dividend: item.sp_dividend_amount.map(|v| v as f64 / divisor),
3396 timestamp: Some(ex_ts as f64),
3397 spin_off_base: item.spin_off_base.map(|v| v as f64),
3398 spin_off_ert: item.spin_off_ert.map(|v| v as f64),
3399 }
3400 })
3401 .collect();
3402
3403 tracing::debug!(conn_id, count = rehab_list.len(), "RequestRehab returning");
3404
3405 let resp = futu_proto::qot_request_rehab::Response {
3406 ret_type: 0,
3407 ret_msg: None,
3408 err_code: None,
3409 s2c: Some(futu_proto::qot_request_rehab::S2c { rehab_list }),
3410 };
3411 Some(prost::Message::encode_to_vec(&resp))
3412 }
3413}
3414
3415struct GetWarrantHandler {
3417 backend: crate::bridge::SharedBackend,
3418 static_cache: Arc<StaticDataCache>,
3419}
3420
3421#[async_trait]
3422impl RequestHandler for GetWarrantHandler {
3423 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3424 let req: futu_proto::qot_get_warrant::Request =
3425 prost::Message::decode(request.body.as_ref()).ok()?;
3426 let c2s = &req.c2s;
3427
3428 let backend = match super::load_backend(&self.backend) {
3429 Some(b) => b,
3430 None => {
3431 tracing::warn!(conn_id, "GetWarrant: no backend connection");
3432 return Some(super::make_error_response(-1, "no backend connection"));
3433 }
3434 };
3435
3436 let owner_stock_id = if let Some(ref owner) = c2s.owner {
3438 let sec_key = format!("{}_{}", owner.market, owner.code);
3439 match self.static_cache.get_security_info(&sec_key) {
3440 Some(info) if info.stock_id > 0 => Some(info.stock_id),
3441 _ => None,
3442 }
3443 } else {
3444 None
3445 };
3446
3447 let backend_req = futu_backend::proto_internal::ftcmd6513::WarrantListReq {
3449 only_count: None,
3450 issuer_id: None,
3451 stock_owner: owner_stock_id,
3452 arry_warrant_type: c2s.type_list.clone(),
3453 cur_min: None,
3454 cur_max: None,
3455 street_min: None,
3456 street_max: None,
3457 vol_min: c2s.vol_min,
3458 vol_max: c2s.vol_max,
3459 maturity_date_min: None,
3460 maturity_date_max: None,
3461 strick_min: None,
3462 strick_max: None,
3463 conversion_min: None,
3464 conversion_max: None,
3465 ipop_min: None,
3466 ipop_max: None,
3467 premium_min: None,
3468 premium_max: None,
3469 recovery_min: None,
3470 recovery_max: None,
3471 implied_min: None,
3472 implied_max: None,
3473 leverage_ratio_min: None,
3474 leverage_ratio_max: None,
3475 lang_id: None,
3476 price_recovery_ratio_min: None,
3477 price_recovery_ratio_max: None,
3478 delta_min: None,
3479 delta_max: None,
3480 sort_col: c2s.sort_field,
3481 sort_ascend: if c2s.ascend { 1 } else { 0 },
3482 data_from: Some(c2s.begin),
3483 data_max_count: Some(c2s.num),
3484 status_filter: c2s.status,
3485 multiple_issuers: c2s.issuer_list.clone(),
3486 ipo_period: c2s.ipo_period,
3487 buy_vol_min: None,
3488 buy_vol_max: None,
3489 sell_vol_min: None,
3490 sell_vol_max: None,
3491 effective_leverage_min: None,
3492 effective_leverage_max: None,
3493 filter_no_trade_status: None,
3494 is_bmp: None,
3495 maturity_date_screens: Vec::new(),
3496 leverage_ratio_screens: Vec::new(),
3497 status_filter_screens: Vec::new(),
3498 market: None,
3499 };
3500 let body = prost::Message::encode_to_vec(&backend_req);
3501
3502 tracing::debug!(conn_id, "sending CMD6513 WarrantListReq");
3503
3504 let resp_frame = match backend.request(6513, body).await {
3505 Ok(f) => f,
3506 Err(e) => {
3507 tracing::error!(conn_id, error = %e, "CMD6513 request failed");
3508 return Some(super::make_error_response(-1, "backend request failed"));
3509 }
3510 };
3511
3512 let backend_rsp: futu_backend::proto_internal::ftcmd6513::WarrantListRsp =
3513 match prost::Message::decode(resp_frame.body.as_ref()) {
3514 Ok(r) => r,
3515 Err(e) => {
3516 tracing::error!(conn_id, error = %e, "CMD6513 decode failed");
3517 return Some(super::make_error_response(
3518 -1,
3519 "backend response decode failed",
3520 ));
3521 }
3522 };
3523
3524 if backend_rsp.result != 0 {
3525 tracing::warn!(
3526 conn_id,
3527 result = backend_rsp.result,
3528 "CMD6513 returned error"
3529 );
3530 return Some(super::make_error_response(
3531 -1,
3532 "backend warrant request failed",
3533 ));
3534 }
3535
3536 let all_count = backend_rsp.all_count as i32;
3537 let last_page = backend_rsp.if_last_page != 0;
3538
3539 let warrant_data_list: Vec<futu_proto::qot_get_warrant::WarrantData> = backend_rsp
3541 .arry_items
3542 .iter()
3543 .filter_map(|item| {
3544 let stock_id = item.stock_id?;
3546 let stock_key = self.static_cache.id_to_key.get(&stock_id)?;
3547 let stock_info = self.static_cache.get_security_info(stock_key.value())?;
3548 let stock = futu_proto::qot_common::Security {
3549 market: stock_info.market,
3550 code: stock_info.code.clone(),
3551 };
3552
3553 let owner_id = item.stock_owner?;
3555 let owner_key = self.static_cache.id_to_key.get(&owner_id)?;
3556 let owner_info = self.static_cache.get_security_info(owner_key.value())?;
3557 let owner = futu_proto::qot_common::Security {
3558 market: owner_info.market,
3559 code: owner_info.code.clone(),
3560 };
3561
3562 let warrant_type = item.warrant_type.unwrap_or(0);
3563
3564 let issuer = map_issuer_backend_to_api(item.issuer_id.unwrap_or(0));
3566
3567 let status = map_warrant_status_backend_to_api(item.status.unwrap_or(0));
3569
3570 let cur_price = item.current_price.unwrap_or(0) as f64 / 1000.0;
3572 let last_close_price = item.lastclose_price.unwrap_or(0) as f64 / 1000.0;
3573 let high_price = item.high_price.unwrap_or(0) as f64 / 1000.0;
3574 let low_price = item.low_price.unwrap_or(0) as f64 / 1000.0;
3575 let strike_price = item.strick_price.unwrap_or(0) as f64 / 1000.0;
3576 let bid_price = item.buy_price.unwrap_or(0) as f64 / 1000.0;
3577 let ask_price = item.sell_price.unwrap_or(0) as f64 / 1000.0;
3578 let recovery_price_val = item.recovery_price.unwrap_or(0) as f64 / 1000.0;
3579 let break_even_point = item.break_even_point.unwrap_or(0) as f64 / 1000.0;
3580
3581 let price_change_val = cur_price - last_close_price;
3583 let change_rate = if last_close_price.abs() > 1e-6 {
3584 price_change_val / last_close_price * 100.0
3585 } else {
3586 0.0
3587 };
3588
3589 let conversion_ratio = item.conversion_ratio.unwrap_or(0) as f64 / 1000.0;
3591 let conversion_price = conversion_ratio * cur_price;
3592 let street_rate = item.street_rate.unwrap_or(0) as f64 / 1000.0;
3593 let premium = item.premium.unwrap_or(0) as f64 / 1000.0;
3594 let leverage = item.leverage.unwrap_or(0) as f64 / 1000.0;
3595 let effective_leverage = item.effective_leverage.unwrap_or(0) as f64 / 1000.0;
3596 let ipop = item.ipop.unwrap_or(0) as f64 / 1000.0;
3597 let amplitude = item.amplitude.unwrap_or(0) as f64 / 1000.0;
3598 let turnover = item.turnover.unwrap_or(0) as f64 / 1000.0;
3599 let score = item.fx_score.unwrap_or(0) as f64 / 1000.0;
3600
3601 let volume = item.volume.unwrap_or(0) as i64;
3603 let bid_vol = item.buy_vol.unwrap_or(0) as i64;
3604 let ask_vol = item.sell_vol.unwrap_or(0) as i64;
3605 let street_vol = item.street_vol.unwrap_or(0) as i64;
3606 let issue_size = item.issue_size.unwrap_or(0) as i64;
3607 let lot_size = item.lot_size.unwrap_or(0) as i32;
3608
3609 let maturity_time = item.maturity_date.map(format_timestamp).unwrap_or_default();
3611 let maturity_timestamp = item.maturity_date.map(|ts| ts as f64);
3612 let list_time = item.ipo_time.map(format_timestamp).unwrap_or_default();
3613 let list_timestamp = item.ipo_time.map(|ts| ts as f64);
3614 let last_trade_time = item
3615 .last_trade_date
3616 .map(format_timestamp)
3617 .unwrap_or_default();
3618 let last_trade_timestamp = item.last_trade_date.map(|ts| ts as f64);
3619
3620 let name = stock_info.name.clone();
3622
3623 let (delta, implied_volatility) = if warrant_type == 1 || warrant_type == 2 {
3625 (
3627 Some(item.delta.unwrap_or(0) as f64 / 1000.0),
3628 Some(item.implied_volatility.unwrap_or(0) as f64 / 1000.0),
3629 )
3630 } else {
3631 (None, None)
3632 };
3633
3634 let (recovery_price, price_recovery_ratio) =
3635 if warrant_type == 3 || warrant_type == 4 {
3636 (
3638 Some(recovery_price_val),
3639 Some(item.price_recovery_ratio.unwrap_or(0) as f64 / 1000.0),
3640 )
3641 } else {
3642 (None, None)
3643 };
3644
3645 let (upper_strike_price, lower_strike_price, in_line_price_status) =
3647 if warrant_type == 5 {
3648 let upper = item.upper_strike_price.unwrap_or(0) as f64 / 1_000_000_000.0;
3650 let lower = item.lower_strike_price.unwrap_or(0) as f64 / 1_000_000_000.0;
3651 let price_status = if item.iw_price_status.unwrap_or(0) == 0 {
3653 1_i32
3654 } else {
3655 2_i32
3656 };
3657 (Some(upper), Some(lower), Some(price_status))
3658 } else {
3659 (None, None, None)
3660 };
3661
3662 Some(futu_proto::qot_get_warrant::WarrantData {
3663 stock,
3664 owner,
3665 r#type: warrant_type,
3666 issuer,
3667 maturity_time,
3668 maturity_timestamp,
3669 list_time,
3670 list_timestamp,
3671 last_trade_time,
3672 last_trade_timestamp,
3673 recovery_price,
3674 conversion_ratio,
3675 lot_size,
3676 strike_price,
3677 last_close_price,
3678 name,
3679 cur_price,
3680 price_change_val,
3681 change_rate,
3682 status,
3683 bid_price,
3684 ask_price,
3685 bid_vol,
3686 ask_vol,
3687 volume,
3688 turnover,
3689 score,
3690 premium,
3691 break_even_point,
3692 leverage,
3693 ipop,
3694 price_recovery_ratio,
3695 conversion_price,
3696 street_rate,
3697 street_vol,
3698 amplitude,
3699 issue_size,
3700 high_price,
3701 low_price,
3702 implied_volatility,
3703 delta,
3704 effective_leverage,
3705 upper_strike_price,
3706 lower_strike_price,
3707 in_line_price_status,
3708 })
3709 })
3710 .collect();
3711
3712 tracing::debug!(
3713 conn_id,
3714 all_count,
3715 last_page,
3716 mapped_count = warrant_data_list.len(),
3717 "GetWarrant: mapped backend warrant items"
3718 );
3719
3720 let resp = futu_proto::qot_get_warrant::Response {
3721 ret_type: 0,
3722 ret_msg: None,
3723 err_code: None,
3724 s2c: Some(futu_proto::qot_get_warrant::S2c {
3725 last_page,
3726 all_count,
3727 warrant_data_list,
3728 }),
3729 };
3730 Some(prost::Message::encode_to_vec(&resp))
3731 }
3732}
3733
3734fn map_issuer_backend_to_api(backend_issuer: u32) -> i32 {
3737 match backend_issuer {
3738 1 => 19, 2 => 12, 3 => 2, 4 => 3, 5 => 4, 6 => 13, 7 => 14, 8 => 5, 9 => 6, 10 => 7, 11 => 8, 12 => 22, 13 => 9, 15 => 15, 16 => 16, 17 => 17, 18 => 18, 19 => 10, 20 => 1, 21 => 11, 22 => 20, 23 => 21, 24 => 23, 25 => 24, 26 => 25, 27 => 26, 28 => 27, 29 => 28, _ => 0, }
3769}
3770
3771fn map_warrant_status_backend_to_api(backend_status: i32) -> i32 {
3773 match backend_status {
3774 0 => 1, 1 => 2, 2 => 3, 3 => 4, _ => 0, }
3780}
3781
3782struct GetCapitalFlowHandler {
3784 backend: crate::bridge::SharedBackend,
3785 static_cache: Arc<StaticDataCache>,
3786}
3787
3788#[async_trait]
3789impl RequestHandler for GetCapitalFlowHandler {
3790 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
3791 let req: futu_proto::qot_get_capital_flow::Request =
3792 prost::Message::decode(request.body.as_ref()).ok()?;
3793 let c2s = &req.c2s;
3794
3795 let backend = match super::load_backend(&self.backend) {
3796 Some(b) => b,
3797 None => {
3798 tracing::warn!(conn_id, "GetCapitalFlow: no backend connection");
3799 return Some(super::make_error_response(-1, "no backend connection"));
3800 }
3801 };
3802
3803 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
3805 let stock_id = match self.static_cache.get_security_info(&sec_key) {
3806 Some(info) if info.stock_id > 0 => info.stock_id,
3807 _ => {
3808 tracing::warn!(conn_id, sec_key, "GetCapitalFlow: stock_id not found");
3809 return Some(super::make_error_response(
3810 -1,
3811 "security not found in cache",
3812 ));
3813 }
3814 };
3815
3816 let period_type = c2s.period_type.unwrap_or(1); let (flow_item_list, last_valid_time, last_valid_ts) = if period_type <= 1 {
3820 let backend_req = futu_backend::proto_internal::cash_flow_cs::RealFlowTrendReq {
3822 stock_id: Some(stock_id),
3823 req_section: Some(0), };
3825 let body = prost::Message::encode_to_vec(&backend_req);
3826
3827 tracing::debug!(conn_id, stock_id, "sending CMD6694 RealFlowTrendReq");
3828
3829 let resp_frame = match backend.request(6694, body).await {
3830 Ok(f) => f,
3831 Err(e) => {
3832 tracing::error!(conn_id, error = %e, "CMD6694 request failed");
3833 return Some(super::make_error_response(-1, "backend request failed"));
3834 }
3835 };
3836
3837 let backend_rsp: futu_backend::proto_internal::cash_flow_cs::RealFlowTrendRsp =
3838 match prost::Message::decode(resp_frame.body.as_ref()) {
3839 Ok(r) => r,
3840 Err(e) => {
3841 tracing::error!(conn_id, error = %e, "CMD6694 decode failed");
3842 return Some(super::make_error_response(
3843 -1,
3844 "backend response decode failed",
3845 ));
3846 }
3847 };
3848
3849 if let Some(err) = backend_rsp.error_code {
3850 if err != 0 {
3851 tracing::warn!(conn_id, err, "CMD6694 returned error");
3852 return Some(super::make_error_response(
3853 -1,
3854 "backend capital flow request failed",
3855 ));
3856 }
3857 }
3858
3859 let items: Vec<futu_proto::qot_get_capital_flow::CapitalFlowItem> = backend_rsp
3860 .section_list
3861 .iter()
3862 .flat_map(|section| {
3863 section.point_list.iter().filter_map(|pt| {
3864 if pt.is_empty.unwrap_or(0) != 0 {
3866 return None;
3867 }
3868 Some(futu_proto::qot_get_capital_flow::CapitalFlowItem {
3869 in_flow: pt.total_net_in.unwrap_or(0) as f64 / 1000.0,
3870 time: pt.time.map(timestamp_to_datetime_str),
3871 timestamp: pt.time.map(|t| t as f64),
3872 main_in_flow: None, super_in_flow: pt.super_net_in.map(|v| v as f64 / 1000.0),
3874 big_in_flow: pt.big_net_in.map(|v| v as f64 / 1000.0),
3875 mid_in_flow: pt.mid_net_in.map(|v| v as f64 / 1000.0),
3876 sml_in_flow: pt.sml_net_in.map(|v| v as f64 / 1000.0),
3877 })
3878 })
3879 })
3880 .collect();
3881
3882 let last_valid_ts = backend_rsp.update_time.map(|t| t as f64);
3883 let last_valid_time = backend_rsp.update_time.map(timestamp_to_datetime_str);
3884 (items, last_valid_time, last_valid_ts)
3885 } else {
3886 let nn_period = match period_type {
3889 2 => 1u32, 3 => 2, 4 => 3, _ => 1, };
3894
3895 let begin_time = c2s.begin_time.as_deref().and_then(parse_date_to_timestamp);
3897 let end_time = c2s.end_time.as_deref().and_then(parse_date_to_timestamp);
3898
3899 if let (Some(bt), Some(et)) = (begin_time, end_time) {
3901 if bt > et {
3902 let resp = futu_proto::qot_get_capital_flow::Response {
3903 ret_type: 0,
3904 ret_msg: None,
3905 err_code: None,
3906 s2c: Some(futu_proto::qot_get_capital_flow::S2c {
3907 flow_item_list: vec![],
3908 last_valid_time: None,
3909 last_valid_timestamp: None,
3910 }),
3911 };
3912 return Some(prost::Message::encode_to_vec(&resp));
3913 }
3914 }
3915
3916 let backend_req = futu_backend::proto_internal::cash_flow_cs::HistoryCashFlowReq {
3917 stock_id: Some(stock_id),
3918 req_period_type: Some(nn_period),
3919 base_time: end_time.map(|t| t as u64),
3920 count: Some(200), need_detail: Some(0),
3922 analysis_type: Some(0),
3923 };
3924 let body = prost::Message::encode_to_vec(&backend_req);
3925
3926 tracing::debug!(
3927 conn_id,
3928 stock_id,
3929 nn_period,
3930 "sending CMD6695 HistoryCashFlowReq"
3931 );
3932
3933 let resp_frame = match backend.request(6695, body).await {
3934 Ok(f) => f,
3935 Err(e) => {
3936 tracing::error!(conn_id, error = %e, "CMD6695 request failed");
3937 return Some(super::make_error_response(-1, "backend request failed"));
3938 }
3939 };
3940
3941 let backend_rsp: futu_backend::proto_internal::cash_flow_cs::HistoryCashFlowRsp =
3942 match prost::Message::decode(resp_frame.body.as_ref()) {
3943 Ok(r) => r,
3944 Err(e) => {
3945 tracing::error!(conn_id, error = %e, "CMD6695 decode failed");
3946 return Some(super::make_error_response(
3947 -1,
3948 "backend response decode failed",
3949 ));
3950 }
3951 };
3952
3953 if let Some(err) = backend_rsp.error_code {
3954 if err != 0 {
3955 tracing::warn!(conn_id, err, "CMD6695 returned error");
3956 return Some(super::make_error_response(
3957 -1,
3958 "backend history capital flow request failed",
3959 ));
3960 }
3961 }
3962
3963 let items: Vec<futu_proto::qot_get_capital_flow::CapitalFlowItem> = backend_rsp
3964 .point_list
3965 .iter()
3966 .filter_map(|pt| {
3967 if pt.is_empty.unwrap_or(0) != 0 {
3968 return None;
3969 }
3970 Some(futu_proto::qot_get_capital_flow::CapitalFlowItem {
3971 in_flow: pt.total_net_in.unwrap_or(0) as f64 / 1000.0,
3972 time: pt.time.map(timestamp_to_datetime_str),
3973 timestamp: pt.time.map(|t| t as f64),
3974 main_in_flow: pt.main_net_in.map(|v| v as f64 / 1000.0),
3975 super_in_flow: pt.super_net_in.map(|v| v as f64 / 1000.0),
3976 big_in_flow: pt.big_net_in.map(|v| v as f64 / 1000.0),
3977 mid_in_flow: pt.mid_net_in.map(|v| v as f64 / 1000.0),
3978 sml_in_flow: pt.sml_net_in.map(|v| v as f64 / 1000.0),
3979 })
3980 })
3981 .collect();
3982
3983 (items, None, None::<f64>)
3984 };
3985
3986 tracing::debug!(
3987 conn_id,
3988 count = flow_item_list.len(),
3989 "GetCapitalFlow: returning flow items"
3990 );
3991
3992 let resp = futu_proto::qot_get_capital_flow::Response {
3993 ret_type: 0,
3994 ret_msg: None,
3995 err_code: None,
3996 s2c: Some(futu_proto::qot_get_capital_flow::S2c {
3997 flow_item_list,
3998 last_valid_time,
3999 last_valid_timestamp: last_valid_ts,
4000 }),
4001 };
4002 Some(prost::Message::encode_to_vec(&resp))
4003 }
4004}
4005
4006struct GetCapitalDistributionHandler {
4008 backend: crate::bridge::SharedBackend,
4009 static_cache: Arc<StaticDataCache>,
4010}
4011
4012#[async_trait]
4013impl RequestHandler for GetCapitalDistributionHandler {
4014 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4015 let req: futu_proto::qot_get_capital_distribution::Request =
4016 prost::Message::decode(request.body.as_ref()).ok()?;
4017 let c2s = &req.c2s;
4018
4019 let backend = match super::load_backend(&self.backend) {
4020 Some(b) => b,
4021 None => {
4022 tracing::warn!(conn_id, "GetCapitalDistribution: no backend connection");
4023 return Some(super::make_error_response(-1, "no backend connection"));
4024 }
4025 };
4026
4027 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
4029 let stock_id = match self.static_cache.get_security_info(&sec_key) {
4030 Some(info) if info.stock_id > 0 => info.stock_id,
4031 _ => {
4032 tracing::warn!(
4033 conn_id,
4034 sec_key,
4035 "GetCapitalDistribution: stock_id not found"
4036 );
4037 return Some(super::make_error_response(
4038 -1,
4039 "security not found in cache",
4040 ));
4041 }
4042 };
4043
4044 let backend_req = futu_backend::proto_internal::cash_flow_cs::RealDistributionReq {
4046 stock_id: Some(stock_id),
4047 };
4048 let body = prost::Message::encode_to_vec(&backend_req);
4049
4050 tracing::debug!(conn_id, stock_id, "sending CMD6693 RealDistributionReq");
4051
4052 let resp_frame = match backend.request(6693, body).await {
4053 Ok(f) => f,
4054 Err(e) => {
4055 tracing::error!(conn_id, error = %e, "CMD6693 request failed");
4056 return Some(super::make_error_response(-1, "backend request failed"));
4057 }
4058 };
4059
4060 let backend_rsp: futu_backend::proto_internal::cash_flow_cs::RealDistributionRsp =
4061 match prost::Message::decode(resp_frame.body.as_ref()) {
4062 Ok(r) => r,
4063 Err(e) => {
4064 tracing::error!(conn_id, error = %e, "CMD6693 decode failed");
4065 return Some(super::make_error_response(
4066 -1,
4067 "backend response decode failed",
4068 ));
4069 }
4070 };
4071
4072 if let Some(err) = backend_rsp.error_code {
4073 if err != 0 {
4074 tracing::warn!(conn_id, err, "CMD6693 returned error");
4075 return Some(super::make_error_response(
4076 -1,
4077 "backend capital distribution request failed",
4078 ));
4079 }
4080 }
4081
4082 let mut in_super = 0.0_f64;
4086 let mut in_big = 0.0_f64;
4087 let mut in_mid = 0.0_f64;
4088 let mut in_small = 0.0_f64;
4089 let mut out_super = 0.0_f64;
4090 let mut out_big = 0.0_f64;
4091 let mut out_mid = 0.0_f64;
4092 let mut out_small = 0.0_f64;
4093
4094 for item in &backend_rsp.items {
4095 let flow_in = item.r#in.unwrap_or(0) as f64 / 1000.0;
4096 let flow_out = item.out.unwrap_or(0) as f64 / 1000.0;
4097 match item.order_type.unwrap_or(0) {
4098 1 => {
4099 in_small = flow_in;
4100 out_small = flow_out;
4101 }
4102 2 => {
4103 in_mid = flow_in;
4104 out_mid = flow_out;
4105 }
4106 3 => {
4107 in_big = flow_in;
4108 out_big = flow_out;
4109 }
4110 4 => {
4111 in_super = flow_in;
4112 out_super = flow_out;
4113 }
4114 _ => {}
4115 }
4116 }
4117
4118 let update_timestamp = backend_rsp.update_time.map(|t| t as f64);
4119
4120 tracing::debug!(
4121 conn_id,
4122 items = backend_rsp.items.len(),
4123 "GetCapitalDistribution: returning distribution"
4124 );
4125
4126 let resp = futu_proto::qot_get_capital_distribution::Response {
4127 ret_type: 0,
4128 ret_msg: None,
4129 err_code: None,
4130 s2c: Some(futu_proto::qot_get_capital_distribution::S2c {
4131 capital_in_super: Some(in_super),
4132 capital_in_big: in_big,
4133 capital_in_mid: in_mid,
4134 capital_in_small: in_small,
4135 capital_out_super: Some(out_super),
4136 capital_out_big: out_big,
4137 capital_out_mid: out_mid,
4138 capital_out_small: out_small,
4139 update_time: backend_rsp.update_time.map(timestamp_to_datetime_str),
4140 update_timestamp,
4141 }),
4142 };
4143 Some(prost::Message::encode_to_vec(&resp))
4144 }
4145}
4146
4147struct GetHistoryKLHandler {
4149 backend: crate::bridge::SharedBackend,
4150 static_cache: Arc<StaticDataCache>,
4151}
4152
4153#[async_trait]
4154impl RequestHandler for GetHistoryKLHandler {
4155 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4156 let req: futu_proto::qot_request_history_kl::Request =
4158 prost::Message::decode(request.body.as_ref()).ok()?;
4159 let c2s = &req.c2s;
4160
4161 let backend = match super::load_backend(&self.backend) {
4162 Some(b) => b,
4163 None => {
4164 tracing::warn!(conn_id, "GetHistoryKL: no backend connection");
4165 return Some(super::make_error_response(-1, "no backend connection"));
4166 }
4167 };
4168
4169 let sec_key = format!("{}_{}", c2s.security.market, c2s.security.code);
4170 let stock_id = match self.static_cache.get_security_info(&sec_key) {
4171 Some(info) if info.stock_id > 0 => info.stock_id,
4172 _ => {
4173 tracing::warn!(conn_id, sec_key, "GetHistoryKL: stock_id not found");
4174 return Some(super::make_error_response(
4175 -1,
4176 "security not found in cache",
4177 ));
4178 }
4179 };
4180
4181 let backend_kl_type = match ftapi_kl_type_to_backend(c2s.kl_type) {
4182 Some(v) => v,
4183 None => {
4184 tracing::warn!(
4185 conn_id,
4186 kl_type = c2s.kl_type,
4187 "GetHistoryKL: invalid kl_type"
4188 );
4189 return Some(super::make_error_response(-1, "invalid kl_type"));
4190 }
4191 };
4192
4193 let exright_type = c2s.rehab_type as u32;
4194 let qot_market = c2s.security.market;
4195 let begin_ts = date_str_to_timestamp(&c2s.begin_time, qot_market).unwrap_or(0);
4196 let end_ts = if c2s.end_time.is_empty() {
4197 u64::MAX
4198 } else {
4199 date_str_to_timestamp(&c2s.end_time, qot_market)
4200 .map(|t| t + 86399)
4201 .unwrap_or(u64::MAX)
4202 };
4203
4204 let max_kl_num = c2s.max_ack_kl_num.unwrap_or(0) as usize;
4206
4207 let kline_req = futu_backend::proto_internal::ft_cmd_kline::KlineReq {
4208 security_id: Some(stock_id),
4209 kline_type: Some(backend_kl_type),
4210 exright_type: Some(exright_type),
4211 data_set_type: Some(0),
4212 data_range_type: Some(1), begin_time: Some(begin_ts),
4214 end_time: Some(end_ts),
4215 item_count: None,
4216 end_time_offset: None,
4217 };
4218
4219 let body = prost::Message::encode_to_vec(&kline_req);
4220
4221 tracing::debug!(
4222 conn_id,
4223 stock_id,
4224 kl_type = backend_kl_type,
4225 exright = exright_type,
4226 "GetHistoryKL: sending CMD6161 KlineReq"
4227 );
4228
4229 let resp_frame = match backend.request(6161, body).await {
4230 Ok(f) => f,
4231 Err(e) => {
4232 tracing::error!(conn_id, error = %e, "GetHistoryKL: CMD6161 request failed");
4233 return Some(super::make_error_response(-1, "backend request failed"));
4234 }
4235 };
4236
4237 let kline_rsp: futu_backend::proto_internal::ft_cmd_kline::KlineRsp =
4238 match prost::Message::decode(resp_frame.body.as_ref()) {
4239 Ok(r) => r,
4240 Err(e) => {
4241 tracing::error!(conn_id, error = %e, "GetHistoryKL: CMD6161 decode failed");
4242 return Some(super::make_error_response(
4243 -1,
4244 "backend response decode failed",
4245 ));
4246 }
4247 };
4248
4249 let result = kline_rsp.result.unwrap_or(-1);
4250 if result != 0 {
4251 tracing::warn!(conn_id, result, "GetHistoryKL: CMD6161 returned error");
4252 return Some(super::make_error_response(
4253 -1,
4254 "backend kline request failed",
4255 ));
4256 }
4257
4258 let mut kl_list: Vec<futu_proto::qot_common::KLine> = kline_rsp
4259 .kline_item_list
4260 .iter()
4261 .map(kline_item_to_ftapi)
4262 .collect();
4263 if max_kl_num > 0 && kl_list.len() > max_kl_num {
4264 kl_list.truncate(max_kl_num);
4265 }
4266
4267 tracing::debug!(
4268 conn_id,
4269 count = kl_list.len(),
4270 "GetHistoryKL returning klines"
4271 );
4272
4273 let resp = futu_proto::qot_request_history_kl::Response {
4275 ret_type: 0,
4276 ret_msg: None,
4277 err_code: None,
4278 s2c: Some(futu_proto::qot_request_history_kl::S2c {
4279 security: c2s.security.clone(),
4280 name: None,
4281 kl_list,
4282 next_req_key: None,
4283 }),
4284 };
4285 Some(prost::Message::encode_to_vec(&resp))
4286 }
4287}
4288
4289struct GetHistoryKLPointsHandler;
4293
4294#[async_trait]
4295impl RequestHandler for GetHistoryKLPointsHandler {
4296 async fn handle(&self, conn_id: u64, _request: &IncomingRequest) -> Option<Vec<u8>> {
4297 tracing::debug!(conn_id, "GetHistoryKLPoints: feature removed in v5.21");
4300 Some(super::make_error_response(
4301 -1,
4302 "GetHistoryKLPoints has been removed since v5.21. Please use RequestHistoryKL instead.",
4303 ))
4304 }
4305}
4306
4307struct GetTradeDateHandler {
4309 backend: crate::bridge::SharedBackend,
4310}
4311
4312#[async_trait]
4313impl RequestHandler for GetTradeDateHandler {
4314 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4315 let req: futu_proto::qot_request_trade_date::Request =
4317 prost::Message::decode(request.body.as_ref()).ok()?;
4318 let c2s = &req.c2s;
4319
4320 let backend = match super::load_backend(&self.backend) {
4321 Some(b) => b,
4322 None => {
4323 tracing::warn!(conn_id, "GetTradeDate: no backend connection");
4324 return Some(super::make_error_response(-1, "no backend connection"));
4325 }
4326 };
4327
4328 let backend_market = match ftapi_market_to_backend(c2s.market) {
4329 Some(m) => m,
4330 None => {
4331 tracing::warn!(
4332 conn_id,
4333 market = c2s.market,
4334 "GetTradeDate: unsupported market"
4335 );
4336 return Some(super::make_error_response(-1, "unsupported market"));
4337 }
4338 };
4339
4340 let begin_key = match date_str_to_yyyymmdd(&c2s.begin_time) {
4341 Some(v) => v,
4342 None => {
4343 tracing::warn!(conn_id, begin = %c2s.begin_time, "GetTradeDate: invalid begin_time");
4344 return Some(super::make_error_response(-1, "invalid begin_time"));
4345 }
4346 };
4347
4348 let end_key = match date_str_to_yyyymmdd(&c2s.end_time) {
4349 Some(v) => v,
4350 None => {
4351 tracing::warn!(conn_id, end = %c2s.end_time, "GetTradeDate: invalid end_time");
4352 return Some(super::make_error_response(-1, "invalid end_time"));
4353 }
4354 };
4355
4356 let range_req = futu_backend::proto_internal::market_trading_day::RangeTradingDayReq {
4357 begin_date_key: Some(begin_key),
4358 end_date_key: Some(end_key),
4359 market_id: Some(backend_market),
4360 };
4361
4362 let body = prost::Message::encode_to_vec(&range_req);
4363
4364 tracing::debug!(
4365 conn_id,
4366 backend_market,
4367 begin_key,
4368 end_key,
4369 "GetTradeDate: sending CMD6733 RangeTradingDayReq"
4370 );
4371
4372 let resp_frame = match backend.request(6733, body).await {
4373 Ok(f) => f,
4374 Err(e) => {
4375 tracing::error!(conn_id, error = %e, "GetTradeDate: CMD6733 request failed");
4376 return Some(super::make_error_response(-1, "backend request failed"));
4377 }
4378 };
4379
4380 let range_rsp: futu_backend::proto_internal::market_trading_day::RangeTradingDayRsp =
4381 match prost::Message::decode(resp_frame.body.as_ref()) {
4382 Ok(r) => r,
4383 Err(e) => {
4384 tracing::error!(conn_id, error = %e, "GetTradeDate: CMD6733 decode failed");
4385 return Some(super::make_error_response(
4386 -1,
4387 "backend response decode failed",
4388 ));
4389 }
4390 };
4391
4392 let code = range_rsp.code.unwrap_or(-1);
4393 if code != 0 {
4394 tracing::warn!(conn_id, code, "GetTradeDate: CMD6733 returned error");
4395 return Some(super::make_error_response(
4396 -1,
4397 "backend trade date request failed",
4398 ));
4399 }
4400
4401 let trade_date_list: Vec<futu_proto::qot_request_trade_date::TradeDate> = range_rsp
4402 .day_infos
4403 .iter()
4404 .map(|day| {
4405 let time_date = day.time_date.unwrap_or(0);
4406 let time_str = yyyymmdd_to_date_str(time_date);
4407 let trading_type = day.trading_type.unwrap_or(4);
4408 let trade_date_type = backend_trading_type_to_ftapi(trading_type);
4409 let ts = day.date_key.unwrap_or(0) as f64;
4410
4411 futu_proto::qot_request_trade_date::TradeDate {
4412 time: time_str,
4413 timestamp: Some(ts),
4414 trade_date_type: Some(trade_date_type),
4415 }
4416 })
4417 .collect();
4418
4419 tracing::debug!(
4420 conn_id,
4421 count = trade_date_list.len(),
4422 "GetTradeDate returning trade dates"
4423 );
4424
4425 let resp = futu_proto::qot_request_trade_date::Response {
4426 ret_type: 0,
4427 ret_msg: None,
4428 err_code: None,
4429 s2c: Some(futu_proto::qot_request_trade_date::S2c { trade_date_list }),
4430 };
4431 Some(prost::Message::encode_to_vec(&resp))
4432 }
4433}
4434
4435struct GetSuspendHandler {
4441 suspend_cache: futu_backend::suspend_data::SuspendCache,
4442 static_cache: Arc<StaticDataCache>,
4443}
4444
4445#[async_trait]
4446impl RequestHandler for GetSuspendHandler {
4447 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4448 let req: futu_proto::qot_get_suspend::Request =
4449 prost::Message::decode(request.body.as_ref()).ok()?;
4450
4451 let c2s = &req.c2s;
4452
4453 let security_suspend_list: Vec<futu_proto::qot_get_suspend::SecuritySuspend> = c2s
4461 .security_list
4462 .iter()
4463 .map(|sec| {
4464 let sec_key = format!("{}_{}", sec.market, sec.code);
4466 let stock_id = self
4467 .static_cache
4468 .get_security_info(&sec_key)
4469 .map(|info| info.stock_id)
4470 .unwrap_or(0);
4471
4472 let begin_ts = parse_api_time_str(&c2s.begin_time, sec.market).unwrap_or(0);
4474 let end_ts = parse_api_time_str(&c2s.end_time, sec.market).unwrap_or(u64::MAX);
4475
4476 let suspend_list = if stock_id > 0 {
4478 if let Some(timestamps) = self.suspend_cache.get(&stock_id) {
4479 let start_idx = timestamps.partition_point(|&t| t < begin_ts);
4481 let end_idx = timestamps.partition_point(|&t| t <= end_ts);
4482 timestamps[start_idx..end_idx]
4483 .iter()
4484 .map(|&ts| futu_proto::qot_get_suspend::Suspend {
4485 time: timestamp_to_datetime_str(ts),
4486 timestamp: Some(ts as f64),
4487 })
4488 .collect()
4489 } else {
4490 Vec::new()
4491 }
4492 } else {
4493 Vec::new()
4494 };
4495
4496 futu_proto::qot_get_suspend::SecuritySuspend {
4497 security: sec.clone(),
4498 suspend_list,
4499 }
4500 })
4501 .collect();
4502
4503 tracing::debug!(conn_id, count = security_suspend_list.len(), "GetSuspend");
4504
4505 let resp = futu_proto::qot_get_suspend::Response {
4506 ret_type: 0,
4507 ret_msg: None,
4508 err_code: None,
4509 s2c: Some(futu_proto::qot_get_suspend::S2c {
4510 security_suspend_list,
4511 }),
4512 };
4513 Some(prost::Message::encode_to_vec(&resp))
4514 }
4515}
4516
4517fn parse_api_time_str(s: &str, market: i32) -> Option<u64> {
4519 if s.len() >= 19 {
4521 let date_part = &s[..10];
4522 let time_part = &s[11..19];
4523 if let Some(day_ts) = date_str_to_timestamp(date_part, market) {
4524 let time_parts: Vec<&str> = time_part.split(':').collect();
4525 if time_parts.len() == 3 {
4526 let h: u64 = time_parts[0].parse().ok()?;
4527 let m: u64 = time_parts[1].parse().ok()?;
4528 let sec: u64 = time_parts[2].parse().ok()?;
4529 return Some(day_ts + h * 3600 + m * 60 + sec);
4530 }
4531 }
4532 }
4533 date_str_to_timestamp(s, market)
4535}
4536
4537struct GetBrokerHandler {
4539 cache: Arc<QotCache>,
4540 static_cache: Arc<StaticDataCache>,
4541}
4542
4543#[async_trait]
4544impl RequestHandler for GetBrokerHandler {
4545 async fn handle(&self, _conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4546 let req: futu_proto::qot_get_broker::Request =
4547 prost::Message::decode(request.body.as_ref()).ok()?;
4548 let sec = &req.c2s.security;
4549 let sec_key = qot_cache::make_key(sec.market, &sec.code);
4550
4551 let name = self
4552 .static_cache
4553 .get_security_info(&sec_key)
4554 .map(|info| info.name);
4555
4556 let cached = self.cache.get_broker(&sec_key);
4557 let (broker_ask_list, broker_bid_list) = match cached {
4558 Some(b) => {
4559 let to_proto =
4560 |items: &[qot_cache::CachedBrokerItem]| -> Vec<futu_proto::qot_common::Broker> {
4561 items
4562 .iter()
4563 .map(|item| futu_proto::qot_common::Broker {
4564 id: item.id,
4565 name: item.name.clone(),
4566 pos: item.pos,
4567 order_id: None,
4568 volume: None,
4569 })
4570 .collect()
4571 };
4572 (to_proto(&b.ask_list), to_proto(&b.bid_list))
4573 }
4574 None => (Vec::new(), Vec::new()),
4575 };
4576
4577 let resp = futu_proto::qot_get_broker::Response {
4578 ret_type: 0,
4579 ret_msg: None,
4580 err_code: None,
4581 s2c: Some(futu_proto::qot_get_broker::S2c {
4582 security: sec.clone(),
4583 name,
4584 broker_ask_list,
4585 broker_bid_list,
4586 }),
4587 };
4588 Some(prost::Message::encode_to_vec(&resp))
4589 }
4590}
4591
4592struct GetOptionChainHandler {
4594 backend: crate::bridge::SharedBackend,
4595 static_cache: Arc<StaticDataCache>,
4596}
4597
4598#[async_trait]
4599impl RequestHandler for GetOptionChainHandler {
4600 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4601 let req: futu_proto::qot_get_option_chain::Request =
4602 prost::Message::decode(request.body.as_ref()).ok()?;
4603 let c2s = &req.c2s;
4604
4605 let backend = match super::load_backend(&self.backend) {
4606 Some(b) => b,
4607 None => {
4608 tracing::warn!(conn_id, "GetOptionChain: no backend connection");
4609 return Some(super::make_error_response(-1, "no backend connection"));
4610 }
4611 };
4612
4613 let sec_key = format!("{}_{}", c2s.owner.market, c2s.owner.code);
4615 let stock_id = match self.static_cache.get_security_info(&sec_key) {
4616 Some(info) if info.stock_id > 0 => info.stock_id,
4617 _ => {
4618 tracing::warn!(conn_id, sec_key, "GetOptionChain: stock_id not found");
4619 return Some(super::make_error_response(
4620 -1,
4621 "security not found in cache",
4622 ));
4623 }
4624 };
4625
4626 let strike_req = futu_backend::proto_internal::ftcmd_option_chain::StrikeDateReq {
4628 stock_id: Some(stock_id),
4629 };
4630 let body = prost::Message::encode_to_vec(&strike_req);
4631
4632 tracing::debug!(conn_id, stock_id, "GetOptionChain: sending CMD6311");
4633
4634 let resp_frame = match backend.request(6311, body).await {
4635 Ok(f) => f,
4636 Err(e) => {
4637 tracing::error!(conn_id, error = %e, "GetOptionChain: CMD6311 request failed");
4638 return Some(super::make_error_response(-1, "backend request failed"));
4639 }
4640 };
4641
4642 let strike_rsp: futu_backend::proto_internal::ftcmd_option_chain::StrikeDateRsp =
4643 match prost::Message::decode(resp_frame.body.as_ref()) {
4644 Ok(r) => r,
4645 Err(e) => {
4646 tracing::error!(conn_id, error = %e, "GetOptionChain: CMD6311 decode failed");
4647 return Some(super::make_error_response(
4648 -1,
4649 "backend response decode failed",
4650 ));
4651 }
4652 };
4653
4654 if strike_rsp.ret.unwrap_or(1) != 0 {
4655 tracing::warn!(conn_id, ret = ?strike_rsp.ret, "GetOptionChain: CMD6311 error");
4656 return Some(super::make_error_response(
4657 -1,
4658 "backend strike date request failed",
4659 ));
4660 }
4661
4662 let qot_market = c2s.owner.market;
4664 let begin_ts = date_str_to_timestamp(&c2s.begin_time, qot_market).unwrap_or(0);
4665 let end_ts = if c2s.end_time.is_empty() {
4666 u64::MAX
4667 } else {
4668 date_str_to_timestamp(&c2s.end_time, qot_market)
4669 .map(|t| t + 86399)
4670 .unwrap_or(u64::MAX)
4671 };
4672
4673 let filtered_dates: Vec<u32> = strike_rsp
4674 .strike_date_list
4675 .iter()
4676 .copied()
4677 .filter(|&ts| {
4678 let ts64 = ts as u64;
4679 ts64 >= begin_ts && ts64 <= end_ts
4680 })
4681 .collect();
4682
4683 let option_type = c2s.r#type.unwrap_or(0) as u32; let mut option_chain_list: Vec<futu_proto::qot_get_option_chain::OptionChain> = Vec::new();
4687
4688 for &strike_date in &filtered_dates {
4689 let chain_req = futu_backend::proto_internal::ftcmd_option_chain::OptionChainReq {
4690 stock_id: Some(stock_id),
4691 strike_date: Some(strike_date),
4692 option_type: if option_type > 0 {
4693 Some(option_type)
4694 } else {
4695 Some(0)
4696 },
4697 sort_type: Some(2), sort_id: Some(1), from: Some(0),
4700 count: Some(200),
4701 };
4702 let body = prost::Message::encode_to_vec(&chain_req);
4703
4704 let resp_frame = match backend.request(6312, body).await {
4705 Ok(f) => f,
4706 Err(e) => {
4707 tracing::warn!(
4708 conn_id,
4709 error = %e,
4710 strike_date,
4711 "GetOptionChain: CMD6312 request failed"
4712 );
4713 continue;
4714 }
4715 };
4716
4717 let chain_rsp: futu_backend::proto_internal::ftcmd_option_chain::OptionChainRsp =
4718 match prost::Message::decode(resp_frame.body.as_ref()) {
4719 Ok(r) => r,
4720 Err(e) => {
4721 tracing::warn!(conn_id, error = %e, "GetOptionChain: CMD6312 decode failed");
4722 continue;
4723 }
4724 };
4725
4726 if chain_rsp.ret.unwrap_or(1) != 0 {
4727 continue;
4728 }
4729
4730 let strike_time_str = timestamp_to_date_str(strike_date as u64);
4731
4732 let options: Vec<futu_proto::qot_get_option_chain::OptionItem> = chain_rsp
4733 .option_chain
4734 .iter()
4735 .map(|item| {
4736 let call_info = item.call_option.as_ref().map(|opt| {
4737 backend_option_to_static_info(opt, c2s.owner.market, strike_date)
4738 });
4739 let put_info = item.put_option.as_ref().map(|opt| {
4740 backend_option_to_static_info(opt, c2s.owner.market, strike_date)
4741 });
4742 futu_proto::qot_get_option_chain::OptionItem {
4743 call: call_info,
4744 put: put_info,
4745 }
4746 })
4747 .collect();
4748
4749 option_chain_list.push(futu_proto::qot_get_option_chain::OptionChain {
4750 strike_time: strike_time_str,
4751 option: options,
4752 strike_timestamp: Some(strike_date as f64),
4753 });
4754 }
4755
4756 tracing::debug!(
4757 conn_id,
4758 chain_count = option_chain_list.len(),
4759 "GetOptionChain returning"
4760 );
4761
4762 let resp = futu_proto::qot_get_option_chain::Response {
4763 ret_type: 0,
4764 ret_msg: None,
4765 err_code: None,
4766 s2c: Some(futu_proto::qot_get_option_chain::S2c {
4767 option_chain: option_chain_list,
4768 }),
4769 };
4770 Some(prost::Message::encode_to_vec(&resp))
4771 }
4772}
4773
4774fn backend_option_to_static_info(
4776 opt: &futu_backend::proto_internal::ftcmd_option_chain::Option,
4777 market: i32,
4778 strike_date: u32,
4779) -> futu_proto::qot_common::SecurityStaticInfo {
4780 let code = opt
4781 .option_string_code
4782 .clone()
4783 .unwrap_or_else(|| opt.option_id.unwrap_or(0).to_string());
4784
4785 let strike_price_raw = opt.hp_strike_price.unwrap_or(0);
4786 let strike_price = strike_price_raw as f64 / 1_000_000_000.0;
4787
4788 let option_type = opt.option_type.unwrap_or(0);
4789 let ftapi_option_type = match option_type {
4791 1 => 1,
4792 2 => 2,
4793 _ => 0,
4794 };
4795
4796 let name = opt.option_name.clone().unwrap_or_default();
4797 let strike_time_str = timestamp_to_date_str(strike_date as u64);
4798
4799 futu_proto::qot_common::SecurityStaticInfo {
4800 basic: futu_proto::qot_common::SecurityStaticBasic {
4801 security: futu_proto::qot_common::Security {
4802 market,
4803 code: code.clone(),
4804 },
4805 id: opt.option_id.unwrap_or(0) as i64,
4806 lot_size: opt.contract_share_size.unwrap_or(100) as i32,
4807 sec_type: 8, name: name.clone(),
4809 list_time: String::new(),
4810 delisting: opt.delisting_flag.map(|f| f != 0),
4811 list_timestamp: None,
4812 exch_type: None,
4813 },
4814 warrant_ex_data: None,
4815 option_ex_data: Some(futu_proto::qot_common::OptionStaticExData {
4816 r#type: ftapi_option_type,
4817 owner: futu_proto::qot_common::Security {
4818 market,
4819 code: String::new(),
4820 },
4821 strike_time: strike_time_str,
4822 strike_price,
4823 suspend: opt.suspend_flag.unwrap_or(0) != 0,
4824 market: opt.market.clone().unwrap_or_default(),
4825 index_option_type: None,
4826 strike_timestamp: Some(strike_date as f64),
4827 expiration_cycle: None,
4828 option_standard_type: None,
4829 option_settlement_mode: None,
4830 }),
4831 future_ex_data: None,
4832 }
4833}
4834
4835struct GetReferenceHandler {
4842 backend: crate::bridge::SharedBackend,
4843 static_cache: Arc<StaticDataCache>,
4844}
4845
4846#[async_trait]
4847impl RequestHandler for GetReferenceHandler {
4848 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
4849 let req: futu_proto::qot_get_reference::Request =
4850 prost::Message::decode(request.body.as_ref()).ok()?;
4851
4852 let ref_type = req.c2s.reference_type;
4853 let security = &req.c2s.security;
4854 let key = futu_cache::qot_cache::make_key(security.market, &security.code);
4855
4856 tracing::debug!(
4857 conn_id,
4858 ref_type,
4859 code = %security.code,
4860 market = security.market,
4861 "GetReference"
4862 );
4863
4864 match ref_type {
4866 1 => {
4867 let stock_info = self.static_cache.get_security_info(&key);
4869 let stock_id = stock_info.map(|i| i.stock_id).unwrap_or(0);
4870
4871 let warrant_ids = if stock_id > 0 {
4872 self.static_cache.search_warrants_by_owner(stock_id)
4873 } else {
4874 Vec::new()
4875 };
4876
4877 let static_info_list: Vec<futu_proto::qot_common::SecurityStaticInfo> = warrant_ids
4878 .iter()
4879 .filter_map(|&wid| {
4880 let wkey = self.static_cache.id_to_key.get(&wid)?;
4881 let winfo = self.static_cache.get_security_info(wkey.value())?;
4882 if winfo.market == 0 {
4884 return None;
4885 }
4886 Some(make_static_info(
4887 futu_proto::qot_common::Security {
4888 market: winfo.market,
4889 code: winfo.code.clone(),
4890 },
4891 &winfo,
4892 ))
4893 })
4894 .collect();
4895
4896 tracing::debug!(
4897 conn_id,
4898 count = static_info_list.len(),
4899 "GetReference(Warrant) returning securities"
4900 );
4901
4902 let resp = futu_proto::qot_get_reference::Response {
4903 ret_type: 0,
4904 ret_msg: None,
4905 err_code: None,
4906 s2c: Some(futu_proto::qot_get_reference::S2c { static_info_list }),
4907 };
4908 Some(prost::Message::encode_to_vec(&resp))
4909 }
4910 2 => {
4911 let stock_info = self.static_cache.get_security_info(&key);
4914 let (stock_id, sec_type) = stock_info
4915 .map(|i| (i.stock_id, i.sec_type))
4916 .unwrap_or((0, 0));
4917
4918 if sec_type != 6 || !security.code.contains("main") {
4921 tracing::debug!(
4922 conn_id,
4923 sec_type,
4924 code = %security.code,
4925 "GetReference(Future): not a main future contract, returning empty"
4926 );
4927 let resp = futu_proto::qot_get_reference::Response {
4928 ret_type: 0,
4929 ret_msg: None,
4930 err_code: None,
4931 s2c: Some(futu_proto::qot_get_reference::S2c {
4932 static_info_list: Vec::new(),
4933 }),
4934 };
4935 return Some(prost::Message::encode_to_vec(&resp));
4936 }
4937
4938 let backend = match super::load_backend(&self.backend) {
4939 Some(b) => b,
4940 None => {
4941 tracing::warn!(conn_id, "GetReference(Future): no backend connection");
4942 return Some(super::make_error_response(-1, "Network interruption"));
4943 }
4944 };
4945
4946 let backend_req =
4948 futu_backend::proto_internal::ft_cmd_hp_plate::PlateUsFutrueRelatedListReq {
4949 future_id: Some(stock_id),
4950 };
4951 let body = prost::Message::encode_to_vec(&backend_req);
4952
4953 let frame = match backend.request(6701, body).await {
4954 Ok(f) => f,
4955 Err(e) => {
4956 tracing::error!(conn_id, error = %e, "CMD6701 request failed");
4957 return Some(super::make_error_response(-1, "pull future related failed"));
4958 }
4959 };
4960
4961 let rsp: futu_backend::proto_internal::ft_cmd_hp_plate::PlateUsFutrueRelatedListRsp =
4962 match prost::Message::decode(frame.body.as_ref()) {
4963 Ok(r) => r,
4964 Err(e) => {
4965 tracing::error!(conn_id, error = %e, "CMD6701 decode failed");
4966 return Some(super::make_error_response(
4967 -1,
4968 "decode future related response failed",
4969 ));
4970 }
4971 };
4972
4973 if rsp.result != 0 {
4974 tracing::warn!(conn_id, result = rsp.result, "CMD6701 returned error");
4975 return Some(super::make_error_response(
4976 -1,
4977 "backend future related query failed",
4978 ));
4979 }
4980
4981 let static_info_list: Vec<futu_proto::qot_common::SecurityStaticInfo> = rsp
4983 .security_qta_list
4984 .iter()
4985 .filter_map(|sq| {
4986 let sid = sq.security_id?;
4987 let skey = self.static_cache.id_to_key.get(&sid)?;
4988 let sinfo = self.static_cache.get_security_info(skey.value())?;
4989 Some(make_static_info(
4990 futu_proto::qot_common::Security {
4991 market: sinfo.market,
4992 code: sinfo.code.clone(),
4993 },
4994 &sinfo,
4995 ))
4996 })
4997 .collect();
4998
4999 tracing::debug!(
5000 conn_id,
5001 count = static_info_list.len(),
5002 "GetReference(Future) returning securities"
5003 );
5004
5005 let resp = futu_proto::qot_get_reference::Response {
5006 ret_type: 0,
5007 ret_msg: None,
5008 err_code: None,
5009 s2c: Some(futu_proto::qot_get_reference::S2c { static_info_list }),
5010 };
5011 Some(prost::Message::encode_to_vec(&resp))
5012 }
5013 _ => {
5014 Some(super::make_error_response(-1, "unsupported reference type"))
5016 }
5017 }
5018 }
5019}
5020
5021struct GetHoldingChangeListHandler;
5023
5024#[async_trait]
5025impl RequestHandler for GetHoldingChangeListHandler {
5026 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
5027 let _req: futu_proto::qot_get_holding_change_list::Request =
5028 prost::Message::decode(request.body.as_ref()).ok()?;
5029 tracing::debug!(
5032 conn_id,
5033 "GetHoldingChangeList: interface deprecated since 2020-12-21"
5034 );
5035 Some(super::make_error_response(
5036 -1,
5037 "Due to the reasons of the upstream data provider, the interface for Get Major Shareholders' Shareholding Changes (protocol ID:3208) will be abandoned after 2020-12-21",
5038 ))
5039 }
5040}
5041
5042struct GetUserSecurityHandler {
5044 backend: crate::bridge::SharedBackend,
5045 static_cache: Arc<StaticDataCache>,
5046 app_lang: i32,
5047}
5048
5049#[async_trait]
5050impl RequestHandler for GetUserSecurityHandler {
5051 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
5052 let req: futu_proto::qot_get_user_security::Request =
5053 prost::Message::decode(request.body.as_ref()).ok()?;
5054 let group_name = &req.c2s.group_name;
5055
5056 let backend = match super::load_backend(&self.backend) {
5057 Some(b) => b,
5058 None => {
5059 tracing::warn!(conn_id, "GetUserSecurity: no backend connection");
5060 return Some(super::make_error_response(-1, "no backend connection"));
5061 }
5062 };
5063
5064 let user_id = backend.user_id.load(std::sync::atomic::Ordering::Relaxed) as u64;
5065
5066 let group_req = futu_backend::proto_internal::wch_lst::GetGroupListReq {
5068 user_id: Some(user_id),
5069 };
5070 let group_body = prost::Message::encode_to_vec(&group_req);
5071 tracing::debug!(conn_id, %group_name, "GetUserSecurity: sending CMD5121");
5072
5073 let group_frame = match backend.request(5121, group_body).await {
5074 Ok(f) => f,
5075 Err(e) => {
5076 tracing::error!(conn_id, error = %e, "CMD5121 request failed");
5077 return Some(super::make_error_response(-1, "pull group info failed"));
5078 }
5079 };
5080
5081 let (group_rsp, group_langs) = super::decode_cmd5121_groups(group_frame.body.as_ref());
5084
5085 if group_rsp.result_code.unwrap_or(-1) != 0 {
5086 tracing::warn!(conn_id, ret = ?group_rsp.result_code, "CMD5121 returned error");
5087 return Some(super::make_error_response(-1, "pull group info failed"));
5088 }
5089
5090 let app_lang = self.app_lang;
5093 let group_id = group_rsp
5094 .group_list
5095 .iter()
5096 .enumerate()
5097 .find(|(i, g)| {
5098 if let Some(langs) = group_langs.get(*i) {
5100 if langs
5101 .iter()
5102 .any(|ml| ml.language_id == app_lang && ml.name == group_name.as_str())
5103 {
5104 return true;
5105 }
5106 }
5107 g.group_name.as_deref() == Some(group_name.as_str())
5109 })
5110 .map(|(_, g)| g.group_id.unwrap_or(0))
5111 .or_else(|| system_group_name_to_id(group_name))
5112 .unwrap_or(0);
5113 if group_id == 0 {
5114 tracing::warn!(conn_id, %group_name, "GetUserSecurity: unknown group name");
5115 return Some(super::make_error_response(
5116 -1,
5117 "unknown user security group",
5118 ));
5119 }
5120
5121 if group_id == 890 || group_id == 891 || group_id == 896 {
5123 return Some(super::make_error_response(
5124 -1,
5125 "unsupported user security group",
5126 ));
5127 }
5128
5129 let stock_req = futu_backend::proto_internal::wch_lst::GetStockListReq {
5131 user_id: Some(user_id),
5132 group_id: Some(group_id),
5133 is_case_top: None,
5134 };
5135 let stock_body = prost::Message::encode_to_vec(&stock_req);
5136 tracing::info!(conn_id, group_id, "GetUserSecurity: sending CMD5120");
5137
5138 let stock_frame = match backend.request(5120, stock_body).await {
5139 Ok(f) => f,
5140 Err(e) => {
5141 tracing::error!(conn_id, error = %e, "CMD5120 request failed");
5142 return Some(super::make_error_response(-1, "backend request failed"));
5143 }
5144 };
5145
5146 let body = stock_frame.body.as_ref();
5148
5149 let stock_rsp = super::decode_srpc_or_direct::<
5150 futu_backend::proto_internal::wch_lst::GetStockListResp,
5151 >(body, |r| {
5152 r.result_code == Some(0) && r.stock_list.len() == r.stock_count.unwrap_or(0) as usize
5153 });
5154
5155 tracing::info!(conn_id, result_code = ?stock_rsp.result_code,
5156 stock_list_len = stock_rsp.stock_list.len(),
5157 "CMD5120 response");
5158
5159 if stock_rsp.result_code.unwrap_or(0) != 0 && stock_rsp.stock_list.is_empty() {
5160 return Some(super::make_error_response(-1, "pull stock list failed"));
5161 }
5162
5163 let mut miss_id_count = 0u32;
5165 let mut miss_info_count = 0u32;
5166 let static_info_list: Vec<futu_proto::qot_common::SecurityStaticInfo> = stock_rsp
5167 .stock_list
5168 .iter()
5169 .filter_map(|si| {
5170 let stock_id = si.stock_id?;
5171 let key = match self.static_cache.id_to_key.get(&stock_id) {
5172 Some(k) => k,
5173 None => {
5174 miss_id_count += 1;
5175 if miss_id_count <= 3 {
5176 tracing::debug!(stock_id, "GetUserSecurity: stock_id not in id_to_key");
5177 }
5178 return None;
5179 }
5180 };
5181 let info = match self.static_cache.get_security_info(key.value()) {
5182 Some(i) => i,
5183 None => {
5184 miss_info_count += 1;
5185 return None;
5186 }
5187 };
5188 let security = futu_proto::qot_common::Security {
5189 market: info.market,
5190 code: info.code.clone(),
5191 };
5192 Some(make_static_info(security, &info))
5193 })
5194 .collect();
5195
5196 tracing::info!(
5197 conn_id,
5198 total = stock_rsp.stock_list.len(),
5199 found = static_info_list.len(),
5200 miss_id = miss_id_count,
5201 miss_info = miss_info_count,
5202 "GetUserSecurity result"
5203 );
5204
5205 let resp = futu_proto::qot_get_user_security::Response {
5206 ret_type: 0,
5207 ret_msg: None,
5208 err_code: None,
5209 s2c: Some(futu_proto::qot_get_user_security::S2c { static_info_list }),
5210 };
5211 Some(prost::Message::encode_to_vec(&resp))
5212 }
5213}
5214
5215struct ModifyUserSecurityHandler {
5217 backend: crate::bridge::SharedBackend,
5218 static_cache: Arc<StaticDataCache>,
5219}
5220
5221#[async_trait]
5222impl RequestHandler for ModifyUserSecurityHandler {
5223 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
5224 let req: futu_proto::qot_modify_user_security::Request =
5225 prost::Message::decode(request.body.as_ref()).ok()?;
5226 let c2s = &req.c2s;
5227 let group_name = &c2s.group_name;
5228 let op = c2s.op;
5229
5230 let backend = match super::load_backend(&self.backend) {
5231 Some(b) => b,
5232 None => {
5233 tracing::warn!(conn_id, "ModifyUserSecurity: no backend connection");
5234 return Some(super::make_error_response(-1, "no backend connection"));
5235 }
5236 };
5237
5238 if c2s.security_list.is_empty() {
5240 let resp = futu_proto::qot_modify_user_security::Response {
5241 ret_type: 0,
5242 ret_msg: None,
5243 err_code: None,
5244 s2c: Some(futu_proto::qot_modify_user_security::S2c {}),
5245 };
5246 return Some(prost::Message::encode_to_vec(&resp));
5247 }
5248
5249 let mut stock_ids = Vec::new();
5251 for sec in &c2s.security_list {
5252 let sec_key = format!("{}_{}", sec.market, sec.code);
5253 if let Some(info) = self.static_cache.get_security_info(&sec_key) {
5254 if info.stock_id > 0 {
5255 stock_ids.push(info.stock_id);
5256 }
5257 }
5258 }
5259
5260 if stock_ids.is_empty() {
5261 tracing::warn!(conn_id, "ModifyUserSecurity: no valid stock_ids found");
5262 return Some(super::make_error_response(-1, "unknown stock"));
5263 }
5264
5265 let user_id = backend.user_id.load(std::sync::atomic::Ordering::Relaxed) as u64;
5266
5267 let group_req = futu_backend::proto_internal::wch_lst::GetGroupListReq {
5269 user_id: Some(user_id),
5270 };
5271 let group_body = prost::Message::encode_to_vec(&group_req);
5272
5273 let group_frame = match backend.request(5121, group_body).await {
5274 Ok(f) => f,
5275 Err(e) => {
5276 tracing::error!(conn_id, error = %e, "CMD5121 request failed");
5277 return Some(super::make_error_response(-1, "pull group info failed"));
5278 }
5279 };
5280
5281 let (group_rsp, _group_langs) = super::decode_cmd5121_groups(group_frame.body.as_ref());
5283
5284 if group_rsp.result_code.unwrap_or(-1) != 0 {
5285 return Some(super::make_error_response(-1, "pull group info failed"));
5286 }
5287
5288 let group_id = match group_rsp
5289 .group_list
5290 .iter()
5291 .find(|g| g.group_name.as_deref() == Some(group_name.as_str()))
5292 {
5293 Some(g) => g.group_id.unwrap_or(0),
5294 None => {
5295 return Some(super::make_error_response(
5296 -1,
5297 "unknown user security group",
5298 ));
5299 }
5300 };
5301
5302 let is_system = (group_id > 0 && group_id < 900) || group_id == 1000;
5304 if is_system {
5305 return Some(super::make_error_response(
5306 -1,
5307 "system group cannot be modified",
5308 ));
5309 }
5310
5311 let action = match op {
5312 1 => 1u32,
5313 2 => 2u32,
5314 3 => 2u32, _ => {
5316 return Some(super::make_error_response(-1, "unknown modify operation"));
5317 }
5318 };
5319
5320 if op == 1 && group_id != 1000 {
5322 let all_req = futu_backend::proto_internal::wch_lst::SetStockReq {
5323 action: Some(1),
5324 group_id: Some(1000),
5325 stock_info: stock_ids
5326 .iter()
5327 .map(|&id| futu_backend::proto_internal::wch_lst::StockInfo {
5328 stock_id: Some(id),
5329 is_top: None,
5330 })
5331 .collect(),
5332 };
5333 let _ = backend
5334 .request(6682, prost::Message::encode_to_vec(&all_req))
5335 .await;
5336 }
5337
5338 let set_req = futu_backend::proto_internal::wch_lst::SetStockReq {
5340 action: Some(action),
5341 group_id: Some(group_id),
5342 stock_info: stock_ids
5343 .iter()
5344 .map(|&id| futu_backend::proto_internal::wch_lst::StockInfo {
5345 stock_id: Some(id),
5346 is_top: None,
5347 })
5348 .collect(),
5349 };
5350
5351 tracing::debug!(
5352 conn_id,
5353 group_id,
5354 op,
5355 count = stock_ids.len(),
5356 "ModifyUserSecurity: sending CMD6682"
5357 );
5358
5359 let set_frame = match backend
5360 .request(6682, prost::Message::encode_to_vec(&set_req))
5361 .await
5362 {
5363 Ok(f) => f,
5364 Err(e) => {
5365 tracing::error!(conn_id, error = %e, "CMD6682 request failed");
5366 return Some(super::make_error_response(-1, "backend request failed"));
5367 }
5368 };
5369
5370 let set_rsp: futu_backend::proto_internal::wch_lst::SetStockResp =
5371 match prost::Message::decode(set_frame.body.as_ref()) {
5372 Ok(r) => r,
5373 Err(e) => {
5374 tracing::error!(conn_id, error = %e, "CMD6682 decode failed");
5375 return Some(super::make_error_response(
5376 -1,
5377 "backend response decode failed",
5378 ));
5379 }
5380 };
5381
5382 if set_rsp.result_code.unwrap_or(-1) != 0 {
5383 let msg = if set_rsp.result_code == Some(3) {
5384 "user security total number exceeded"
5385 } else {
5386 "modify user security failed"
5387 };
5388 return Some(super::make_error_response(-1, msg));
5389 }
5390
5391 if op == 2 {
5393 let mut other_groups = vec![1000u32];
5394 for g in &group_rsp.group_list {
5395 let gid = g.group_id.unwrap_or(0);
5396 let g_is_system = (gid > 0 && gid < 900) || gid == 1000;
5397 if !g_is_system && gid != group_id {
5398 other_groups.push(gid);
5399 }
5400 }
5401 for gid in other_groups {
5402 let del_req = futu_backend::proto_internal::wch_lst::SetStockReq {
5403 action: Some(2),
5404 group_id: Some(gid),
5405 stock_info: stock_ids
5406 .iter()
5407 .map(|&id| futu_backend::proto_internal::wch_lst::StockInfo {
5408 stock_id: Some(id),
5409 is_top: None,
5410 })
5411 .collect(),
5412 };
5413 let _ = backend
5414 .request(6682, prost::Message::encode_to_vec(&del_req))
5415 .await;
5416 }
5417 }
5418
5419 let resp = futu_proto::qot_modify_user_security::Response {
5420 ret_type: 0,
5421 ret_msg: None,
5422 err_code: None,
5423 s2c: Some(futu_proto::qot_modify_user_security::S2c {}),
5424 };
5425 Some(prost::Message::encode_to_vec(&resp))
5426 }
5427}
5428
5429struct StockFilterHandler {
5438 backend: crate::bridge::SharedBackend,
5439 static_cache: Arc<StaticDataCache>,
5440}
5441
5442#[async_trait]
5443impl RequestHandler for StockFilterHandler {
5444 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
5445 let req: futu_proto::qot_stock_filter::Request =
5446 prost::Message::decode(request.body.as_ref()).ok()?;
5447 let c2s = &req.c2s;
5448
5449 let backend = match super::load_backend(&self.backend) {
5450 Some(b) => b,
5451 None => {
5452 tracing::warn!(conn_id, "StockFilter: no backend connection");
5453 return Some(super::make_error_response(-1, "no backend connection"));
5454 }
5455 };
5456
5457 if c2s.begin < 0 {
5459 return Some(super::make_error_response(-1, "begin must be >= 0"));
5460 }
5461 if c2s.num < 0 {
5462 return Some(super::make_error_response(-1, "num must be >= 0"));
5463 }
5464 if c2s.num > 200 {
5465 return Some(super::make_error_response(-1, "num exceeds limit 200"));
5466 }
5467
5468 let nn_market = stock_filter_market_api_to_nn(c2s.market);
5469 if nn_market == 0 {
5470 return Some(super::make_error_response(-1, "unsupported market"));
5471 }
5472
5473 tracing::debug!(
5474 conn_id,
5475 market = c2s.market,
5476 begin = c2s.begin,
5477 num = c2s.num,
5478 base_filters = c2s.base_filter_list.len(),
5479 accumulate_filters = c2s.accumulate_filter_list.len(),
5480 financial_filters = c2s.financial_filter_list.len(),
5481 pattern_filters = c2s.pattern_filter_list.len(),
5482 custom_filters = c2s.custom_indicator_filter_list.len(),
5483 "StockFilter: building CMD 9010 ScreenRequest"
5484 );
5485
5486 let mut screen_req = futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest {
5488 market: nn_market,
5489 ..Default::default()
5490 };
5491
5492 if let Some(plate) = &c2s.plate {
5494 if !plate.code.is_empty() {
5495 if let Some(plate_id) = stock_filter_security_to_id(&self.static_cache, plate) {
5496 screen_req.categories.push(plate_id);
5497 }
5498 }
5499 }
5500
5501 stock_filter_build_base_queries(c2s, &mut screen_req);
5503
5504 stock_filter_build_accumulate_queries(c2s, &mut screen_req);
5506
5507 stock_filter_build_financial_queries(c2s, nn_market, &mut screen_req);
5509
5510 stock_filter_build_pattern_queries(c2s, &mut screen_req);
5512
5513 stock_filter_build_custom_queries(c2s, &mut screen_req);
5515
5516 stock_filter_build_sort(c2s, &mut screen_req);
5518
5519 let screen_body = prost::Message::encode_to_vec(&screen_req);
5521 tracing::debug!(
5522 conn_id,
5523 body_len = screen_body.len(),
5524 "sending CMD 9010 ScreenRequest"
5525 );
5526
5527 let screen_frame = match backend.request(9010, screen_body).await {
5528 Ok(f) => f,
5529 Err(e) => {
5530 tracing::error!(conn_id, error = %e, "CMD 9010 request failed");
5531 return Some(super::make_error_response(
5532 -1,
5533 "backend screen request failed",
5534 ));
5535 }
5536 };
5537
5538 let screen_rsp: futu_backend::proto_internal::ft_cmd_stock_screener::ScreenResponse =
5539 match prost::Message::decode(screen_frame.body.as_ref()) {
5540 Ok(r) => r,
5541 Err(e) => {
5542 tracing::error!(conn_id, error = %e, "CMD 9010 decode failed");
5543 return Some(super::make_error_response(
5544 -1,
5545 "backend screen response decode failed",
5546 ));
5547 }
5548 };
5549
5550 if screen_rsp.result_code != 0 {
5551 tracing::warn!(
5552 conn_id,
5553 result = screen_rsp.result_code,
5554 "CMD 9010 returned error"
5555 );
5556 return Some(super::make_error_response(
5557 -1,
5558 "backend screen request failed",
5559 ));
5560 }
5561
5562 let all_stock_ids = &screen_rsp.stock_ids;
5563 let all_count = all_stock_ids.len() as i32;
5564
5565 let real_begin = 0.max(c2s.begin.min(all_count)) as usize;
5567 let real_count = 0.max(c2s.num.min(all_count - real_begin as i32)) as usize;
5568 let last_page = (real_begin + real_count) >= all_count as usize;
5569
5570 let paged_ids: Vec<u64> = if real_count > 0 {
5571 all_stock_ids[real_begin..real_begin + real_count].to_vec()
5572 } else {
5573 Vec::new()
5574 };
5575
5576 if paged_ids.is_empty() {
5577 let resp = futu_proto::qot_stock_filter::Response {
5578 ret_type: 0,
5579 ret_msg: None,
5580 err_code: None,
5581 s2c: Some(futu_proto::qot_stock_filter::S2c {
5582 last_page: true,
5583 all_count,
5584 data_list: Vec::new(),
5585 }),
5586 };
5587 return Some(prost::Message::encode_to_vec(&resp));
5588 }
5589
5590 let mut retrieve_req =
5592 futu_backend::proto_internal::ft_cmd_stock_screener::RetrieveRequest {
5593 stock_ids: paged_ids.clone(),
5594 ..Default::default()
5595 };
5596
5597 retrieve_req.basic_properties.push(
5599 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyBasic {
5600 name: Some(1101), },
5602 );
5603 retrieve_req.basic_properties.push(
5604 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyBasic {
5605 name: Some(1102), },
5607 );
5608
5609 stock_filter_build_retrieve_properties(c2s, &mut retrieve_req);
5611
5612 let retrieve_body = prost::Message::encode_to_vec(&retrieve_req);
5614 tracing::debug!(
5615 conn_id,
5616 body_len = retrieve_body.len(),
5617 stock_count = paged_ids.len(),
5618 "sending CMD 9011 RetrieveRequest"
5619 );
5620
5621 let retrieve_frame = match backend.request(9011, retrieve_body).await {
5622 Ok(f) => f,
5623 Err(e) => {
5624 tracing::error!(conn_id, error = %e, "CMD 9011 request failed");
5625 return Some(super::make_error_response(
5626 -1,
5627 "backend retrieve request failed",
5628 ));
5629 }
5630 };
5631
5632 let retrieve_rsp: futu_backend::proto_internal::ft_cmd_stock_screener::RetrieveResponse =
5633 match prost::Message::decode(retrieve_frame.body.as_ref()) {
5634 Ok(r) => r,
5635 Err(e) => {
5636 tracing::error!(conn_id, error = %e, "CMD 9011 decode failed");
5637 return Some(super::make_error_response(
5638 -1,
5639 "backend retrieve response decode failed",
5640 ));
5641 }
5642 };
5643
5644 if retrieve_rsp.result_code != 0 {
5645 tracing::warn!(
5646 conn_id,
5647 result = retrieve_rsp.result_code,
5648 "CMD 9011 returned error"
5649 );
5650 return Some(super::make_error_response(
5651 -1,
5652 "backend retrieve request failed",
5653 ));
5654 }
5655
5656 let data_list: Vec<futu_proto::qot_stock_filter::StockData> = retrieve_rsp
5658 .items
5659 .iter()
5660 .filter_map(|item| self.stock_filter_item_to_stock_data(item))
5661 .collect();
5662
5663 tracing::debug!(
5664 conn_id,
5665 all_count,
5666 returned = data_list.len(),
5667 last_page,
5668 "StockFilter returning data"
5669 );
5670
5671 let resp = futu_proto::qot_stock_filter::Response {
5672 ret_type: 0,
5673 ret_msg: None,
5674 err_code: None,
5675 s2c: Some(futu_proto::qot_stock_filter::S2c {
5676 last_page,
5677 all_count,
5678 data_list,
5679 }),
5680 };
5681 Some(prost::Message::encode_to_vec(&resp))
5682 }
5683}
5684
5685impl StockFilterHandler {
5686 fn stock_filter_item_to_stock_data(
5688 &self,
5689 item: &futu_backend::proto_internal::ft_cmd_stock_screener::retrieve_response::Item,
5690 ) -> Option<futu_proto::qot_stock_filter::StockData> {
5691 let stock_id = item.stock_id;
5692
5693 let security = self.resolve_security_from_stock_id(stock_id)?;
5695
5696 let name = item
5698 .basic_property_results
5699 .iter()
5700 .find(|r| r.property.name == Some(1102)) .map(|r| r.value.clone())
5702 .unwrap_or_default();
5703
5704 let mut base_data_list = Vec::new();
5706
5707 for r in &item.simple_property_results {
5708 let prop = &r.property;
5709 let nn_name = prop.name.unwrap_or(0);
5710 let api_field = stock_filter_simple_nn_to_api(nn_name);
5711 if api_field != 0 {
5712 let scale = stock_filter_base_scaling_ratio(nn_name);
5713 let value = if scale > 0.0 {
5714 r.value as f64 / scale
5715 } else {
5716 r.value as f64
5717 };
5718 base_data_list.push(futu_proto::qot_stock_filter::BaseData {
5719 field_name: api_field,
5720 value,
5721 });
5722 }
5723 }
5724
5725 let accumulate_data_list: Vec<futu_proto::qot_stock_filter::AccumulateData> = item
5727 .cumulative_property_results
5728 .iter()
5729 .filter_map(|r| {
5730 let prop = &r.property;
5731 let nn_name = prop.name.unwrap_or(0);
5732 let api_field = stock_filter_accumulate_nn_to_api(nn_name);
5733 if api_field == 0 {
5734 return None;
5735 }
5736 let scale = stock_filter_accumulate_scaling_ratio(nn_name);
5737 let value = if scale > 0.0 {
5738 r.value as f64 / scale
5739 } else {
5740 r.value as f64
5741 };
5742 let days = prop.days.unwrap_or(1) as i32;
5743 Some(futu_proto::qot_stock_filter::AccumulateData {
5744 field_name: api_field,
5745 value,
5746 days,
5747 })
5748 })
5749 .collect();
5750
5751 let mut financial_data_list = Vec::new();
5754 for r in &item.financial_property_results {
5755 let prop = &r.property;
5756 let nn_name = prop.name.unwrap_or(0);
5757 let scale = stock_filter_financial_scaling_ratio(nn_name);
5758 let value = if scale > 0.0 {
5759 r.value as f64 / scale
5760 } else {
5761 r.value as f64
5762 };
5763
5764 if let Some(api_base) = stock_filter_financial_to_base_api(nn_name) {
5766 base_data_list.push(futu_proto::qot_stock_filter::BaseData {
5767 field_name: api_base,
5768 value,
5769 });
5770 } else {
5771 let api_field = stock_filter_financial_nn_to_api(nn_name);
5772 if api_field != 0 {
5773 let quarter = stock_filter_financial_quarter_nn_to_api(prop.term.unwrap_or(0));
5774 financial_data_list.push(futu_proto::qot_stock_filter::FinancialData {
5775 field_name: api_field,
5776 value,
5777 quarter,
5778 });
5779 }
5780 }
5781 }
5782
5783 let custom_indicator_data_list: Vec<futu_proto::qot_stock_filter::CustomIndicatorData> =
5785 item.indicator_property_results
5786 .iter()
5787 .filter_map(|r| {
5788 let prop = &r.property;
5789 let nn_indicator = prop.name.unwrap_or(0);
5790 let api_field = stock_filter_indicator_nn_to_custom_api(nn_indicator);
5791 if api_field == 0 {
5792 return None;
5793 }
5794 let scale = stock_filter_custom_scaling_ratio(nn_indicator);
5795 let value = if scale > 0.0 {
5796 r.value as f64 / scale
5797 } else {
5798 r.value as f64
5799 };
5800 let kl_type = stock_filter_period_nn_to_api(prop.period.unwrap_or(0));
5801 let field_para_list: Vec<i32> =
5802 prop.indicator_params.iter().map(|&p| p as i32).collect();
5803 Some(futu_proto::qot_stock_filter::CustomIndicatorData {
5804 field_name: api_field,
5805 value,
5806 kl_type,
5807 field_para_list,
5808 })
5809 })
5810 .collect();
5811
5812 Some(futu_proto::qot_stock_filter::StockData {
5813 security,
5814 name,
5815 base_data_list,
5816 accumulate_data_list,
5817 financial_data_list,
5818 custom_indicator_data_list,
5819 })
5820 }
5821
5822 fn resolve_security_from_stock_id(
5823 &self,
5824 stock_id: u64,
5825 ) -> Option<futu_proto::qot_common::Security> {
5826 let key = self.static_cache.id_to_key.get(&stock_id)?;
5827 let parts: Vec<&str> = key.split('_').collect();
5828 if parts.len() != 2 {
5829 return None;
5830 }
5831 let market: i32 = parts[0].parse().ok()?;
5832 let code = parts[1].to_string();
5833 Some(futu_proto::qot_common::Security { market, code })
5834 }
5835}
5836
5837fn stock_filter_market_api_to_nn(api_market: i32) -> i32 {
5840 match api_market {
5841 1 | 12 => 1, 11 => 2, 21 | 22 => 3, _ => 0, }
5846}
5847
5848fn stock_filter_security_to_id(
5849 cache: &StaticDataCache,
5850 sec: &futu_proto::qot_common::Security,
5851) -> Option<u64> {
5852 let key = format!("{}_{}", sec.market, sec.code);
5853 cache.id_to_key.iter().find_map(|entry| {
5854 if *entry.value() == key {
5855 Some(*entry.key())
5856 } else {
5857 None
5858 }
5859 })
5860}
5861
5862fn stock_filter_base_api_to_nn_simple(api_field: i32) -> i32 {
5865 match api_field {
5866 3 => 2201, 4 => 2209, 5 => 2210, 6 => 2211, 7 => 2212, 8 => 2217, 9 => 2218, 10 => 2219, 11 => 2301, 12 => 2302, 13 => 2303, 14 => 2304, 15 => 2213, 16 => 2214, _ => 0,
5881 }
5882}
5883
5884fn stock_filter_base_api_to_nn_financial(api_field: i32) -> i32 {
5886 match api_field {
5887 17 => 4904, 18 => 4905, 19 => 4901, 20 => 4902, 21 => 4903, _ => 0,
5893 }
5894}
5895
5896fn stock_filter_is_base_price_field(api_field: i32) -> bool {
5897 matches!(api_field, 17..=21)
5898}
5899
5900fn stock_filter_simple_nn_to_api(nn_name: i32) -> i32 {
5901 match nn_name {
5902 2201 => 3, 2209 => 4, 2210 => 5, 2211 => 6, 2212 => 7, 2217 => 8, 2218 => 9, 2219 => 10, 2301 => 11, 2302 => 12, 2303 => 13, 2304 => 14, 2213 => 15, 2214 => 16, _ => 0,
5917 }
5918}
5919
5920fn stock_filter_base_scaling_ratio(nn_name: i32) -> f64 {
5921 match nn_name {
5922 2201 | 2209 | 2210 | 2211 | 2212 | 2213 | 2214 | 2219 | 2301 => 1000.0,
5923 2217 | 2218 | 2302 | 2303 | 2304 => 100_000.0,
5924 _ => 1.0,
5925 }
5926}
5927
5928fn stock_filter_accumulate_api_to_nn(api_field: i32) -> i32 {
5931 match api_field {
5932 1 => 3102, 2 => 3103, 3 => 3104, 4 => 3105, 5 => 3106, _ => 0,
5938 }
5939}
5940
5941fn stock_filter_accumulate_nn_to_api(nn_name: i32) -> i32 {
5942 match nn_name {
5943 3102 => 1, 3103 => 2, 3104 => 3, 3105 => 4, 3106 => 5, _ => 0,
5949 }
5950}
5951
5952fn stock_filter_accumulate_scaling_ratio(nn_name: i32) -> f64 {
5953 match nn_name {
5954 3102 | 3103 | 3105 | 3106 => 1000.0,
5955 3104 => 1.0,
5956 _ => 1.0,
5957 }
5958}
5959
5960fn stock_filter_financial_api_to_nn(api_field: i32) -> i32 {
5963 match api_field {
5964 1 => 4101, 2 => 4102, 3 => 4105, 4 => 4106, 5 => 4107, 6 => 4108, 7 => 4109, 8 => 4110, 9 => 4202, 10 => 4209, 11 => 4206, 12 => 4201, 13 => 4210, 14 => 4203, 15 => 4204, 16 => 4205, 17 => 4207, 18 => 4208, 19 => 4211, 20 => 4301, 21 => 4302, 22 => 4402, 23 => 4403, 24 => 4404, 25 => 4405, 26 => 4401, 27 => 4502, 28 => 4503, 29 => 4504, 30 => 4505, 31 => 4501, 32 => 4601, 33 => 4602, 34 => 4603, 35 => 4604, 36 => 4605, 37 => 4606, 38 => 4607, 39 => 4608, 40 => 4609, 41 => 4610, 42 => 4701, 43 => 4702, 44 => 4801, 45 => 4802, 46 => 4803, _ => 0,
6011 }
6012}
6013
6014fn stock_filter_financial_nn_to_api(nn_name: i32) -> i32 {
6015 match nn_name {
6016 4101 => 1,
6017 4102 => 2,
6018 4105 => 3,
6019 4106 => 4,
6020 4107 => 5,
6021 4108 => 6,
6022 4109 => 7,
6023 4110 => 8,
6024 4202 => 9,
6025 4209 => 10,
6026 4206 => 11,
6027 4201 => 12,
6028 4210 => 13,
6029 4203 => 14,
6030 4204 => 15,
6031 4205 => 16,
6032 4207 => 17,
6033 4208 => 18,
6034 4211 => 19,
6035 4301 => 20,
6036 4302 => 21,
6037 4402 => 22,
6038 4403 => 23,
6039 4404 => 24,
6040 4405 => 25,
6041 4401 => 26,
6042 4502 => 27,
6043 4503 => 28,
6044 4504 => 29,
6045 4505 => 30,
6046 4501 => 31,
6047 4601 => 32,
6048 4602 => 33,
6049 4603 => 34,
6050 4604 => 35,
6051 4605 => 36,
6052 4606 => 37,
6053 4607 => 38,
6054 4608 => 39,
6055 4609 => 40,
6056 4610 => 41,
6057 4701 => 42,
6058 4702 => 43,
6059 4801 => 44,
6060 4802 => 45,
6061 4803 => 46,
6062 _ => 0,
6063 }
6064}
6065
6066fn stock_filter_financial_to_base_api(nn_name: i32) -> Option<i32> {
6069 match nn_name {
6070 4901 => Some(19), 4902 => Some(20), 4903 => Some(21), 4904 => Some(17), 4905 => Some(18), _ => None,
6076 }
6077}
6078
6079fn stock_filter_financial_scaling_ratio(nn_name: i32) -> f64 {
6080 match nn_name {
6081 4404 => 100_000.0, _ => 1000.0, }
6084}
6085
6086fn stock_filter_financial_quarter_api_to_nn(api_quarter: i32) -> i32 {
6089 match api_quarter {
6090 1 => 100, 2 => 1, 3 => 6, 4 => 9, 5 => 10, _ => 0,
6096 }
6097}
6098
6099fn stock_filter_financial_quarter_nn_to_api(nn_term: i32) -> i32 {
6100 match nn_term {
6101 100 => 1, 1 => 2, 6 => 3, 9 => 4, 10 => 5, _ => 0,
6107 }
6108}
6109
6110fn stock_filter_is_annual_only(api_field: i32) -> bool {
6111 matches!(api_field, 10 | 11 | 13 | 17 | 18 | 19 | 30)
6112}
6113
6114fn stock_filter_pattern_api_to_nn(api_field: i32) -> i32 {
6117 match api_field {
6118 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 31, 6 => 32, 7 => 33, 8 => 34, 9 => 11, 10 => 12, 11 => 13, 12 => 14, 13 => 21, 14 => 22, 15 => 23, 16 => 24, 17 => 41, 18 => 42, 19 => 43, 20 => 44, _ => 0,
6139 }
6140}
6141
6142fn stock_filter_custom_api_to_nn(api_field: i32) -> i32 {
6145 match api_field {
6146 1 => 1, 2 => 11, 3 => 12, 4 => 13, 5 => 14, 6 => 15, 7 => 16, 8 => 17, 9 => 52, 10 => 21, 11 => 22, 12 => 23, 13 => 24, 14 => 25, 15 => 26, 16 => 27, 17 => 0, 30 => 18, 40 => 28, 50 => 34, 51 => 35, 52 => 36, 60 => 44, 61 => 45, 62 => 46, 70 => 64, 71 => 65, 72 => 66, _ => 0,
6175 }
6176}
6177
6178fn stock_filter_indicator_nn_to_custom_api(nn_indicator: i32) -> i32 {
6179 match nn_indicator {
6180 1 => 1, 11 => 2, 12 => 3, 13 => 4, 14 => 5, 15 => 6, 16 => 7, 17 => 8, 18 => 30, 21 => 10, 22 => 11, 23 => 12, 24 => 13, 25 => 14, 26 => 15, 27 => 16, 28 => 40, 31 | 34 => 50, 32 | 35 => 51, 33 | 36 => 52, 41 | 44 => 60, 42 | 45 => 61, 43 | 46 => 62, 51 | 52 => 9, 61 | 64 => 70, 62 | 65 => 71, 63 | 66 => 72, _ => 0,
6208 }
6209}
6210
6211fn stock_filter_custom_scaling_ratio(nn_indicator: i32) -> f64 {
6212 match nn_indicator {
6213 0 => 1.0,
6214 _ => 1000.0,
6215 }
6216}
6217
6218fn stock_filter_relative_position_api_to_nn(api_pos: i32) -> i32 {
6221 match api_pos {
6222 1 => 1, 2 => 2, 3 => 3, 4 => 4, _ => 0,
6227 }
6228}
6229
6230fn stock_filter_kl_type_api_to_nn(api_kl: i32) -> i32 {
6231 match api_kl {
6232 6 => 5, 8 => 11, 9 => 21, 10 => 31, _ => 0,
6237 }
6238}
6239
6240fn stock_filter_period_nn_to_api(nn_period: i32) -> i32 {
6241 match nn_period {
6242 5 => 6, 11 => 8, 21 => 9, 31 => 10, _ => 0,
6247 }
6248}
6249
6250fn stock_filter_sort_dir_api_to_nn(api_dir: i32) -> i32 {
6251 match api_dir {
6252 1 => 1, 2 => 2, _ => 0,
6255 }
6256}
6257
6258fn stock_filter_make_boundary(
6261 value: f64,
6262 scale: f64,
6263) -> futu_backend::proto_internal::ft_cmd_stock_screener::Bounary {
6264 let scaled = if scale > 0.0 {
6265 (value * scale) as i64
6266 } else {
6267 value as i64
6268 };
6269 futu_backend::proto_internal::ft_cmd_stock_screener::Bounary {
6270 value: scaled,
6271 includes: true,
6272 }
6273}
6274
6275fn stock_filter_build_base_queries(
6276 c2s: &futu_proto::qot_stock_filter::C2s,
6277 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6278) {
6279 for f in &c2s.base_filter_list {
6280 if f.is_no_filter.unwrap_or(true) {
6281 continue;
6282 }
6283 let api_field = f.field_name;
6284 if api_field == 1 || api_field == 2 {
6285 continue;
6286 }
6287
6288 if stock_filter_is_base_price_field(api_field) {
6289 let nn_fin = stock_filter_base_api_to_nn_financial(api_field);
6290 if nn_fin == 0 {
6291 continue;
6292 }
6293 let scale = stock_filter_financial_scaling_ratio(nn_fin);
6294 let mut query =
6295 futu_backend::proto_internal::ft_cmd_stock_screener::QueryPropertyFinancial {
6296 property:
6297 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6298 name: Some(nn_fin),
6299 term: None,
6300 duration: None,
6301 year: None,
6302 period_average: None,
6303 },
6304 ..Default::default()
6305 };
6306 if let Some(min) = f.filter_min {
6307 query.lower = Some(stock_filter_make_boundary(min, scale));
6308 }
6309 if let Some(max) = f.filter_max {
6310 query.upper = Some(stock_filter_make_boundary(max, scale));
6311 }
6312 screen_req.financial_property_queries.push(query);
6313 } else {
6314 let nn_simple = stock_filter_base_api_to_nn_simple(api_field);
6315 if nn_simple == 0 {
6316 continue;
6317 }
6318 let scale = stock_filter_base_scaling_ratio(nn_simple);
6319 let mut query =
6320 futu_backend::proto_internal::ft_cmd_stock_screener::QueryPropertySimple {
6321 property: futu_backend::proto_internal::ft_cmd_stock_screener::PropertySimple {
6322 name: Some(nn_simple),
6323 },
6324 ..Default::default()
6325 };
6326 if let Some(min) = f.filter_min {
6327 query.lower = Some(stock_filter_make_boundary(min, scale));
6328 }
6329 if let Some(max) = f.filter_max {
6330 query.upper = Some(stock_filter_make_boundary(max, scale));
6331 }
6332 screen_req.simple_property_queries.push(query);
6333 }
6334 }
6335}
6336
6337fn stock_filter_build_accumulate_queries(
6338 c2s: &futu_proto::qot_stock_filter::C2s,
6339 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6340) {
6341 for f in &c2s.accumulate_filter_list {
6342 if f.is_no_filter.unwrap_or(true) {
6343 continue;
6344 }
6345 let nn_field = stock_filter_accumulate_api_to_nn(f.field_name);
6346 if nn_field == 0 {
6347 continue;
6348 }
6349 let scale = stock_filter_accumulate_scaling_ratio(nn_field);
6350 let mut query =
6351 futu_backend::proto_internal::ft_cmd_stock_screener::QueryPropertyCumulative {
6352 property: futu_backend::proto_internal::ft_cmd_stock_screener::PropertyCumulative {
6353 name: Some(nn_field),
6354 days: Some(f.days as u32),
6355 period_average: None,
6356 },
6357 ..Default::default()
6358 };
6359 if let Some(min) = f.filter_min {
6360 query.lower = Some(stock_filter_make_boundary(min, scale));
6361 }
6362 if let Some(max) = f.filter_max {
6363 query.upper = Some(stock_filter_make_boundary(max, scale));
6364 }
6365 screen_req.cumulative_property_queries.push(query);
6366 }
6367}
6368
6369fn stock_filter_build_financial_queries(
6370 c2s: &futu_proto::qot_stock_filter::C2s,
6371 nn_market: i32,
6372 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6373) {
6374 for f in &c2s.financial_filter_list {
6375 if f.is_no_filter.unwrap_or(true) {
6376 continue;
6377 }
6378 let nn_field = stock_filter_financial_api_to_nn(f.field_name);
6379 if nn_field == 0 {
6380 continue;
6381 }
6382 let scale = stock_filter_financial_scaling_ratio(nn_field);
6383
6384 let nn_term = if stock_filter_is_annual_only(f.field_name) {
6385 100
6386 } else {
6387 let q = stock_filter_financial_quarter_api_to_nn(f.quarter);
6388 if q == 0 {
6389 continue;
6390 }
6391 if (nn_market == 1 || nn_market == 3) && q == 10 {
6392 continue;
6393 }
6394 if nn_market == 2 && matches!(q, 1 | 6 | 9) {
6395 continue;
6396 }
6397 q
6398 };
6399
6400 let mut query =
6401 futu_backend::proto_internal::ft_cmd_stock_screener::QueryPropertyFinancial {
6402 property: futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6403 name: Some(nn_field),
6404 term: Some(nn_term),
6405 duration: None,
6406 year: None,
6407 period_average: None,
6408 },
6409 ..Default::default()
6410 };
6411 if let Some(min) = f.filter_min {
6412 query.lower = Some(stock_filter_make_boundary(min, scale));
6413 }
6414 if let Some(max) = f.filter_max {
6415 query.upper = Some(stock_filter_make_boundary(max, scale));
6416 }
6417 screen_req.financial_property_queries.push(query);
6418 }
6419}
6420
6421fn stock_filter_build_pattern_queries(
6422 c2s: &futu_proto::qot_stock_filter::C2s,
6423 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6424) {
6425 for f in &c2s.pattern_filter_list {
6426 if f.is_no_filter.unwrap_or(true) {
6427 continue;
6428 }
6429 let nn_pattern = stock_filter_pattern_api_to_nn(f.field_name);
6430 if nn_pattern == 0 {
6431 continue;
6432 }
6433 let nn_period = stock_filter_kl_type_api_to_nn(f.kl_type);
6434 let mut query =
6435 futu_backend::proto_internal::ft_cmd_stock_screener::QueryIndicatorPattern {
6436 pattern: nn_pattern,
6437 period: nn_period,
6438 continuous_period: None,
6439 };
6440 if let Some(cp) = f.consecutive_period {
6441 if cp > 0 {
6442 query.continuous_period = Some(cp);
6443 }
6444 }
6445 screen_req.indicator_pattern_queries.push(query);
6446 }
6447}
6448
6449fn stock_filter_build_custom_queries(
6450 c2s: &futu_proto::qot_stock_filter::C2s,
6451 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6452) {
6453 for f in &c2s.custom_indicator_filter_list {
6454 if f.is_no_filter.unwrap_or(true) {
6455 continue;
6456 }
6457 let nn_first = stock_filter_custom_api_to_nn(f.first_field_name);
6458 if nn_first == 0 {
6459 continue;
6460 }
6461 let nn_second_raw = f.second_field_name;
6462 let nn_second = stock_filter_custom_api_to_nn(nn_second_raw);
6463 let nn_position = stock_filter_relative_position_api_to_nn(f.relative_position);
6464 let nn_period = stock_filter_kl_type_api_to_nn(f.kl_type);
6465 let scale = stock_filter_custom_scaling_ratio(nn_first);
6466
6467 let mut query =
6468 futu_backend::proto_internal::ft_cmd_stock_screener::QueryIndicatorPositional {
6469 position: nn_position,
6470 period: nn_period,
6471 first_indicator: nn_first,
6472 second_indicator: None,
6473 second_value: None,
6474 first_indicator_params: f.first_field_para_list.iter().map(|&v| v as i64).collect(),
6475 second_indicator_params: f
6476 .second_field_para_list
6477 .iter()
6478 .map(|&v| v as i64)
6479 .collect(),
6480 continuous_period: None,
6481 };
6482
6483 if nn_second_raw != 17 && nn_second_raw != 0 && nn_second != 0 {
6484 query.second_indicator = Some(nn_second);
6485 }
6486 if nn_second_raw == 17 {
6487 if let Some(val) = f.field_value {
6488 query.second_value = Some((val * scale) as i64);
6489 }
6490 }
6491 if let Some(cp) = f.consecutive_period {
6492 if cp > 0 {
6493 query.continuous_period = Some(cp);
6494 }
6495 }
6496 screen_req.indicator_positional_queries.push(query);
6497 }
6498}
6499
6500fn stock_filter_build_sort(
6501 c2s: &futu_proto::qot_stock_filter::C2s,
6502 screen_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::ScreenRequest,
6503) {
6504 for f in &c2s.base_filter_list {
6506 if f.is_no_filter.unwrap_or(true) {
6507 continue;
6508 }
6509 let dir = f.sort_dir.unwrap_or(0);
6510 let nn_dir = stock_filter_sort_dir_api_to_nn(dir);
6511 if nn_dir == 0 {
6512 continue;
6513 }
6514 let api_field = f.field_name;
6515 if api_field == 1 || api_field == 2 {
6516 continue;
6517 }
6518 let mut sort = futu_backend::proto_internal::ft_cmd_stock_screener::Sort {
6519 direction: nn_dir,
6520 ..Default::default()
6521 };
6522 if stock_filter_is_base_price_field(api_field) {
6523 sort.financial_property = Some(
6524 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6525 name: Some(stock_filter_base_api_to_nn_financial(api_field)),
6526 term: None,
6527 duration: None,
6528 year: None,
6529 period_average: None,
6530 },
6531 );
6532 } else {
6533 sort.simple_property = Some(
6534 futu_backend::proto_internal::ft_cmd_stock_screener::PropertySimple {
6535 name: Some(stock_filter_base_api_to_nn_simple(api_field)),
6536 },
6537 );
6538 }
6539 screen_req.sort = Some(sort);
6540 return;
6541 }
6542
6543 for f in &c2s.accumulate_filter_list {
6545 if f.is_no_filter.unwrap_or(true) {
6546 continue;
6547 }
6548 let dir = f.sort_dir.unwrap_or(0);
6549 let nn_dir = stock_filter_sort_dir_api_to_nn(dir);
6550 if nn_dir == 0 {
6551 continue;
6552 }
6553 let nn_field = stock_filter_accumulate_api_to_nn(f.field_name);
6554 if nn_field == 0 {
6555 continue;
6556 }
6557 screen_req.sort = Some(futu_backend::proto_internal::ft_cmd_stock_screener::Sort {
6558 direction: nn_dir,
6559 cumulative_property: Some(
6560 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyCumulative {
6561 name: Some(nn_field),
6562 days: Some(f.days as u32),
6563 period_average: None,
6564 },
6565 ),
6566 ..Default::default()
6567 });
6568 return;
6569 }
6570
6571 for f in &c2s.financial_filter_list {
6573 if f.is_no_filter.unwrap_or(true) {
6574 continue;
6575 }
6576 let dir = f.sort_dir.unwrap_or(0);
6577 let nn_dir = stock_filter_sort_dir_api_to_nn(dir);
6578 if nn_dir == 0 {
6579 continue;
6580 }
6581 let nn_field = stock_filter_financial_api_to_nn(f.field_name);
6582 if nn_field == 0 {
6583 continue;
6584 }
6585 let nn_term = if stock_filter_is_annual_only(f.field_name) {
6586 100
6587 } else {
6588 stock_filter_financial_quarter_api_to_nn(f.quarter)
6589 };
6590 screen_req.sort = Some(futu_backend::proto_internal::ft_cmd_stock_screener::Sort {
6591 direction: nn_dir,
6592 financial_property: Some(
6593 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6594 name: Some(nn_field),
6595 term: Some(nn_term),
6596 duration: None,
6597 year: None,
6598 period_average: None,
6599 },
6600 ),
6601 ..Default::default()
6602 });
6603 return;
6604 }
6605}
6606
6607fn stock_filter_build_retrieve_properties(
6610 c2s: &futu_proto::qot_stock_filter::C2s,
6611 retrieve_req: &mut futu_backend::proto_internal::ft_cmd_stock_screener::RetrieveRequest,
6612) {
6613 for f in &c2s.base_filter_list {
6614 if f.is_no_filter.unwrap_or(true) {
6615 continue;
6616 }
6617 let api_field = f.field_name;
6618 if api_field == 1 || api_field == 2 {
6619 continue;
6620 }
6621 if stock_filter_is_base_price_field(api_field) {
6622 let nn_fin = stock_filter_base_api_to_nn_financial(api_field);
6623 if nn_fin != 0 {
6624 retrieve_req.financial_properties.push(
6625 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6626 name: Some(nn_fin),
6627 term: None,
6628 duration: None,
6629 year: None,
6630 period_average: None,
6631 },
6632 );
6633 }
6634 } else {
6635 let nn_simple = stock_filter_base_api_to_nn_simple(api_field);
6636 if nn_simple != 0 {
6637 retrieve_req.simple_properties.push(
6638 futu_backend::proto_internal::ft_cmd_stock_screener::PropertySimple {
6639 name: Some(nn_simple),
6640 },
6641 );
6642 }
6643 }
6644 }
6645
6646 for f in &c2s.accumulate_filter_list {
6647 if f.is_no_filter.unwrap_or(true) {
6648 continue;
6649 }
6650 let nn_field = stock_filter_accumulate_api_to_nn(f.field_name);
6651 if nn_field != 0 {
6652 retrieve_req.cumulative_properties.push(
6653 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyCumulative {
6654 name: Some(nn_field),
6655 days: Some(f.days as u32),
6656 period_average: None,
6657 },
6658 );
6659 }
6660 }
6661
6662 for f in &c2s.financial_filter_list {
6663 if f.is_no_filter.unwrap_or(true) {
6664 continue;
6665 }
6666 let nn_field = stock_filter_financial_api_to_nn(f.field_name);
6667 if nn_field != 0 {
6668 let nn_term = if stock_filter_is_annual_only(f.field_name) {
6669 100
6670 } else {
6671 stock_filter_financial_quarter_api_to_nn(f.quarter)
6672 };
6673 retrieve_req.financial_properties.push(
6674 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyFinancial {
6675 name: Some(nn_field),
6676 term: Some(nn_term),
6677 duration: None,
6678 year: None,
6679 period_average: None,
6680 },
6681 );
6682 }
6683 }
6684
6685 let mut seen_indicators: std::collections::HashSet<(i32, i32, Vec<i32>)> =
6687 std::collections::HashSet::new();
6688 for f in &c2s.custom_indicator_filter_list {
6689 if f.is_no_filter.unwrap_or(true) {
6690 continue;
6691 }
6692 if f.consecutive_period.unwrap_or(0) > 1 {
6693 continue;
6694 }
6695 let nn_period = stock_filter_kl_type_api_to_nn(f.kl_type);
6696
6697 let nn_first = stock_filter_custom_api_to_nn(f.first_field_name);
6698 if nn_first != 0 {
6699 let params: Vec<i32> = f.first_field_para_list.clone();
6700 let key = (nn_first, nn_period, params.clone());
6701 if seen_indicators.insert(key) {
6702 retrieve_req.indicator_properties.push(
6703 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyIndicator {
6704 name: Some(nn_first),
6705 period: Some(nn_period),
6706 indicator_params: params.iter().map(|&v| v as i64).collect(),
6707 },
6708 );
6709 }
6710 }
6711
6712 let nn_second_raw = f.second_field_name;
6713 if nn_second_raw != 17 && nn_second_raw != 0 {
6714 let nn_second = stock_filter_custom_api_to_nn(nn_second_raw);
6715 if nn_second != 0 {
6716 let params: Vec<i32> = f.second_field_para_list.clone();
6717 let key = (nn_second, nn_period, params.clone());
6718 if seen_indicators.insert(key) {
6719 retrieve_req.indicator_properties.push(
6720 futu_backend::proto_internal::ft_cmd_stock_screener::PropertyIndicator {
6721 name: Some(nn_second),
6722 period: Some(nn_period),
6723 indicator_params: params.iter().map(|&v| v as i64).collect(),
6724 },
6725 );
6726 }
6727 }
6728 }
6729 }
6730}
6731
6732struct GetCodeChangeHandler {
6743 code_change_cache: futu_backend::code_change::CodeChangeCache,
6744}
6745
6746fn code_change_fits_condition(
6748 info: &futu_backend::code_change::CodeChangeInfo,
6749 c2s: &futu_proto::qot_get_code_change::C2s,
6750) -> bool {
6751 if info.change_type == futu_backend::code_change::CodeChangeType::Unknown {
6753 return false;
6754 }
6755
6756 for sec in &c2s.security_list {
6758 if sec.market != info.qot_market || sec.code != info.sec_code {
6759 return false;
6760 }
6761 }
6762
6763 for &type_val in &c2s.type_list {
6765 if type_val != info.change_type as i32 {
6766 return false;
6767 }
6768 }
6769
6770 for tf in &c2s.time_filter_list {
6772 let has_begin = tf.begin_time.is_some();
6773 let has_end = tf.end_time.is_some();
6774 if !has_begin && !has_end {
6775 continue;
6776 }
6777
6778 let time_val = match tf.r#type {
6780 1 => info.public_time, 2 => info.effective_time, 3 => info.end_time, _ => continue,
6784 };
6785 if time_val == 0 {
6786 continue;
6787 }
6788
6789 let filter_market = c2s.security_list.first().map(|s| s.market).unwrap_or(1);
6791 if let Some(ref begin_str) = tf.begin_time {
6792 let begin_ts = parse_api_time_str(begin_str, filter_market).unwrap_or(0);
6793 if begin_ts > 0 && time_val < begin_ts {
6794 return false;
6795 }
6796 }
6797
6798 if let Some(ref end_str) = tf.end_time {
6799 let end_ts = parse_api_time_str(end_str, filter_market).unwrap_or(0) + 86400 - 1;
6802 if end_ts > 0 && time_val > end_ts {
6803 return false;
6804 }
6805 }
6806 }
6807
6808 true
6809}
6810
6811fn code_change_info_to_proto(
6817 info: &futu_backend::code_change::CodeChangeInfo,
6818) -> futu_proto::qot_get_code_change::CodeChangeInfo {
6819 let security = futu_proto::qot_common::Security {
6820 market: info.qot_market,
6821 code: info.sec_code.clone(),
6822 };
6823
6824 let related_security = futu_proto::qot_common::Security {
6825 market: info.qot_market,
6826 code: info.relate_sec_code.clone(),
6827 };
6828
6829 let public_time = if info.public_time > 0 {
6831 Some(format_timestamp(info.public_time))
6832 } else {
6833 None
6834 };
6835 let public_timestamp = if info.public_time > 0 {
6836 Some(info.public_time as f64)
6837 } else {
6838 None
6839 };
6840
6841 let effective_time = if info.effective_time > 0 {
6842 Some(format_timestamp(info.effective_time))
6843 } else {
6844 None
6845 };
6846 let effective_timestamp = if info.effective_time > 0 {
6847 Some(info.effective_time as f64)
6848 } else {
6849 None
6850 };
6851
6852 let (end_time, end_timestamp) = if info.change_type
6854 != futu_backend::code_change::CodeChangeType::GemToMain
6855 && info.end_time > 0
6856 {
6857 (
6858 Some(format_timestamp(info.end_time)),
6859 Some(info.end_time as f64),
6860 )
6861 } else {
6862 (None, None)
6863 };
6864
6865 futu_proto::qot_get_code_change::CodeChangeInfo {
6866 r#type: info.change_type as i32,
6867 security,
6868 related_security,
6869 public_time,
6870 public_timestamp,
6871 effective_time,
6872 effective_timestamp,
6873 end_time,
6874 end_timestamp,
6875 }
6876}
6877
6878#[async_trait]
6879impl RequestHandler for GetCodeChangeHandler {
6880 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
6881 let req: futu_proto::qot_get_code_change::Request =
6882 prost::Message::decode(request.body.as_ref()).ok()?;
6883 let c2s = &req.c2s;
6884
6885 let cache = self.code_change_cache.read();
6886 let mut code_change_list = Vec::new();
6887
6888 for info in cache.iter() {
6889 if code_change_fits_condition(info, c2s) {
6890 code_change_list.push(code_change_info_to_proto(info));
6891 }
6892 }
6893
6894 tracing::debug!(
6895 conn_id,
6896 total_cached = cache.len(),
6897 matched = code_change_list.len(),
6898 "GetCodeChange"
6899 );
6900
6901 let resp = futu_proto::qot_get_code_change::Response {
6902 ret_type: 0,
6903 ret_msg: None,
6904 err_code: None,
6905 s2c: Some(futu_proto::qot_get_code_change::S2c { code_change_list }),
6906 };
6907 Some(prost::Message::encode_to_vec(&resp))
6908 }
6909}
6910
6911struct GetIpoListHandler {
6913 backend: crate::bridge::SharedBackend,
6914 static_cache: Arc<StaticDataCache>,
6915}
6916
6917#[async_trait]
6918impl RequestHandler for GetIpoListHandler {
6919 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
6920 let req: futu_proto::qot_get_ipo_list::Request =
6921 prost::Message::decode(request.body.as_ref()).ok()?;
6922 let c2s = &req.c2s;
6923
6924 let backend = match super::load_backend(&self.backend) {
6925 Some(b) => b,
6926 None => {
6927 tracing::warn!(conn_id, "GetIpoList: no backend connection");
6928 return Some(super::make_error_response(-1, "no backend connection"));
6929 }
6930 };
6931
6932 let request_types: Vec<i32> = match c2s.market {
6937 1 => vec![6, 7], 2 => vec![5], 21 | 22 => vec![1, 2, 3, 4], _ => vec![5], };
6942
6943 let mut merged_hk_list = Vec::new();
6945 let mut merged_us_list = Vec::new();
6946 let mut merged_cn_list = Vec::new();
6947
6948 for req_type in &request_types {
6949 let backend_req =
6950 futu_backend::proto_internal::ft_cmd_ipo_calender6955_6959::IpoListReq {
6951 market: Some(c2s.market),
6952 request_type: Some(*req_type),
6953 language: Some(1), };
6955 let body = prost::Message::encode_to_vec(&backend_req);
6956
6957 tracing::debug!(
6958 conn_id,
6959 market = c2s.market,
6960 request_type = req_type,
6961 "sending CMD6956 IpoListReq"
6962 );
6963
6964 let resp_frame = match backend.request(6956, body).await {
6965 Ok(f) => f,
6966 Err(e) => {
6967 tracing::warn!(conn_id, error = %e, request_type = req_type, "CMD6956 request failed");
6968 continue;
6969 }
6970 };
6971
6972 let rsp: futu_backend::proto_internal::ft_cmd_ipo_calender6955_6959::IpoListRsp =
6973 match prost::Message::decode(resp_frame.body.as_ref()) {
6974 Ok(r) => r,
6975 Err(e) => {
6976 tracing::warn!(conn_id, error = %e, request_type = req_type, "CMD6956 decode failed");
6977 continue;
6978 }
6979 };
6980
6981 merged_hk_list.extend(rsp.hk_list);
6982 merged_us_list.extend(rsp.us_list);
6983 merged_cn_list.extend(rsp.cn_list);
6984 }
6985
6986 let backend_rsp = futu_backend::proto_internal::ft_cmd_ipo_calender6955_6959::IpoListRsp {
6987 market: None,
6988 hk_list: merged_hk_list,
6989 us_list: merged_us_list,
6990 cn_list: merged_cn_list,
6991 request_type: None,
6992 };
6993
6994 let ipo_count =
6995 backend_rsp.cn_list.len() + backend_rsp.hk_list.len() + backend_rsp.us_list.len();
6996
6997 tracing::debug!(
6998 conn_id,
6999 cn = backend_rsp.cn_list.len(),
7000 hk = backend_rsp.hk_list.len(),
7001 us = backend_rsp.us_list.len(),
7002 "GetIpoList: received {} IPO items from backend",
7003 ipo_count
7004 );
7005
7006 let mut ipo_list = Vec::with_capacity(ipo_count);
7007
7008 for item in &backend_rsp.hk_list {
7010 let stock_id = match item.stock_id {
7011 Some(id) if id > 0 => id,
7012 _ => continue,
7013 };
7014 let security = match self.resolve_security(stock_id) {
7015 Some(s) => s,
7016 None => {
7017 tracing::warn!(
7018 stock_id,
7019 name = ?item.stock_name,
7020 "GetIpoList: HK stock_id not found in cache, skipping"
7021 );
7022 continue;
7023 }
7024 };
7025 let name = item.stock_name.clone().unwrap_or_default();
7026
7027 let (list_time, list_timestamp) = ipo_timestamp_fields(item.ipo_timestamp);
7029
7030 let basic = futu_proto::qot_get_ipo_list::BasicIpoData {
7031 security,
7032 name,
7033 list_time,
7034 list_timestamp,
7035 };
7036
7037 let ipo_price_min = item.ipo_price_low.map(|v| v as f64 / 100.0).unwrap_or(0.0);
7039 let ipo_price_max = item.ipo_price_high.map(|v| v as f64 / 100.0).unwrap_or(0.0);
7040 let list_price = item
7042 .ipo_price
7043 .as_deref()
7044 .and_then(|s| s.parse::<f64>().ok())
7045 .unwrap_or(0.0);
7046 let lot_size = item
7048 .lot_size
7049 .as_deref()
7050 .and_then(|s| s.parse::<i32>().ok())
7051 .unwrap_or(0);
7052 let entrance_price = item
7054 .entrance_fee_num
7055 .map(|v| v as f64 / 100.0)
7056 .unwrap_or(0.0);
7057 let is_subscribe_status =
7059 item.eipo_flag.unwrap_or(0) != 0 && item.apply_countdown_secs.unwrap_or(0) > 0;
7060
7061 let (apply_end_time, apply_end_timestamp) =
7063 ipo_timestamp_fields(item.apply_end_timestamp);
7064
7065 let hk_ex = futu_proto::qot_get_ipo_list::HkIpoExData {
7066 ipo_price_min,
7067 ipo_price_max,
7068 list_price,
7069 lot_size,
7070 entrance_price,
7071 is_subscribe_status,
7072 apply_end_time,
7073 apply_end_timestamp,
7074 };
7075
7076 ipo_list.push(futu_proto::qot_get_ipo_list::IpoData {
7077 basic,
7078 cn_ex_data: None,
7079 hk_ex_data: Some(hk_ex),
7080 us_ex_data: None,
7081 });
7082 }
7083
7084 for item in &backend_rsp.us_list {
7086 let stock_id = match item.stock_id {
7087 Some(id) if id > 0 => id,
7088 _ => continue,
7089 };
7090 let security = match self.resolve_security(stock_id) {
7091 Some(s) => s,
7092 None => continue,
7093 };
7094 let name = item.stock_name.clone().unwrap_or_default();
7095
7096 let (list_time, list_timestamp) = ipo_timestamp_fields_u64(item.ipo_date_timestamp);
7098
7099 let basic = futu_proto::qot_get_ipo_list::BasicIpoData {
7100 security,
7101 name,
7102 list_time,
7103 list_timestamp,
7104 };
7105
7106 let ipo_price_min = item.ipo_price_low.map(|v| v as f64 / 100.0).unwrap_or(0.0);
7108 let ipo_price_max = item.ipo_price_high.map(|v| v as f64 / 100.0).unwrap_or(0.0);
7109 let issue_size = item.shares_num.unwrap_or(0);
7111
7112 let us_ex = futu_proto::qot_get_ipo_list::UsIpoExData {
7113 ipo_price_min,
7114 ipo_price_max,
7115 issue_size,
7116 };
7117
7118 ipo_list.push(futu_proto::qot_get_ipo_list::IpoData {
7119 basic,
7120 cn_ex_data: None,
7121 hk_ex_data: None,
7122 us_ex_data: Some(us_ex),
7123 });
7124 }
7125
7126 for item in &backend_rsp.cn_list {
7128 let stock_id = match item.stock_id {
7129 Some(id) if id > 0 => id,
7130 _ => continue,
7131 };
7132 let security = match self.resolve_security(stock_id) {
7133 Some(s) => s,
7134 None => continue,
7135 };
7136 let name = item.stock_name.clone().unwrap_or_default();
7137
7138 let (list_time, list_timestamp) = ipo_timestamp_fields_u64(item.ipo_date_timestamp);
7140
7141 let basic = futu_proto::qot_get_ipo_list::BasicIpoData {
7142 security,
7143 name,
7144 list_time,
7145 list_timestamp,
7146 };
7147
7148 let apply_code = item.apply_code.clone().unwrap_or_default();
7149 let ipo_price = item
7151 .ipo_price
7152 .as_deref()
7153 .and_then(|s| s.parse::<f64>().ok())
7154 .unwrap_or(0.0);
7155 let is_estimate_ipo_price = item.is_ipo_price_preview.unwrap_or(0) != 0;
7156 let apply_upper_limit = item.apply_limit_num.unwrap_or(0);
7158 let issue_pe_rate = item.pe_num.map(|v| v as f64 / 100.0).unwrap_or(0.0);
7160 let winning_ratio = item
7162 .lucky_ratio
7163 .as_deref()
7164 .and_then(|s| s.parse::<f64>().ok())
7165 .unwrap_or(0.0);
7166 let is_estimate_winning_ratio = item.is_lucky_ratio_preview.unwrap_or(0) != 0;
7167 let is_has_won = item.lucky_ok.unwrap_or(0) != 0;
7169
7170 let (apply_time, apply_timestamp) = ipo_timestamp_fields_u64(item.apply_date_timestamp);
7172 let (winning_time, winning_timestamp) =
7174 ipo_timestamp_fields_u64(item.lucky_date_timestamp);
7175
7176 let cn_ex = futu_proto::qot_get_ipo_list::CnIpoExData {
7180 apply_code,
7181 issue_size: 0,
7182 online_issue_size: 0,
7183 apply_upper_limit,
7184 apply_limit_market_value: 0,
7185 is_estimate_ipo_price,
7186 ipo_price,
7187 industry_pe_rate: 0.0,
7188 is_estimate_winning_ratio,
7189 winning_ratio,
7190 issue_pe_rate,
7191 apply_time,
7192 apply_timestamp,
7193 winning_time,
7194 winning_timestamp,
7195 is_has_won,
7196 winning_num_data: Vec::new(),
7197 };
7198
7199 ipo_list.push(futu_proto::qot_get_ipo_list::IpoData {
7200 basic,
7201 cn_ex_data: Some(cn_ex),
7202 hk_ex_data: None,
7203 us_ex_data: None,
7204 });
7205 }
7206
7207 tracing::debug!(
7208 conn_id,
7209 mapped = ipo_list.len(),
7210 "GetIpoList: mapped {} IPO items to FTAPI",
7211 ipo_list.len()
7212 );
7213
7214 let resp = futu_proto::qot_get_ipo_list::Response {
7215 ret_type: 0,
7216 ret_msg: None,
7217 err_code: None,
7218 s2c: Some(futu_proto::qot_get_ipo_list::S2c { ipo_list }),
7219 };
7220 Some(prost::Message::encode_to_vec(&resp))
7221 }
7222}
7223
7224impl GetIpoListHandler {
7225 fn resolve_security(&self, stock_id: u64) -> Option<futu_proto::qot_common::Security> {
7227 let key = self.static_cache.id_to_key.get(&stock_id)?;
7228 let parts: Vec<&str> = key.split('_').collect();
7229 if parts.len() != 2 {
7230 return None;
7231 }
7232 let market: i32 = parts[0].parse().ok()?;
7233 let code = parts[1].to_string();
7234 Some(futu_proto::qot_common::Security { market, code })
7235 }
7236}
7237
7238fn ipo_timestamp_fields(ts: Option<u32>) -> (Option<String>, Option<f64>) {
7241 match ts {
7242 Some(v) if v > 0 => {
7243 let time_str = timestamp_to_date_str(v as u64);
7244 (Some(time_str), Some(v as f64))
7245 }
7246 _ => (None, None),
7247 }
7248}
7249
7250fn ipo_timestamp_fields_u64(ts: Option<u64>) -> (Option<String>, Option<f64>) {
7252 match ts {
7253 Some(v) if v > 0 => {
7254 let time_str = timestamp_to_date_str(v);
7255 (Some(time_str), Some(v as f64))
7256 }
7257 _ => (None, None),
7258 }
7259}
7260
7261struct GetFutureInfoHandler {
7263 backend: crate::bridge::SharedBackend,
7264 static_cache: Arc<StaticDataCache>,
7265}
7266
7267#[async_trait]
7268impl RequestHandler for GetFutureInfoHandler {
7269 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
7270 let req: futu_proto::qot_get_future_info::Request =
7271 prost::Message::decode(request.body.as_ref()).ok()?;
7272 let c2s = &req.c2s;
7273
7274 let backend = match super::load_backend(&self.backend) {
7275 Some(b) => b,
7276 None => {
7277 tracing::warn!(conn_id, "GetFutureInfo: no backend connection");
7278 return Some(super::make_error_response(-1, "no backend connection"));
7279 }
7280 };
7281
7282 let mut stock_ids = Vec::new();
7284 for sec in &c2s.security_list {
7285 let sec_key = format!("{}_{}", sec.market, sec.code);
7286 if let Some(info) = self.static_cache.get_security_info(&sec_key) {
7287 if info.stock_id > 0 {
7288 stock_ids.push(info.stock_id);
7289 }
7290 }
7291 }
7292
7293 if stock_ids.is_empty() {
7294 tracing::warn!(conn_id, "GetFutureInfo: no valid stock_ids found");
7295 let resp = futu_proto::qot_get_future_info::Response {
7296 ret_type: 0,
7297 ret_msg: None,
7298 err_code: None,
7299 s2c: Some(futu_proto::qot_get_future_info::S2c {
7300 future_info_list: Vec::new(),
7301 }),
7302 };
7303 return Some(prost::Message::encode_to_vec(&resp));
7304 }
7305
7306 let backend_req =
7308 futu_backend::proto_internal::ft_cmd_us_future_info::FutureDetailInfoListReq {
7309 security_id_list: stock_ids.clone(),
7310 };
7311 let body = prost::Message::encode_to_vec(&backend_req);
7312
7313 tracing::debug!(
7314 conn_id,
7315 count = stock_ids.len(),
7316 "sending CMD6337 FutureDetailInfoListReq"
7317 );
7318
7319 let resp_frame = match backend.request(6337, body).await {
7320 Ok(f) => f,
7321 Err(e) => {
7322 tracing::error!(conn_id, error = %e, "CMD6337 request failed");
7323 return Some(super::make_error_response(-1, "backend request failed"));
7324 }
7325 };
7326
7327 let backend_rsp: futu_backend::proto_internal::ft_cmd_us_future_info::FutureDetailInfoListRsp =
7328 match prost::Message::decode(resp_frame.body.as_ref()) {
7329 Ok(r) => r,
7330 Err(e) => {
7331 tracing::error!(conn_id, error = %e, "CMD6337 decode failed");
7332 return Some(super::make_error_response(
7333 -1,
7334 "backend response decode failed",
7335 ));
7336 }
7337 };
7338
7339 if backend_rsp.ret_code.unwrap_or(-1) != 0 {
7340 tracing::warn!(conn_id, ret = ?backend_rsp.ret_code, "CMD6337 returned error");
7341 return Some(super::make_error_response(
7342 -1,
7343 "backend future info request failed",
7344 ));
7345 }
7346
7347 let future_info_list: Vec<futu_proto::qot_get_future_info::FutureInfo> = backend_rsp
7349 .future_detail_info_list
7350 .iter()
7351 .filter_map(|detail| {
7352 let security_id = detail.security_id?;
7354 let key = self.static_cache.id_to_key.get(&security_id)?;
7355 let parts: Vec<&str> = key.split('_').collect();
7356 if parts.len() != 2 {
7357 return None;
7358 }
7359 let market: i32 = parts[0].parse().ok()?;
7360 let code = parts[1].to_string();
7361 let security = futu_proto::qot_common::Security { market, code };
7362
7363 let last_trade_time = detail
7365 .last_trade_date
7366 .map(|ts| timestamp_to_date_str(ts as u64))
7367 .unwrap_or_default();
7368
7369 let last_trade_timestamp = detail.last_trade_date.map(|ts| ts as f64);
7370
7371 let trade_time: Vec<futu_proto::qot_get_future_info::TradeTime> = detail
7373 .trade_time_detail_list
7374 .iter()
7375 .map(|tt| futu_proto::qot_get_future_info::TradeTime {
7376 begin: tt.begin.map(|v| v as f64),
7377 end: tt.end.map(|v| v as f64),
7378 })
7379 .collect();
7380
7381 Some(futu_proto::qot_get_future_info::FutureInfo {
7382 name: detail.name.clone().unwrap_or_default(),
7383 security,
7384 last_trade_time,
7385 last_trade_timestamp,
7386 owner: None,
7387 owner_other: detail.show_variety.clone().unwrap_or_default(),
7388 exchange: detail.exchange.clone().unwrap_or_default(),
7389 contract_type: detail.category.clone().unwrap_or_default(),
7390 contract_size: detail.scale.unwrap_or(0) as f64,
7391 contract_size_unit: detail.scale_unit.clone().unwrap_or_default(),
7392 quote_currency: detail.currency.clone().unwrap_or_default(),
7393 min_var: detail.minimum_variation_value.unwrap_or(0.0),
7394 min_var_unit: detail.minimum_variation_unit.clone().unwrap_or_default(),
7395 quote_unit: detail.price_quotation.clone(),
7396 trade_time,
7397 time_zone: detail.time_zone.clone().unwrap_or_default(),
7398 exchange_format_url: detail.specification_url.clone().unwrap_or_default(),
7399 origin: None,
7400 })
7401 })
7402 .collect();
7403
7404 tracing::debug!(
7405 conn_id,
7406 count = future_info_list.len(),
7407 "GetFutureInfo: returning future info"
7408 );
7409
7410 let resp = futu_proto::qot_get_future_info::Response {
7411 ret_type: 0,
7412 ret_msg: None,
7413 err_code: None,
7414 s2c: Some(futu_proto::qot_get_future_info::S2c { future_info_list }),
7415 };
7416 Some(prost::Message::encode_to_vec(&resp))
7417 }
7418}
7419
7420struct SetPriceReminderHandler {
7422 backend: crate::bridge::SharedBackend,
7423 static_cache: Arc<StaticDataCache>,
7424}
7425
7426#[async_trait]
7427impl RequestHandler for SetPriceReminderHandler {
7428 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
7429 let req: futu_proto::qot_set_price_reminder::Request =
7430 prost::Message::decode(request.body.as_ref()).ok()?;
7431 let c2s = &req.c2s;
7432
7433 let backend = match super::load_backend(&self.backend) {
7434 Some(b) => b,
7435 None => {
7436 tracing::warn!(conn_id, "SetPriceReminder: no backend connection");
7437 return Some(super::make_error_response(-1, "no backend connection"));
7438 }
7439 };
7440
7441 let sec = &c2s.security;
7443 let sec_key = format!("{}_{}", sec.market, sec.code);
7444 let stock_id = match self.static_cache.get_security_info(&sec_key) {
7445 Some(info) if info.stock_id > 0 => info.stock_id,
7446 _ => {
7447 return Some(super::make_error_response(-1, "unknown stock"));
7448 }
7449 };
7450
7451 let api_op = c2s.op;
7454 let is_del_all = api_op == 6;
7455
7456 let mut backend_req = futu_backend::proto_internal::ft_cmd_price_warn::PriceWarnSetNewReq {
7457 sid: Some(stock_id),
7458 delete_flag: None,
7459 attr: Vec::new(),
7460 };
7461
7462 if is_del_all {
7463 backend_req.delete_flag = Some(true);
7464 } else {
7465 let oper_type: u32 = match api_op {
7466 1 => 1, 2 => 2, 3..=5 => 3, _ => {
7470 return Some(super::make_error_response(-1, "unknown price reminder op"));
7471 }
7472 };
7473
7474 let key_val = c2s.key.filter(|_| api_op != 1); let enable = match api_op {
7476 3 => Some(true),
7477 4 => Some(false),
7478 _ => None,
7479 };
7480
7481 let warn_type = c2s.r#type.and_then(api_reminder_type_to_warn_type);
7483
7484 let freq_type = c2s.freq.and_then(api_freq_to_backend_freq);
7486
7487 let warn_param = c2s.value.map(|v| (v * 1000.0).round() as i64);
7489
7490 let note = c2s.note.clone();
7491
7492 let notify_time_periods: Vec<u32> = c2s
7494 .reminder_session_list
7495 .iter()
7496 .filter_map(|&s| api_market_status_to_notify_period(s))
7497 .collect();
7498
7499 let now_ts = std::time::SystemTime::now()
7500 .duration_since(std::time::UNIX_EPOCH)
7501 .unwrap_or_default()
7502 .as_secs();
7503
7504 let attr = futu_backend::proto_internal::ft_cmd_price_warn::ItemAttr {
7505 key: key_val.map(|k| k as u64),
7506 warn_type,
7507 warn_param,
7508 note,
7509 enable,
7510 update_time: Some(now_ts),
7511 freq_type,
7512 oper_type: Some(oper_type),
7513 stock_id: None,
7514 fine_warn_param: None,
7515 notify_time_periods,
7516 };
7517 backend_req.attr.push(attr);
7518 }
7519
7520 let body = prost::Message::encode_to_vec(&backend_req);
7521 tracing::debug!(
7522 conn_id,
7523 stock_id,
7524 api_op,
7525 "SetPriceReminder: sending CMD6809"
7526 );
7527
7528 let resp_frame = match backend.request(6809, body).await {
7529 Ok(f) => f,
7530 Err(e) => {
7531 tracing::error!(conn_id, error = %e, "CMD6809 request failed");
7532 return Some(super::make_error_response(-1, "backend request failed"));
7533 }
7534 };
7535
7536 let backend_rsp: futu_backend::proto_internal::ft_cmd_price_warn::PriceWarnSetNewRsp =
7537 match prost::Message::decode(resp_frame.body.as_ref()) {
7538 Ok(r) => r,
7539 Err(e) => {
7540 tracing::error!(conn_id, error = %e, "CMD6809 decode failed");
7541 return Some(super::make_error_response(
7542 -1,
7543 "backend response decode failed",
7544 ));
7545 }
7546 };
7547
7548 if backend_rsp.result.unwrap_or(-1) != 0 {
7549 let err_text = backend_rsp.err_text.unwrap_or_default();
7550 let msg = if err_text.is_empty() {
7551 "set price reminder failed".to_string()
7552 } else {
7553 err_text
7554 };
7555 return Some(super::make_error_response(-1, &msg));
7556 }
7557
7558 let key = backend_rsp.server_seq.unwrap_or(0) as i64;
7561
7562 let resp = futu_proto::qot_set_price_reminder::Response {
7563 ret_type: 0,
7564 ret_msg: None,
7565 err_code: None,
7566 s2c: Some(futu_proto::qot_set_price_reminder::S2c { key }),
7567 };
7568 Some(prost::Message::encode_to_vec(&resp))
7569 }
7570}
7571
7572fn api_reminder_type_to_warn_type(api_type: i32) -> Option<u32> {
7574 match api_type {
7575 1 => Some(4), 2 => Some(8), 3 => Some(1), 4 => Some(2), 5 => Some(9), 6 => Some(10), 7 => Some(11), 8 => Some(12), 9 => Some(13), 10 => Some(14), 11 => Some(15), 12 => Some(16), 13 => Some(17), 14 => Some(19), 15 => Some(20), _ => None,
7591 }
7592}
7593
7594fn warn_type_to_api_reminder_type(warn_type: u32) -> i32 {
7596 match warn_type {
7597 4 => 1, 8 => 2, 1 => 3, 2 => 4, 9 => 5, 10 => 6, 11 => 7, 12 => 8, 13 => 9, 14 => 10, 15 => 11, 16 => 12, 17 => 13, 19 => 14, 20 => 15, _ => 0, }
7614}
7615
7616fn api_freq_to_backend_freq(api_freq: i32) -> Option<u32> {
7618 match api_freq {
7619 1 => Some(1), 2 => Some(2), 3 => Some(4), _ => None,
7623 }
7624}
7625
7626fn backend_freq_to_api_freq(freq: u32) -> i32 {
7628 match freq {
7629 1 => 1, 2 => 2, 4 => 3, _ => 0, }
7634}
7635
7636fn api_market_status_to_notify_period(status: i32) -> Option<u32> {
7638 match status {
7639 1 => Some(1), 2 => Some(2), 3 => Some(3), 4 => Some(4), _ => None,
7644 }
7645}
7646
7647fn notify_period_to_api_market_status(period: u32) -> i32 {
7649 match period {
7650 1 => 1, 2 => 2, 3 => 3, 4 => 4, _ => 0, }
7656}
7657
7658fn api_qot_market_to_price_reminder_market(qot_market: i32) -> u32 {
7660 match qot_market {
7661 1 | 6 => 1, 11 => 10, 21 | 22 => 30, _ => 0,
7665 }
7666}
7667
7668struct GetPriceReminderHandler {
7670 backend: crate::bridge::SharedBackend,
7671 static_cache: Arc<StaticDataCache>,
7672}
7673
7674#[async_trait]
7675impl RequestHandler for GetPriceReminderHandler {
7676 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
7677 let req: futu_proto::qot_get_price_reminder::Request =
7678 prost::Message::decode(request.body.as_ref()).ok()?;
7679 let c2s = &req.c2s;
7680
7681 let backend = match super::load_backend(&self.backend) {
7682 Some(b) => b,
7683 None => {
7684 tracing::warn!(conn_id, "GetPriceReminder: no backend connection");
7685 return Some(super::make_error_response(-1, "no backend connection"));
7686 }
7687 };
7688
7689 let (stock_id_opt, market_opt) = if let Some(sec) = &c2s.security {
7691 let sec_key = format!("{}_{}", sec.market, sec.code);
7692 match self.static_cache.get_security_info(&sec_key) {
7693 Some(info) if info.stock_id > 0 => (Some(info.stock_id), None),
7694 _ => {
7695 return Some(super::make_error_response(-1, "unknown stock"));
7696 }
7697 }
7698 } else if let Some(market) = c2s.market {
7699 let backend_market = api_qot_market_to_price_reminder_market(market);
7700 if backend_market == 0 {
7701 return Some(super::make_error_response(-1, "unsupported market"));
7702 }
7703 (None, Some(backend_market))
7704 } else {
7705 return Some(super::make_error_response(
7706 -1,
7707 "must specify security or market",
7708 ));
7709 };
7710
7711 let backend_req = futu_backend::proto_internal::ft_cmd_price_warn::PriceWarnGetNewReq {
7713 stock_id: stock_id_opt,
7714 market: market_opt,
7715 };
7716 let body = prost::Message::encode_to_vec(&backend_req);
7717
7718 tracing::debug!(
7719 conn_id,
7720 stock_id = ?stock_id_opt,
7721 market = ?market_opt,
7722 "GetPriceReminder: sending CMD6808"
7723 );
7724
7725 let resp_frame = match backend.request(6808, body).await {
7726 Ok(f) => f,
7727 Err(e) => {
7728 tracing::error!(conn_id, error = %e, "CMD6808 request failed");
7729 return Some(super::make_error_response(-1, "backend request failed"));
7730 }
7731 };
7732
7733 let backend_rsp: futu_backend::proto_internal::ft_cmd_price_warn::PriceWarnGetNewRsp =
7734 match prost::Message::decode(resp_frame.body.as_ref()) {
7735 Ok(r) => r,
7736 Err(e) => {
7737 tracing::error!(conn_id, error = %e, "CMD6808 decode failed");
7738 return Some(super::make_error_response(
7739 -1,
7740 "backend response decode failed",
7741 ));
7742 }
7743 };
7744
7745 if backend_rsp.result.unwrap_or(-1) != 0 {
7746 let err_text = backend_rsp.err_text.unwrap_or_default();
7747 let msg = if err_text.is_empty() {
7748 "get price reminder failed".to_string()
7749 } else {
7750 err_text
7751 };
7752 return Some(super::make_error_response(-1, &msg));
7753 }
7754
7755 let price_reminder_list: Vec<futu_proto::qot_get_price_reminder::PriceReminder> =
7757 backend_rsp
7758 .warn_items
7759 .iter()
7760 .filter_map(|warn_item| {
7761 let sid = warn_item.stock_id?;
7762 let key_ref = self.static_cache.id_to_key.get(&sid)?;
7764 let info = self.static_cache.get_security_info(key_ref.value())?;
7765 let security = futu_proto::qot_common::Security {
7766 market: info.market,
7767 code: info.code.clone(),
7768 };
7769
7770 let item_list: Vec<futu_proto::qot_get_price_reminder::PriceReminderItem> =
7771 warn_item
7772 .attr
7773 .iter()
7774 .filter_map(|attr| {
7775 let wt = attr.warn_type?;
7776 if wt == 18 {
7778 return None;
7779 }
7780 let api_type = warn_type_to_api_reminder_type(wt);
7781 let value = attr.warn_param.unwrap_or(0) as f64 / 1000.0;
7782 let note = attr.note.clone().unwrap_or_default();
7783 let freq = backend_freq_to_api_freq(attr.freq_type.unwrap_or(0));
7784 let is_enable = attr.enable.unwrap_or(false);
7785 let key = attr.key.unwrap_or(0) as i64;
7786
7787 let reminder_session_list: Vec<i32> = attr
7788 .notify_time_periods
7789 .iter()
7790 .map(|&p| notify_period_to_api_market_status(p))
7791 .collect();
7792
7793 Some(futu_proto::qot_get_price_reminder::PriceReminderItem {
7794 key,
7795 r#type: api_type,
7796 value,
7797 note,
7798 freq,
7799 is_enable,
7800 reminder_session_list,
7801 })
7802 })
7803 .collect();
7804
7805 Some(futu_proto::qot_get_price_reminder::PriceReminder {
7806 security,
7807 name: Some(info.name.clone()),
7808 item_list,
7809 })
7810 })
7811 .collect();
7812
7813 tracing::debug!(
7814 conn_id,
7815 count = price_reminder_list.len(),
7816 "GetPriceReminder: returning {} reminders",
7817 price_reminder_list.len()
7818 );
7819
7820 let resp = futu_proto::qot_get_price_reminder::Response {
7821 ret_type: 0,
7822 ret_msg: None,
7823 err_code: None,
7824 s2c: Some(futu_proto::qot_get_price_reminder::S2c {
7825 price_reminder_list,
7826 }),
7827 };
7828 Some(prost::Message::encode_to_vec(&resp))
7829 }
7830}
7831
7832struct GetUserSecurityGroupHandler {
7836 backend: crate::bridge::SharedBackend,
7837 app_lang: i32,
7838}
7839
7840#[async_trait]
7841impl RequestHandler for GetUserSecurityGroupHandler {
7842 async fn handle(&self, conn_id: u64, request: &IncomingRequest) -> Option<Vec<u8>> {
7843 let req: futu_proto::qot_get_user_security_group::Request =
7844 prost::Message::decode(request.body.as_ref()).ok()?;
7845 let group_type = req.c2s.group_type;
7846
7847 let backend = match super::load_backend(&self.backend) {
7848 Some(b) => b,
7849 None => {
7850 tracing::warn!(conn_id, "GetUserSecurityGroup: no backend connection");
7851 return Some(super::make_error_response(-1, "no backend connection"));
7852 }
7853 };
7854
7855 if !(1..=3).contains(&group_type) {
7857 return Some(super::make_error_response(-1, "unsupported group type"));
7858 }
7859
7860 let user_id = backend.user_id.load(std::sync::atomic::Ordering::Relaxed) as u64;
7861
7862 let group_req = futu_backend::proto_internal::wch_lst::GetGroupListReq {
7863 user_id: Some(user_id),
7864 };
7865 let group_body = prost::Message::encode_to_vec(&group_req);
7866 tracing::info!(conn_id, group_type, "GetUserSecurityGroup: sending CMD5121");
7867
7868 let group_frame = match backend.request(5121, group_body).await {
7869 Ok(f) => f,
7870 Err(e) => {
7871 tracing::error!(conn_id, error = %e, "CMD5121 request failed");
7872 return Some(super::make_error_response(-1, "pull group info failed"));
7873 }
7874 };
7875
7876 let (group_rsp, group_langs) = super::decode_cmd5121_groups(group_frame.body.as_ref());
7879
7880 if group_rsp.result_code.unwrap_or(-1) != 0 {
7881 return Some(super::make_error_response(-1, "pull group info failed"));
7882 }
7883
7884 let app_lang = self.app_lang;
7886 let mut group_list = Vec::new();
7887 for (i, g) in group_rsp.group_list.iter().enumerate() {
7888 let gid = g.group_id.unwrap_or(0);
7889 let is_system_group = (gid > 0 && gid < 900) || gid == 1000;
7890
7891 let mut name = String::new();
7896 if is_system_group {
7897 if let Some(langs) = group_langs.get(i) {
7898 if let Some(ml) = langs.iter().find(|m| m.language_id == app_lang) {
7899 name = ml.name.clone();
7900 }
7901 }
7902 }
7903 if name.is_empty() {
7904 name = g.group_name.clone().unwrap_or_default();
7905 }
7906 if is_system_group && name.is_empty() {
7907 if let Some(localized) = system_group_id_to_name(gid, app_lang as u8) {
7908 name = localized.to_string();
7909 }
7910 }
7911
7912 if gid == 890 || gid == 891 || gid == 896 {
7914 continue;
7915 }
7916
7917 let include = match group_type {
7918 1 => !is_system_group, 2 => is_system_group, 3 => true, _ => false,
7922 };
7923
7924 if include {
7925 let api_group_type = if is_system_group { 2 } else { 1 };
7926 group_list.push(futu_proto::qot_get_user_security_group::GroupData {
7927 group_name: name,
7928 group_type: api_group_type,
7929 });
7930 }
7931 }
7932
7933 tracing::debug!(
7934 conn_id,
7935 count = group_list.len(),
7936 "GetUserSecurityGroup: returning {} groups",
7937 group_list.len()
7938 );
7939
7940 let resp = futu_proto::qot_get_user_security_group::Response {
7941 ret_type: 0,
7942 ret_msg: None,
7943 err_code: None,
7944 s2c: Some(futu_proto::qot_get_user_security_group::S2c { group_list }),
7945 };
7946 Some(prost::Message::encode_to_vec(&resp))
7947 }
7948}
7949
7950fn system_group_name_to_id(name: &str) -> Option<u32> {
7954 match name {
7955 "全部" | "ALL" | "All" => Some(1000),
7956 "沪深" | "滬深" | "A股" | "CN" => Some(899),
7957 "港股" | "HK" => Some(898),
7958 "美股" | "US" => Some(897),
7959 "持仓" | "持倉" | "Position" => Some(896),
7960 "美股期权" | "美股期權" | "US Options" => Some(895),
7961 "特别关注" | "特別關注" | "Focus" => Some(894),
7962 "港股期权" | "港股期權" | "HK Options" => Some(893),
7963 "期货" | "期貨" | "Futures" => Some(892),
7964 "外汇" | "外匯" | "Forex" => Some(891),
7965 "基金宝" | "基金寶" | "Fund" => Some(890),
7966 "期权" | "期權" | "Options" => Some(889),
7967 "债券" | "債券" | "Bond" => Some(888),
7968 "新加坡" | "SG" => Some(887),
7969 "指数" | "指數" | "Index" => Some(886),
7970 "数字货币" | "數字貨幣" | "Crypto" => Some(885),
7971 _ => None,
7972 }
7973}
7974
7975fn system_group_id_to_name(id: u32, lang: u8) -> Option<&'static str> {
7978 let lang = if lang > 2 { 0 } else { lang };
7980 match (id, lang) {
7981 (1000, 0) => Some("全部"),
7983 (899, 0) => Some("沪深"),
7984 (898, 0) => Some("港股"),
7985 (897, 0) => Some("美股"),
7986 (896, 0) => Some("持仓"),
7987 (895, 0) => Some("美股期权"),
7988 (894, 0) => Some("特别关注"),
7989 (893, 0) => Some("港股期权"),
7990 (892, 0) => Some("期货"),
7991 (891, 0) => Some("外汇"),
7992 (890, 0) => Some("基金宝"),
7993 (889, 0) => Some("期权"),
7994 (888, 0) => Some("债券"),
7995 (887, 0) => Some("新加坡"),
7996 (886, 0) => Some("指数"),
7997 (885, 0) => Some("数字货币"),
7998 (1000, 1) => Some("全部"),
8000 (899, 1) => Some("滬深"),
8001 (898, 1) => Some("港股"),
8002 (897, 1) => Some("美股"),
8003 (896, 1) => Some("持倉"),
8004 (895, 1) => Some("美股期權"),
8005 (894, 1) => Some("特別關注"),
8006 (893, 1) => Some("港股期權"),
8007 (892, 1) => Some("期貨"),
8008 (891, 1) => Some("外匯"),
8009 (890, 1) => Some("基金寶"),
8010 (889, 1) => Some("期權"),
8011 (888, 1) => Some("債券"),
8012 (887, 1) => Some("新加坡"),
8013 (886, 1) => Some("指數"),
8014 (885, 1) => Some("數字貨幣"),
8015 (1000, 2) => Some("All"),
8017 (899, 2) => Some("CN"),
8018 (898, 2) => Some("HK"),
8019 (897, 2) => Some("US"),
8020 (896, 2) => Some("Position"),
8021 (895, 2) => Some("US Options"),
8022 (894, 2) => Some("Focus"),
8023 (893, 2) => Some("HK Options"),
8024 (892, 2) => Some("Futures"),
8025 (891, 2) => Some("Forex"),
8026 (890, 2) => Some("Fund"),
8027 (889, 2) => Some("Options"),
8028 (888, 2) => Some("Bond"),
8029 (887, 2) => Some("SG"),
8030 (886, 2) => Some("Index"),
8031 (885, 2) => Some("Crypto"),
8032 _ => None,
8033 }
8034}
8035
8036struct GetUsedQuotaHandler {
8038 subscriptions: Arc<SubscriptionManager>,
8039 kl_quota_counter: Arc<std::sync::atomic::AtomicU32>,
8040}
8041
8042#[async_trait]
8043impl RequestHandler for GetUsedQuotaHandler {
8044 async fn handle(&self, conn_id: u64, _req: &IncomingRequest) -> Option<Vec<u8>> {
8045 let used_sub_quota = self.subscriptions.get_total_used_quota() as i32;
8046 let used_kl_quota = self
8047 .kl_quota_counter
8048 .load(std::sync::atomic::Ordering::Relaxed) as i32;
8049
8050 tracing::debug!(conn_id, used_sub_quota, used_kl_quota, "GetUsedQuota");
8051
8052 let resp = futu_proto::used_quota::Response {
8053 ret_type: 0,
8054 ret_msg: None,
8055 err_code: None,
8056 s2c: Some(futu_proto::used_quota::S2c {
8057 used_sub_quota: Some(used_sub_quota),
8058 used_k_line_quota: Some(used_kl_quota),
8059 }),
8060 };
8061 Some(prost::Message::encode_to_vec(&resp))
8062 }
8063}