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