Skip to main content

futu_cache/
trading_session.rs

1use std::collections::VecDeque;
2
3use dashmap::DashMap;
4use futu_domain_qot_calendar::{RangeSelection, TradingDay};
5use parking_lot::Mutex;
6
7use crate::market_event::MarketTradeDateToken;
8
9// Daemon-local safety bounds, not backend protocol limits. Eviction only causes a cache miss and
10// never changes response semantics. Replace with operator configuration if this cache becomes a
11// tunable service; workloads above the bound remain correct but issue more backend reads.
12const SECURITY_CACHE_CAPACITY: usize = 1_024;
13const MARKET_CACHE_CAPACITY: usize = 1_024;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct SecurityTradingSessionCacheKey {
17    pub market_id: u32,
18    pub code: String,
19    pub exchange: String,
20    pub begin_date: Option<i32>,
21    pub end_date: Option<i32>,
22    pub num: Option<i32>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct MarketTradingSessionCacheKey {
27    pub market_id: i32,
28    pub begin_date: Option<i32>,
29    pub end_date: Option<i32>,
30    pub num: Option<i32>,
31}
32
33impl MarketTradingSessionCacheKey {
34    #[must_use]
35    pub fn new(market_id: i32, range: &RangeSelection) -> Self {
36        Self {
37            market_id,
38            begin_date: range.begin_date,
39            end_date: range.end_date,
40            num: range.num,
41        }
42    }
43}
44
45#[derive(Clone)]
46struct CacheEntry {
47    token: MarketTradeDateToken,
48    days: Vec<TradingDay>,
49}
50
51#[derive(Default)]
52pub struct TradingSessionCache {
53    security: DashMap<SecurityTradingSessionCacheKey, CacheEntry>,
54    market: DashMap<MarketTradingSessionCacheKey, CacheEntry>,
55    security_order: Mutex<VecDeque<SecurityTradingSessionCacheKey>>,
56    market_order: Mutex<VecDeque<MarketTradingSessionCacheKey>>,
57}
58
59impl TradingSessionCache {
60    #[must_use]
61    pub fn security(
62        &self,
63        key: &SecurityTradingSessionCacheKey,
64        current_token: MarketTradeDateToken,
65    ) -> Option<Vec<TradingDay>> {
66        let entry = self.security.get(key)?;
67        if key.market_id != current_token.market_id || entry.token != current_token {
68            drop(entry);
69            self.security.remove(key);
70            return None;
71        }
72        Some(entry.days.clone())
73    }
74
75    pub fn insert_security_if_current(
76        &self,
77        key: SecurityTradingSessionCacheKey,
78        token: MarketTradeDateToken,
79        days: Vec<TradingDay>,
80    ) -> bool {
81        if key.market_id != token.market_id {
82            return false;
83        }
84        let mut order = self.security_order.lock();
85        if !self.security.contains_key(&key) {
86            while order.len() >= SECURITY_CACHE_CAPACITY {
87                if let Some(evicted) = order.pop_front() {
88                    self.security.remove(&evicted);
89                }
90            }
91            order.push_back(key.clone());
92        }
93        self.security.insert(key, CacheEntry { token, days });
94        true
95    }
96
97    #[must_use]
98    pub fn market(
99        &self,
100        key: &MarketTradingSessionCacheKey,
101        current_token: MarketTradeDateToken,
102    ) -> Option<Vec<TradingDay>> {
103        let entry = self.market.get(key)?;
104        if u32::try_from(key.market_id).ok() != Some(current_token.market_id)
105            || entry.token != current_token
106        {
107            drop(entry);
108            self.market.remove(key);
109            return None;
110        }
111        Some(entry.days.clone())
112    }
113
114    pub fn insert_market_if_current(
115        &self,
116        key: MarketTradingSessionCacheKey,
117        token: MarketTradeDateToken,
118        days: Vec<TradingDay>,
119    ) -> bool {
120        if u32::try_from(key.market_id).ok() != Some(token.market_id) {
121            return false;
122        }
123        let mut order = self.market_order.lock();
124        if !self.market.contains_key(&key) {
125            while order.len() >= MARKET_CACHE_CAPACITY {
126                if let Some(evicted) = order.pop_front() {
127                    self.market.remove(&evicted);
128                }
129            }
130            order.push_back(key.clone());
131        }
132        self.market.insert(key, CacheEntry { token, days });
133        true
134    }
135
136    pub fn invalidate_market(&self, market_id: u32) {
137        self.security.retain(|key, _| key.market_id != market_id);
138        if let Ok(market_id) = i32::try_from(market_id) {
139            self.market.retain(|key, _| key.market_id != market_id);
140        }
141        self.security_order
142            .lock()
143            .retain(|key| key.market_id != market_id);
144        if let Ok(market_id) = i32::try_from(market_id) {
145            self.market_order
146                .lock()
147                .retain(|key| key.market_id != market_id);
148        }
149    }
150
151    #[must_use]
152    pub fn entry_counts(&self) -> (usize, usize) {
153        (self.security.len(), self.market.len())
154    }
155}