futu_cache/
qot_cache.rs

1// 行情数据缓存
2//
3// 对应 C++ NNDataCenter 中的 INNData_Qot_SecQot / INNData_Qot_KLRT 等
4// 使用 DashMap 实现并发安全的内存缓存
5
6use dashmap::DashMap;
7
8/// 股票行情缓存 key: "market_code" (如 "1_00700")
9pub type SecurityKey = String;
10
11/// 生成缓存 key
12pub fn make_key(market: i32, code: &str) -> SecurityKey {
13    format!("{market}_{code}")
14}
15
16/// 基本报价缓存
17#[derive(Debug, Clone)]
18pub struct CachedBasicQot {
19    pub cur_price: f64,
20    pub open_price: f64,
21    pub high_price: f64,
22    pub low_price: f64,
23    pub last_close_price: f64,
24    pub volume: i64,
25    pub turnover: f64,
26    pub turnover_rate: f64,
27    pub amplitude: f64,
28    pub is_suspended: bool,
29    pub update_time: String,
30    pub update_timestamp: f64,
31}
32
33/// K 线缓存
34#[derive(Debug, Clone)]
35pub struct CachedKLine {
36    pub time: String,
37    pub open_price: f64,
38    pub high_price: f64,
39    pub low_price: f64,
40    pub close_price: f64,
41    pub volume: i64,
42    pub turnover: f64,
43}
44
45/// 摆盘缓存 (对齐 C++ Qot_UpdateOrderBook::S2C)
46#[derive(Debug, Clone, Default)]
47pub struct CachedOrderBook {
48    pub ask_list: Vec<CachedOrderBookLevel>,
49    pub bid_list: Vec<CachedOrderBookLevel>,
50    pub svr_recv_time_bid: Option<String>,
51    pub svr_recv_time_bid_timestamp: Option<f64>,
52    pub svr_recv_time_ask: Option<String>,
53    pub svr_recv_time_ask_timestamp: Option<f64>,
54}
55
56/// 摆盘单层
57#[derive(Debug, Clone)]
58pub struct CachedOrderBookLevel {
59    pub price: f64,
60    pub volume: i64,
61    pub order_count: i32,
62}
63
64/// 逐笔成交缓存 (对齐 C++ Qot_UpdateTicker::S2C)
65#[derive(Debug, Clone)]
66pub struct CachedTicker {
67    pub time: String,
68    pub sequence: i64, // tick_key, 用于去重
69    pub dir: i32,      // 1=Bid/卖盘, 2=Ask/买盘, 3=Neutral
70    pub price: f64,
71    pub volume: i64,
72    pub turnover: f64, // price × volume
73    pub recv_time: Option<f64>,
74    pub ticker_type: Option<i32>,
75    pub push_data_type: Option<i32>,
76    pub timestamp: Option<f64>,
77}
78
79/// 分时数据点
80#[derive(Debug, Clone)]
81pub struct CachedTimeShare {
82    pub time: String,
83    pub minute: i32,
84    pub price: f64,
85    pub last_close_price: f64,
86    pub avg_price: f64,
87    pub volume: i64,
88    pub turnover: f64,
89    pub timestamp: f64,
90}
91
92/// 经纪队列缓存 (对齐 C++ Qot_UpdateBroker::S2C)
93#[derive(Debug, Clone, Default)]
94pub struct CachedBroker {
95    pub bid_list: Vec<CachedBrokerItem>,
96    pub ask_list: Vec<CachedBrokerItem>,
97}
98
99/// 经纪队列单项
100#[derive(Debug, Clone)]
101pub struct CachedBrokerItem {
102    pub id: i64,
103    pub name: String,
104    pub pos: i32,
105}
106
107/// 行情缓存管理器
108pub struct QotCache {
109    /// 基本报价缓存
110    pub basic_qot: DashMap<SecurityKey, CachedBasicQot>,
111    /// K 线缓存: key = "market_code:kl_type"
112    pub klines: DashMap<String, Vec<CachedKLine>>,
113    /// 摆盘缓存
114    pub order_books: DashMap<SecurityKey, CachedOrderBook>,
115    /// 逐笔缓存: 保留最近 N 条
116    pub tickers: DashMap<SecurityKey, Vec<CachedTicker>>,
117    /// 分时缓存
118    pub rt_data: DashMap<SecurityKey, Vec<CachedTimeShare>>,
119    /// 经纪队列缓存
120    pub brokers: DashMap<SecurityKey, CachedBroker>,
121}
122
123impl QotCache {
124    pub fn new() -> Self {
125        Self {
126            basic_qot: DashMap::new(),
127            klines: DashMap::new(),
128            order_books: DashMap::new(),
129            tickers: DashMap::new(),
130            rt_data: DashMap::new(),
131            brokers: DashMap::new(),
132        }
133    }
134
135    /// 更新基本报价
136    pub fn update_basic_qot(&self, key: &str, qot: CachedBasicQot) {
137        self.basic_qot.insert(key.to_string(), qot);
138    }
139
140    /// 获取基本报价
141    pub fn get_basic_qot(&self, key: &str) -> Option<CachedBasicQot> {
142        self.basic_qot.get(key).map(|v| v.clone())
143    }
144
145    /// 更新 K 线
146    pub fn update_klines(&self, key: &str, kl_type: i32, klines: Vec<CachedKLine>) {
147        let cache_key = format!("{key}:{kl_type}");
148        self.klines.insert(cache_key, klines);
149    }
150
151    /// 获取 K 线
152    pub fn get_klines(&self, key: &str, kl_type: i32) -> Option<Vec<CachedKLine>> {
153        let cache_key = format!("{key}:{kl_type}");
154        self.klines.get(&cache_key).map(|v| v.clone())
155    }
156
157    /// 更新摆盘
158    pub fn update_order_book(&self, key: &str, ob: CachedOrderBook) {
159        self.order_books.insert(key.to_string(), ob);
160    }
161
162    /// 追加逐笔(保留最近 1000 条)
163    pub fn append_tickers(&self, key: &str, new_tickers: Vec<CachedTicker>) {
164        let mut entry = self.tickers.entry(key.to_string()).or_default();
165        entry.extend(new_tickers);
166        if entry.len() > 1000 {
167            let drain_count = entry.len() - 1000;
168            entry.drain(..drain_count);
169        }
170    }
171
172    /// 更新经纪队列
173    pub fn update_broker(&self, key: &str, broker: CachedBroker) {
174        self.brokers.insert(key.to_string(), broker);
175    }
176
177    /// 获取经纪队列
178    pub fn get_broker(&self, key: &str) -> Option<CachedBroker> {
179        self.brokers.get(key).map(|v| v.clone())
180    }
181
182    /// 清除指定股票的所有缓存
183    pub fn clear_security(&self, key: &str) {
184        self.basic_qot.remove(key);
185        self.order_books.remove(key);
186        self.tickers.remove(key);
187        self.brokers.remove(key);
188        // K线需要清除所有类型
189        let prefix = format!("{key}:");
190        self.klines.retain(|k, _| !k.starts_with(&prefix));
191    }
192}
193
194impl Default for QotCache {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_basic_qot_cache() {
206        let cache = QotCache::new();
207        let key = make_key(1, "00700");
208
209        assert!(cache.get_basic_qot(&key).is_none());
210
211        cache.update_basic_qot(
212            &key,
213            CachedBasicQot {
214                cur_price: 500.0,
215                open_price: 495.0,
216                high_price: 510.0,
217                low_price: 490.0,
218                last_close_price: 498.0,
219                volume: 10_000_000,
220                turnover: 5_000_000_000.0,
221                turnover_rate: 0.12,
222                amplitude: 4.0,
223                is_suspended: false,
224                update_time: "2026-04-09".to_string(),
225                update_timestamp: 0.0,
226            },
227        );
228
229        let cached = cache.get_basic_qot(&key).unwrap();
230        assert_eq!(cached.cur_price, 500.0);
231    }
232
233    #[test]
234    fn test_ticker_cap() {
235        let cache = QotCache::new();
236        let key = make_key(1, "00700");
237
238        // 追加 1100 条,应保留最近 1000 条
239        let tickers: Vec<_> = (0..1100)
240            .map(|i| CachedTicker {
241                time: format!("t{i}"),
242                sequence: i,
243                price: i as f64,
244                volume: 100,
245                turnover: i as f64 * 100.0,
246                dir: 1,
247                recv_time: None,
248                ticker_type: None,
249                push_data_type: None,
250                timestamp: None,
251            })
252            .collect();
253
254        cache.append_tickers(&key, tickers);
255        let cached = cache.tickers.get(&key).unwrap();
256        assert_eq!(cached.len(), 1000);
257        assert_eq!(cached[0].time, "t100"); // 前 100 条被淘汰
258    }
259}