Skip to main content

futu_server/
push.rs

1// 推送分发:三种推送模式
2
3use std::collections::HashMap;
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9use bytes::Bytes;
10use dashmap::DashMap;
11use futu_auth::Scope;
12use futu_codec::frame::FutuFrame;
13use tokio::sync::mpsc::error::TrySendError;
14
15use crate::conn::ClientConn;
16use crate::metrics::GatewayMetrics;
17use crate::subscription::SubscriptionManager;
18
19mod kline_delivery;
20use kline_delivery::{KlineCursorKey, KlinePushCursor, RegisteredKlinePushCursor};
21
22/// **防御深度**:即使客户端在订阅阶段某种方式绕过了 scope gate,推送时
23/// 再按 client key 的 scope 过滤一次。
24///
25/// `conn.scopes` 语义:
26/// - **空集** → legacy 模式(TCP listener / WS 未配 keys.json),全放行
27/// - 非空集 → scope 模式,必须包含 `needed` 才推
28fn should_push_to(conn: &ClientConn, needed: Scope, event_label: &str) -> bool {
29    if conn.scopes.is_empty() {
30        return true; // legacy 全放行
31    }
32    if conn.scopes.contains(&needed) {
33        return true;
34    }
35    // 过滤掉 —— 记 metrics 便于运维发现"谁订阅了但 scope 不够"这种配置问题
36    let key_id = conn.key_id.as_deref().unwrap_or("<none>");
37    futu_auth::metrics::bump_ws_filtered(event_label, key_id);
38    false
39}
40
41/// 外部推送接收器 trait
42///
43/// 允许外部模块(如 REST WebSocket)接收推送事件,
44/// 不引入模块间循环依赖。
45///
46/// **v1.4.106 codex 1131 F4 [P1]**: `on_quote_push` 加 `rehab_type` 参数. KL 类
47/// push 走非 0 rehab (forward / backward / 无), 其它 sub_type 走 0. Sink 实装
48/// 应用 (sec_key, sub_type, rehab_type) 三元过滤 push 接收方 — 不再 broadcast
49/// 给所有 quote-scope conn (老行为是 silent leak: 仅订未 RegPush 的 conn 也收
50/// 到 quote push, 违反 C++ `QotSubscribe::GetPushConn` 三元 key 路由).
51pub trait ExternalPushSink: Send + Sync {
52    /// 行情推送 (rehab_type=0 for non-KL).
53    fn on_quote_push(
54        &self,
55        sec_key: &str,
56        sub_type: i32,
57        rehab_type: i32,
58        proto_id: u32,
59        body: &[u8],
60    );
61    /// 广播推送 (到价提醒、系统通知等)
62    fn on_broadcast_push(&self, proto_id: u32, body: &[u8]);
63    /// UID-bound private user push. The public body has already removed UID.
64    fn on_private_user_push(&self, _proto_id: u32, _body: &[u8]) {}
65    /// 交易推送 (订单更新、成交更新等).
66    ///
67    /// `trd_market` 是 PushDispatcher 一次性 decode `body` 提取的
68    /// `s2c.header.trd_market` 大写字符串 ("HK" / "US" / "CN" / ...), 为
69    /// 4 surface (gRPC / REST WS / MCP) 复用避免各自 decode. 老 sink 实现可
70    /// 忽略此参数 (只看 acc_id + proto_id + body), Layer 3 (allowed_markets)
71    /// filter 直接从这里取 — 见 [`extract_trd_market_from_trade_body`].
72    ///
73    /// `None` = decode 失败 / proto_id 不识别 / market enum unknown — 老
74    /// 路径下游应**不 trigger Layer 3 drop** (向后兼容 — pitfall #57
75    /// backend-semantic 未真机验证前 default OFF behavior).
76    fn on_trade_push(&self, acc_id: u64, proto_id: u32, body: &[u8], trd_market: Option<&str>);
77}
78
79/// v1.4.105 D3 (Phase 4) T-B: trade push body decode → trd_market 提取.
80///
81/// 4 surface (gRPC / REST WS / raw TCP WS / MCP) 共用同一 helper 而非各自
82/// decode 一次, 避免 mapping 漂移 (与 futu-auth-pipeline::body_aware /
83/// futu-rest::trd::trd_market_str 一致, 但本 crate 不能跨 dep 复用所以重复
84/// 一份 — 跨 crate mismatch 会被 cross_surface_smoke 抓出).
85///
86/// caller (PushDispatcher) 在分发到 sink 前**只 decode 一次**, 把字符串塞
87/// PushEventCtx.event_trd_market 让 TradePushFilter Layer 3 用
88/// allowed_markets 校验.
89///
90/// 不识别 / decode 失败 / market enum unknown → None (Layer 3 不 trigger).
91///
92/// UNVERIFIED — 真机 verify 跨 market 推送流 (HK + US 双账户) 后才能升
93/// confidence (per pitfall #57 backend-semantic risk).
94#[must_use]
95pub fn extract_trd_market_from_trade_body(proto_id: u32, body: &[u8]) -> Option<&'static str> {
96    use prost::Message;
97    let market_int = match proto_id {
98        // TRD_UPDATE_ORDER (2208) → Trd_UpdateOrder.Response.s2c.header.trd_market
99        2208 => {
100            let resp = match futu_proto::trd_update_order::Response::decode(body) {
101                Ok(resp) => resp,
102                Err(e) => {
103                    tracing::debug!(
104                        proto_id,
105                        body_len = body.len(),
106                        error = %e,
107                        "trade push body decode failed while extracting trd_market"
108                    );
109                    return None;
110                }
111            };
112            resp.s2c?.header.trd_market
113        }
114        // TRD_UPDATE_ORDER_FILL (2218) → Trd_UpdateOrderFill.Response.s2c.header.trd_market
115        2218 => {
116            let resp = match futu_proto::trd_update_order_fill::Response::decode(body) {
117                Ok(resp) => resp,
118                Err(e) => {
119                    tracing::debug!(
120                        proto_id,
121                        body_len = body.len(),
122                        error = %e,
123                        "trade push body decode failed while extracting trd_market"
124                    );
125                    return None;
126                }
127            };
128            resp.s2c?.header.trd_market
129        }
130        // 未知 trade push proto_id → 不识别, 让 Layer 3 不 trigger
131        _ => return None,
132    };
133    // Trd_Common.TrdMarket enum int → 大写字符串. 与 futu-rest::trd::trd_market_str
134    // / futu-auth-pipeline::body_aware::trd_market_str 一致.
135    match market_int {
136        1 => Some("HK"),
137        2 => Some("US"),
138        3 => Some("CN"),
139        4 => Some("HKCC"),
140        5 => Some("FUTURES"),
141        6 => Some("SG"),
142        7 => Some("CRYPTO"),
143        8 => Some("AU"),
144        10 => Some("FUTURES_SIMULATE_HK"),
145        11 => Some("FUTURES_SIMULATE_US"),
146        12 => Some("FUTURES_SIMULATE_SG"),
147        13 => Some("FUTURES_SIMULATE_JP"),
148        15 => Some("JP"),
149        111 => Some("MY"),
150        112 => Some("CA"),
151        113 => Some("HKFUND"),
152        123 => Some("USFUND"),
153        124 => Some("SGFUND"),
154        125 => Some("MYFUND"),
155        126 => Some("JPFUND"),
156        _ => None,
157    }
158}
159
160/// 推送分发器
161pub struct PushDispatcher {
162    connections: Arc<DashMap<u64, ClientConn>>,
163    subscriptions: Arc<SubscriptionManager>,
164    metrics: Option<Arc<GatewayMetrics>>,
165    /// C++ `APIServerCS_Core.cpp:246-247` assigns a monotonic push serial
166    /// number before sending each client push frame. It is only used by
167    /// clients/CS reconciliation to identify push packets, not for backend
168    /// replay.
169    push_serial_no: AtomicU32,
170    /// Per-connection EventContract push cursors.
171    ///
172    /// Frozen C++ keeps these in `IQotLastPushRecord` plus the
173    /// `APIServer_Qot_EventContractPush` ticker/K-line maps. They cannot live
174    /// in the shared quote cache because first push advances only the target
175    /// connection's ticker cursor.
176    event_contract_cursors: parking_lot::Mutex<EventContractPushCursors>,
177    /// Ordinary KLine last-push state shared by first-push and live delivery.
178    /// Physical connection generation is part of the key, so a replacement
179    /// socket cannot inherit the prior socket's cursor. The cursor value also
180    /// owns the current push-registration intent epoch, so unregister followed
181    /// by re-register receives one new first-push without weakening per-intent
182    /// duplicate suppression.
183    kline_cursors: Arc<parking_lot::Mutex<HashMap<KlineCursorKey, RegisteredKlinePushCursor>>>,
184    /// Canonical ordinary KLine cursor for broadcast-style external sinks.
185    /// Native first/live delivery remains independently per connection.
186    external_kline_cursors: parking_lot::Mutex<HashMap<(String, i32, i32), KlinePushCursor>>,
187    /// 外部推送接收器列表 (REST WebSocket, gRPC 等)
188    external_sinks: Vec<Arc<dyn ExternalPushSink>>,
189    startup_readiness: crate::identity::StartupReadiness,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
193struct EventContractKlineCursor {
194    time_key: String,
195    fingerprint: u64,
196}
197
198#[derive(Default)]
199struct EventContractPushCursors {
200    order_book_hashes: HashMap<(u64, String), u64>,
201    ticker_sequences: HashMap<(u64, String), u64>,
202    kline_points: HashMap<(u64, String, i32, i32), EventContractKlineCursor>,
203}
204
205type EventContractKlineUpdates = Vec<(i32, EventContractKlineCursor)>;
206
207enum EventContractCursorUpdate {
208    OrderBook(u64),
209    Ticker(u64),
210    Kline(EventContractKlineUpdates),
211}
212
213impl PushDispatcher {
214    /// 创建推送分发器。`connections` 和 `subscriptions` 由
215    /// [`super::listener::ApiServer`] 共享;外部 sink / metrics 可通过
216    /// [`Self::with_metrics`] / [`Self::with_external_sink`] 后续注入。
217    pub fn new(
218        connections: Arc<DashMap<u64, ClientConn>>,
219        subscriptions: Arc<SubscriptionManager>,
220    ) -> Self {
221        let kline_cursors = Arc::new(parking_lot::Mutex::new(HashMap::<
222            KlineCursorKey,
223            RegisteredKlinePushCursor,
224        >::new()));
225        let cursor_cleanup = Arc::clone(&kline_cursors);
226        subscriptions.register_disconnect_observer(Arc::new(move |conn_id| {
227            cursor_cleanup
228                .lock()
229                .retain(|key, _| key.conn_id != conn_id);
230        }));
231        Self {
232            connections,
233            subscriptions,
234            metrics: None,
235            push_serial_no: AtomicU32::new(0),
236            event_contract_cursors: parking_lot::Mutex::new(EventContractPushCursors::default()),
237            kline_cursors,
238            external_kline_cursors: parking_lot::Mutex::new(HashMap::new()),
239            external_sinks: Vec::new(),
240            startup_readiness: crate::identity::StartupReadiness::default(),
241        }
242    }
243
244    /// 设置监控指标引用
245    pub fn with_metrics(mut self, metrics: Arc<GatewayMetrics>) -> Self {
246        self.metrics = Some(metrics);
247        self
248    }
249
250    /// 添加外部推送接收器(可多次调用注册多个)
251    pub fn with_external_sink(mut self, sink: Arc<dyn ExternalPushSink>) -> Self {
252        self.external_sinks.push(sink);
253        self
254    }
255
256    pub fn with_startup_readiness(
257        mut self,
258        startup_readiness: crate::identity::StartupReadiness,
259    ) -> Self {
260        self.startup_readiness = startup_readiness;
261        self
262    }
263
264    fn delivery_ready(&self) -> bool {
265        self.startup_readiness.snapshot().state == crate::identity::StartupState::Ready
266    }
267
268    fn record_push(&self) {
269        if let Some(ref m) = self.metrics {
270            m.client_pushes_sent
271                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
272        }
273    }
274
275    fn record_push_send_failure(&self) {
276        if let Some(ref m) = self.metrics {
277            m.client_push_send_failures
278                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
279        }
280    }
281
282    fn record_ordinary_client_backpressure_disconnect(&self) {
283        if let Some(ref m) = self.metrics {
284            m.ordinary_client_push_backpressure_disconnects
285                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
286        }
287    }
288
289    fn record_qot_client_backpressure_drop(&self, sub_type: i32) {
290        if let Some(ref m) = self.metrics {
291            m.record_qot_client_push_backpressure_drop(sub_type);
292        }
293    }
294
295    fn next_push_serial_no(&self) -> u32 {
296        self.push_serial_no
297            .fetch_add(1, Ordering::Relaxed)
298            .wrapping_add(1)
299    }
300
301    fn try_send_ordinary_client_frame(
302        &self,
303        conn_id: u64,
304        tx: tokio::sync::mpsc::Sender<FutuFrame>,
305        frame: FutuFrame,
306        push_path: &'static str,
307    ) {
308        match tx.try_send(frame) {
309            Ok(()) => self.record_push(),
310            Err(TrySendError::Full(_frame)) => {
311                match self.subscriptions.request_client_close(conn_id) {
312                    Some(true) => {
313                        self.record_ordinary_client_backpressure_disconnect();
314                        tracing::warn!(
315                            conn_id,
316                            push_path,
317                            "ordinary client push queue is full; closing slow connection"
318                        );
319                    }
320                    Some(false) => {}
321                    None => {
322                        let removed = self.connections.remove(&conn_id).is_some();
323                        self.subscriptions.on_disconnect(conn_id);
324                        if removed {
325                            self.record_ordinary_client_backpressure_disconnect();
326                        }
327                        tracing::error!(
328                            conn_id,
329                            push_path,
330                            removed,
331                            "ordinary client push queue is full without registered close control; removed connection fail-closed"
332                        );
333                    }
334                }
335            }
336            Err(TrySendError::Closed(_frame)) => {
337                self.record_push_send_failure();
338                tracing::warn!(
339                    conn_id,
340                    push_path,
341                    "client push send failed because downstream channel is closed"
342                );
343            }
344        }
345    }
346
347    fn try_send_qot_client_frame(
348        &self,
349        tx: tokio::sync::mpsc::Sender<FutuFrame>,
350        frame: FutuFrame,
351        sub_type: i32,
352        push_path: &'static str,
353    ) -> bool {
354        match tx.try_send(frame) {
355            Ok(()) => {
356                self.record_push();
357                true
358            }
359            Err(TrySendError::Full(_frame)) => {
360                self.record_qot_client_backpressure_drop(sub_type);
361                tracing::warn!(
362                    push_path,
363                    sub_type,
364                    "client quote push dropped because downstream channel is full"
365                );
366                false
367            }
368            Err(TrySendError::Closed(_frame)) => {
369                self.record_push_send_failure();
370                tracing::warn!(
371                    push_path,
372                    "client quote push send failed because downstream channel is closed"
373                );
374                false
375            }
376        }
377    }
378
379    /// 向指定连接推送(自动处理 AES 加密)
380    pub async fn push_to_conn(&self, conn_id: u64, proto_id: u32, body: Vec<u8>) {
381        if !self.delivery_ready() {
382            return;
383        }
384        let push = self.connections.get(&conn_id).map(|conn| {
385            let frame = conn.make_frame(proto_id, self.next_push_serial_no(), Bytes::from(body));
386            (conn.tx.clone(), frame)
387        });
388        if let Some((tx, frame)) = push {
389            self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_to_conn");
390        }
391    }
392
393    /// Direct async delivery; generation is checked in the physical tx lookup.
394    pub async fn push_qot_to_conn_generation(
395        &self,
396        conn_id: u64,
397        expected_generation: u64,
398        proto_id: u32,
399        body: Vec<u8>,
400    ) {
401        if !self.delivery_ready() {
402            return;
403        }
404        let push = self.connections.get(&conn_id).and_then(|conn| {
405            if conn.session_generation != expected_generation
406                || !should_push_to(&conn, Scope::QotRead, "indicator_direct")
407            {
408                return None;
409            }
410            let frame = conn.make_frame(proto_id, self.next_push_serial_no(), Bytes::from(body));
411            Some((conn.tx.clone(), frame))
412        });
413        if let Some((tx, frame)) = push {
414            self.try_send_qot_client_frame(tx, frame, 0, "push_qot_to_conn_generation");
415        }
416    }
417
418    /// 向所有订阅了通知的连接广播(每个连接独立 AES 加密)
419    pub async fn push_notify(&self, proto_id: u32, body: Vec<u8>) {
420        if !self.delivery_ready() {
421            return;
422        }
423        let body = Bytes::from(body);
424        let body_sha1 = FutuFrame::body_sha1(&body);
425        let pushes: Vec<_> = self
426            .connections
427            .iter()
428            .filter_map(|entry| {
429                let conn = entry.value();
430                if !conn.recv_notify {
431                    return None;
432                }
433                // 防御深度:订阅阶段应该已经挡了 qot:read 外的 key,这里再过滤一次
434                if !should_push_to(conn, Scope::QotRead, "notify") {
435                    return None;
436                }
437                let serial_no = self.next_push_serial_no();
438                let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
439                Some((conn.conn_id, conn.tx.clone(), frame))
440            })
441            .collect();
442        for (conn_id, tx, frame) in pushes {
443            self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_notify");
444        }
445    }
446
447    /// 向订阅了指定交易账户的所有连接推送
448    pub async fn push_trd_acc(&self, acc_id: u64, proto_id: u32, body: Vec<u8>) {
449        if !self.delivery_ready() {
450            return;
451        }
452        // v1.4.105 D3 (Phase 4) T-B4: 一次 decode 提取 trd_market 给 sinks
453        // 共用. 4 surface (gRPC / REST WS / 等) 复用同一字符串避免重复 decode.
454        let trd_market = extract_trd_market_from_trade_body(proto_id, &body);
455        // 同时推送给外部接收器 (REST WebSocket, gRPC 等)
456        for sink in &self.external_sinks {
457            sink.on_trade_push(acc_id, proto_id, &body, trd_market);
458        }
459        let body = Bytes::from(body);
460        let body_sha1 = FutuFrame::body_sha1(&body);
461        let subscribers = self.subscriptions.get_acc_subscribers(acc_id);
462        let pushes: Vec<_> = subscribers
463            .into_iter()
464            .filter_map(|conn_id| {
465                let conn = self.connections.get(&conn_id)?;
466                // 防御深度:trade push 要求 acc:read
467                if !should_push_to(&conn, Scope::AccRead, "trade") {
468                    return None;
469                }
470                // codex round 1 F4 (P2) v1.4.105: Layer 1 — caller key
471                // allowed_acc_ids push-time 硬过滤. 防 stale subscription /
472                // KeyRecord reload 后 acc 范围窄化 / 历史 bug 留下的 conn→acc
473                // 关系 让受限 key 仍收到非授权 acc 的 trade push.
474                //
475                // 设计同 futu-auth::Limits / KeyRecord:
476                // - allowed_acc_ids None = 无限制 (legacy / unrestricted key) → 放行
477                // - 非空 set + acc_id ∉ set → drop + metric
478                // - 空 set = 无限制 (向后兼容); deny-all 用 sentinel {0}
479                if let Some(allowed_accs) = conn.allowed_acc_ids.as_ref()
480                    && !allowed_accs.is_empty()
481                    && !allowed_accs.contains(&acc_id)
482                {
483                    let key_id = conn.key_id.as_deref().unwrap_or("<none>");
484                    futu_auth::metrics::bump_ws_filtered("trade_acc_id", key_id);
485                    return None;
486                }
487                // v1.4.105 D3 (Phase 4) T-B2: Layer 3 — caller key allowed_markets
488                // 限制. trd_market None (decode 失败 / market 未知) → 不 trigger
489                // drop (向后兼容 — pitfall #57 backend-semantic 未真机 verify
490                // 前 default 不 drop, 防 false-negative 错过用户合法 push).
491                // allowed_markets None / 空 set = 无限制.
492                if let (Some(market), Some(allowed_mkts)) =
493                    (trd_market, conn.allowed_markets.as_ref())
494                    && !allowed_mkts.is_empty()
495                    && !allowed_mkts.contains(market)
496                {
497                    let key_id = conn.key_id.as_deref().unwrap_or("<none>");
498                    futu_auth::metrics::bump_ws_filtered("trade_market", key_id);
499                    return None;
500                }
501                let serial_no = self.next_push_serial_no();
502                let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
503                Some((conn.conn_id, conn.tx.clone(), frame))
504            })
505            .collect();
506        for (conn_id, tx, frame) in pushes {
507            self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_trd_acc");
508        }
509    }
510
511    /// 向所有已连接的客户端广播(到价提醒等,不需要订阅通知)
512    /// C++ 检查 IsConnSubRecvNotify,对齐使用 InitConnect.recvNotify。
513    pub async fn push_broadcast(&self, proto_id: u32, body: Vec<u8>) {
514        if !self.delivery_ready() {
515            return;
516        }
517        // 同时推送给外部接收器 (REST WebSocket, gRPC 等)
518        for sink in &self.external_sinks {
519            sink.on_broadcast_push(proto_id, &body);
520        }
521        let body = Bytes::from(body);
522        let body_sha1 = FutuFrame::body_sha1(&body);
523        let pushes: Vec<_> = self
524            .connections
525            .iter()
526            .filter_map(|entry| {
527                let conn = entry.value();
528                if !conn.recv_notify {
529                    return None;
530                }
531                if !should_push_to(conn, Scope::QotRead, "broadcast") {
532                    return None;
533                }
534                let serial_no = self.next_push_serial_no();
535                let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
536                Some((conn.conn_id, conn.tx.clone(), frame))
537            })
538            .collect();
539        for (conn_id, tx, frame) in pushes {
540            self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_broadcast");
541        }
542    }
543
544    pub fn push_private_user(&self, proto_id: u32, body: Vec<u8>) {
545        if !self.delivery_ready() {
546            return;
547        }
548        for sink in &self.external_sinks {
549            sink.on_private_user_push(proto_id, &body);
550        }
551        let body = Bytes::from(body);
552        let body_sha1 = FutuFrame::body_sha1(&body);
553        let pushes: Vec<_> = self
554            .connections
555            .iter()
556            .filter_map(|entry| {
557                let conn = entry.value();
558                if !conn.recv_notify
559                    || conn.key_id.is_none()
560                    || !conn.scopes.contains(&Scope::AccRead)
561                {
562                    return None;
563                }
564                let serial_no = self.next_push_serial_no();
565                let frame = conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
566                Some((conn.conn_id, conn.tx.clone(), frame))
567            })
568            .collect();
569        for (conn_id, tx, frame) in pushes {
570            self.try_send_ordinary_client_frame(conn_id, tx, frame, "push_private_user");
571        }
572    }
573
574    fn push_event_contract_qot(
575        &self,
576        security_key: &str,
577        sub_type: i32,
578        rehab_type: i32,
579        proto_id: u32,
580        body: &[u8],
581    ) {
582        self.prune_event_contract_cursors();
583        let subscribers = self.subscriptions.get_qot_push_subscribers_by_cache_key(
584            security_key,
585            sub_type,
586            rehab_type,
587        );
588        for conn_id in subscribers {
589            let Some(conn) = self.connections.get(&conn_id) else {
590                continue;
591            };
592            if !should_push_to(&conn, Scope::QotRead, "quote") {
593                continue;
594            }
595            let decision =
596                self.event_contract_body_for_conn(conn_id, security_key, sub_type, proto_id, body);
597            let Some((body, update)) = decision else {
598                continue;
599            };
600            let body = Bytes::from(body);
601            let frame = conn.make_frame_with_sha1(
602                proto_id,
603                self.next_push_serial_no(),
604                body.clone(),
605                FutuFrame::body_sha1(&body),
606            );
607            let tx = conn.tx.clone();
608            drop(conn);
609            if self.try_send_qot_client_frame(tx, frame, sub_type, "push_event_contract_qot") {
610                self.commit_event_contract_cursor(conn_id, security_key, sub_type, update);
611            }
612        }
613    }
614
615    fn prune_event_contract_cursors(&self) {
616        let mut cursors = self.event_contract_cursors.lock();
617        cursors
618            .order_book_hashes
619            .retain(|(conn_id, _), _| self.connections.contains_key(conn_id));
620        cursors
621            .ticker_sequences
622            .retain(|(conn_id, _), _| self.connections.contains_key(conn_id));
623        cursors
624            .kline_points
625            .retain(|(conn_id, _, _, _), _| self.connections.contains_key(conn_id));
626    }
627
628    fn event_contract_body_for_conn(
629        &self,
630        conn_id: u64,
631        security_key: &str,
632        sub_type: i32,
633        proto_id: u32,
634        body: &[u8],
635    ) -> Option<(Vec<u8>, EventContractCursorUpdate)> {
636        match proto_id {
637            futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_ORDER_BOOK => {
638                let hash = push_body_hash(body);
639                let repeated = self
640                    .event_contract_cursors
641                    .lock()
642                    .order_book_hashes
643                    .get(&(conn_id, security_key.to_owned()))
644                    .is_some_and(|previous| *previous == hash);
645                (!repeated).then(|| (body.to_vec(), EventContractCursorUpdate::OrderBook(hash)))
646            }
647            futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_TICKER => {
648                let previous = self
649                    .event_contract_cursors
650                    .lock()
651                    .ticker_sequences
652                    .get(&(conn_id, security_key.to_owned()))
653                    .copied()
654                    .unwrap_or(0);
655                filter_event_contract_ticker_body(body, previous)
656                    .map(|(body, sequence)| (body, EventContractCursorUpdate::Ticker(sequence)))
657            }
658            futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_KLINE => {
659                let cursors = self.event_contract_cursors.lock();
660                filter_event_contract_kline_body(body, |direction| {
661                    cursors
662                        .kline_points
663                        .get(&(conn_id, security_key.to_owned(), sub_type, direction))
664                        .cloned()
665                })
666                .map(|(body, updates)| (body, EventContractCursorUpdate::Kline(updates)))
667            }
668            _ => None,
669        }
670    }
671
672    fn commit_event_contract_cursor(
673        &self,
674        conn_id: u64,
675        security_key: &str,
676        sub_type: i32,
677        update: EventContractCursorUpdate,
678    ) {
679        let mut cursors = self.event_contract_cursors.lock();
680        match update {
681            EventContractCursorUpdate::OrderBook(hash) => {
682                cursors
683                    .order_book_hashes
684                    .insert((conn_id, security_key.to_owned()), hash);
685            }
686            EventContractCursorUpdate::Ticker(sequence) => {
687                cursors
688                    .ticker_sequences
689                    .entry((conn_id, security_key.to_owned()))
690                    .and_modify(|current| *current = (*current).max(sequence))
691                    .or_insert(sequence);
692            }
693            EventContractCursorUpdate::Kline(updates) => {
694                for (direction, cursor) in updates {
695                    cursors.kline_points.insert(
696                        (conn_id, security_key.to_owned(), sub_type, direction),
697                        cursor,
698                    );
699                }
700            }
701        }
702    }
703}
704
705fn push_body_hash(body: &[u8]) -> u64 {
706    let mut hasher = DefaultHasher::new();
707    body.hash(&mut hasher);
708    hasher.finish()
709}
710
711fn event_contract_ticker_cursor_from_body(proto_id: u32, body: &[u8]) -> Option<(String, u64)> {
712    use prost::Message;
713
714    if proto_id != futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_TICKER {
715        return None;
716    }
717    let response = futu_proto::qot_update_event_contract_ticker::Response::decode(body).ok()?;
718    let item = response.s2c?.ticker_list.into_iter().next()?;
719    let sequence = item
720        .ticker_list
721        .iter()
722        .filter_map(|point| point.sequence.as_deref()?.parse::<u64>().ok())
723        .max()?;
724    Some((format!("{}_{}", item.code.market, item.code.code), sequence))
725}
726
727fn filter_event_contract_ticker_body(body: &[u8], previous: u64) -> Option<(Vec<u8>, u64)> {
728    use prost::Message;
729
730    let mut response = futu_proto::qot_update_event_contract_ticker::Response::decode(body).ok()?;
731    let s2c = response.s2c.as_mut()?;
732    let mut newest = previous;
733    for item in &mut s2c.ticker_list {
734        item.ticker_list.retain(|point| {
735            let Some(sequence) = point
736                .sequence
737                .as_deref()
738                .and_then(|value| value.parse::<u64>().ok())
739            else {
740                return false;
741            };
742            if sequence <= previous {
743                return false;
744            }
745            newest = newest.max(sequence);
746            true
747        });
748    }
749    s2c.ticker_list.retain(|item| !item.ticker_list.is_empty());
750    (!s2c.ticker_list.is_empty()).then(|| (response.encode_to_vec(), newest))
751}
752
753fn filter_event_contract_kline_body(
754    body: &[u8],
755    mut previous_for_direction: impl FnMut(i32) -> Option<EventContractKlineCursor>,
756) -> Option<(Vec<u8>, EventContractKlineUpdates)> {
757    use prost::Message;
758
759    let mut response = futu_proto::qot_update_event_contract_kline::Response::decode(body).ok()?;
760    let s2c = response.s2c.as_mut()?;
761    let mut updates = Vec::new();
762    for item in &mut s2c.kline_list {
763        let direction = item.pre_side.unwrap_or(0);
764        let mut cursor = previous_for_direction(direction);
765        if item.kline_list.is_empty() {
766            let fingerprint = push_body_hash(&item.encode_to_vec());
767            if cursor
768                .as_ref()
769                .is_some_and(|previous| previous.fingerprint == fingerprint)
770            {
771                continue;
772            }
773            updates.push((
774                direction,
775                EventContractKlineCursor {
776                    time_key: cursor
777                        .as_ref()
778                        .map(|previous| previous.time_key.clone())
779                        .unwrap_or_default(),
780                    fingerprint,
781                },
782            ));
783            continue;
784        }
785        item.kline_list.retain(|point| {
786            let fingerprint = push_body_hash(&point.encode_to_vec());
787            if cursor.as_ref().is_some_and(|previous| {
788                previous.time_key > point.time_key
789                    || (previous.time_key == point.time_key && previous.fingerprint == fingerprint)
790            }) {
791                return false;
792            }
793            cursor = Some(EventContractKlineCursor {
794                time_key: point.time_key.clone(),
795                fingerprint,
796            });
797            true
798        });
799        if let Some(cursor) = cursor
800            && !item.kline_list.is_empty()
801        {
802            updates.push((direction, cursor));
803        }
804    }
805    s2c.kline_list.retain(|item| {
806        !item.kline_list.is_empty()
807            || updates
808                .iter()
809                .any(|(direction, _)| *direction == item.pre_side.unwrap_or(0))
810    });
811    (!s2c.kline_list.is_empty() && !updates.is_empty()).then(|| (response.encode_to_vec(), updates))
812}
813
814#[cfg(test)]
815mod tests;