1use futu_core::qot_stock_key::QotSecurityKey;
2use futu_domain_qot_klrt::KlineAggregateSession;
3
4use super::QotCache;
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct CachedKLine {
9 pub time: String,
10 pub is_blank: bool,
11 pub open_price: f64,
12 pub high_price: f64,
13 pub low_price: f64,
14 pub close_price: f64,
15 pub last_close_price: f64,
16 pub volume: i64,
17 pub hp_volume: f64,
18 pub turnover: f64,
19 pub turnover_rate: f64,
20 pub pe: f64,
21 pub timestamp: f64,
22 pub is_replenish: Option<bool>,
24 pub direction: Option<i32>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct KlineDims {
35 pub rehab: i32,
36 pub kl_type: i32,
37 pub session: KlineAggregateSession,
38}
39
40impl KlineDims {
41 #[must_use]
42 pub const fn new(rehab: i32, kl_type: i32, session: KlineAggregateSession) -> Self {
43 Self {
44 rehab,
45 kl_type,
46 session,
47 }
48 }
49}
50
51impl QotCache {
52 const KLINE_PUSH_CACHE_MAX_POINTS: usize = 2000;
53 const EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION: usize = 1000;
54
55 pub fn make_kline_key_by_dims(sec_key: &str, dims: KlineDims) -> String {
71 format!(
72 "{sec_key}:r{}:k{}:s{}",
73 dims.rehab,
74 dims.kl_type,
75 dims.session.key_code()
76 )
77 }
78
79 pub fn make_kline_key(
80 sec_key: &str,
81 rehab: i32,
82 kl_type: i32,
83 session: KlineAggregateSession,
84 ) -> String {
85 Self::make_kline_key_by_dims(sec_key, KlineDims::new(rehab, kl_type, session))
86 }
87
88 pub fn update_klines_by_dims(&self, sec_key: &str, dims: KlineDims, klines: Vec<CachedKLine>) {
90 let _owner = self.kline_data_lock.write();
91 self.update_klines_by_dims_unlocked(sec_key, dims, klines);
92 }
93
94 fn update_klines_by_dims_unlocked(
95 &self,
96 sec_key: &str,
97 dims: KlineDims,
98 mut klines: Vec<CachedKLine>,
99 ) {
100 fill_kline_last_close_like_cpp(&mut klines);
101 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
102 self.klines.insert(cache_key, klines);
103 }
104
105 pub fn update_klines(
106 &self,
107 sec_key: &str,
108 rehab: i32,
109 kl_type: i32,
110 session: KlineAggregateSession,
111 klines: Vec<CachedKLine>,
112 ) {
113 self.update_klines_by_dims(sec_key, KlineDims::new(rehab, kl_type, session), klines);
114 }
115
116 pub fn upsert_kline_push_point(
123 &self,
124 sec_key: &str,
125 dims: KlineDims,
126 incoming: CachedKLine,
127 ) -> Option<CachedKLine> {
128 let _owner = self.kline_data_lock.write();
129 self.upsert_kline_push_point_unlocked(sec_key, dims, incoming)
130 }
131
132 fn upsert_kline_push_point_unlocked(
133 &self,
134 sec_key: &str,
135 dims: KlineDims,
136 mut incoming: CachedKLine,
137 ) -> Option<CachedKLine> {
138 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
139 let mut bucket = self.klines.entry(cache_key).or_default();
140
141 if let Some(index) = bucket
142 .iter()
143 .position(|point| point.timestamp == incoming.timestamp)
144 {
145 if incoming.hp_volume < bucket[index].hp_volume
146 || ordinary_kline_content_eq_like_cpp(&incoming, &bucket[index])
147 {
148 return None;
149 }
150 incoming.last_close_price = bucket[index].last_close_price;
151 let close_price = incoming.close_price;
152 bucket[index] = incoming;
153 if let Some(next) = bucket.get_mut(index + 1) {
154 next.last_close_price = close_price;
155 }
156 return Some(bucket[index].clone());
157 }
158
159 if let Some(last) = bucket.last() {
160 if incoming.timestamp <= last.timestamp {
161 return None;
162 }
163 incoming.last_close_price = last.close_price;
164 }
165 bucket.push(incoming);
166 if bucket.len() > Self::KLINE_PUSH_CACHE_MAX_POINTS {
167 let drain = bucket.len() - Self::KLINE_PUSH_CACHE_MAX_POINTS;
168 bucket.drain(0..drain);
169 }
170 bucket.last().cloned()
171 }
172
173 pub fn upsert_kline_push_aggregates(
179 &self,
180 sec_key: &str,
181 rehab: i32,
182 kl_type: i32,
183 sessions: &[KlineAggregateSession],
184 incoming: CachedKLine,
185 ) -> Option<CachedKLine> {
186 let _owner = self.kline_data_lock.write();
187 let mut final_result = None;
188 for session in sessions {
189 final_result = self.upsert_kline_push_point_unlocked(
190 sec_key,
191 KlineDims::new(rehab, kl_type, *session),
192 incoming.clone(),
193 );
194 }
195 final_result
196 }
197
198 pub fn get_klines_by_dims(&self, sec_key: &str, dims: KlineDims) -> Option<Vec<CachedKLine>> {
200 let _owner = self.kline_data_lock.read();
201 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
202 self.klines.get(&cache_key).map(|v| v.clone())
203 }
204
205 pub fn get_klines(
206 &self,
207 sec_key: &str,
208 rehab: i32,
209 kl_type: i32,
210 session: KlineAggregateSession,
211 ) -> Option<Vec<CachedKLine>> {
212 self.get_klines_by_dims(sec_key, KlineDims::new(rehab, kl_type, session))
213 }
214
215 pub fn update_klines_broker_by_dims(
221 &self,
222 key: &QotSecurityKey,
223 dims: KlineDims,
224 klines: Vec<CachedKLine>,
225 ) {
226 let _owner = self.kline_data_lock.write();
227 self.update_klines_by_dims_unlocked(&key.cache_key(), dims, klines);
228 }
229
230 pub fn merge_kline_pull_generation_broker_by_dims(
238 &self,
239 key: &QotSecurityKey,
240 dims: KlineDims,
241 mut incoming: Vec<CachedKLine>,
242 ) {
243 let _owner = self.kline_data_lock.write();
244 incoming.sort_by(|left, right| left.timestamp.total_cmp(&right.timestamp));
245 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
246 let existing = self
247 .klines
248 .get(&cache_key)
249 .map_or_else(Vec::new, |points| points.clone());
250 if existing.is_empty() {
251 fill_kline_last_close_like_cpp(&mut incoming);
252 self.klines.insert(cache_key, incoming);
253 return;
254 }
255
256 let mut merged = Vec::with_capacity(existing.len().max(incoming.len()));
257 let (mut old_index, mut new_index) = (0, 0);
258 while old_index < existing.len() && new_index < incoming.len() {
259 let old = &existing[old_index];
260 let new = &incoming[new_index];
261 match old.timestamp.total_cmp(&new.timestamp) {
262 std::cmp::Ordering::Less => {
263 merged.push(old.clone());
264 old_index += 1;
265 }
266 std::cmp::Ordering::Greater => {
267 merged.push(new.clone());
268 new_index += 1;
269 }
270 std::cmp::Ordering::Equal => {
271 let selected = if old.hp_volume > new.hp_volume
272 || (old.hp_volume == new.hp_volume
273 && ordinary_kline_content_eq_except_last_close(old, new))
274 {
275 old
276 } else {
277 new
278 };
279 merged.push(selected.clone());
280 old_index += 1;
281 new_index += 1;
282 }
283 }
284 }
285 merged.extend_from_slice(&existing[old_index..]);
286 merged.extend_from_slice(&incoming[new_index..]);
287 fill_kline_last_close_like_cpp(&mut merged);
288 if merged.len() > Self::KLINE_PUSH_CACHE_MAX_POINTS {
289 let drain = merged.len() - Self::KLINE_PUSH_CACHE_MAX_POINTS;
290 merged.drain(0..drain);
291 }
292 self.klines.insert(cache_key, merged);
293 }
294
295 pub fn merge_event_contract_klines_broker_by_direction(
304 &self,
305 key: &QotSecurityKey,
306 dims: KlineDims,
307 direction: i32,
308 klines: Vec<CachedKLine>,
309 ) {
310 let _owner = self.kline_data_lock.write();
311 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
312 let mut bucket = self.klines.entry(cache_key).or_default();
313 bucket.retain(|point| point.direction.unwrap_or(0) != direction);
314 let mut replacement = klines;
315 replacement.sort_by(|left, right| left.timestamp.total_cmp(&right.timestamp));
316 if replacement.len() > Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION {
317 let drain = replacement.len() - Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION;
318 replacement.drain(0..drain);
319 }
320 bucket.extend(replacement);
321 bucket.sort_by(|left, right| {
322 left.timestamp.total_cmp(&right.timestamp).then_with(|| {
323 left.direction
324 .unwrap_or(0)
325 .cmp(&right.direction.unwrap_or(0))
326 })
327 });
328 }
329
330 pub fn upsert_event_contract_kline_push_broker_by_direction(
336 &self,
337 key: &QotSecurityKey,
338 dims: KlineDims,
339 direction: i32,
340 incoming: CachedKLine,
341 ) {
342 let _owner = self.kline_data_lock.write();
343 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
344 let mut bucket = self.klines.entry(cache_key).or_default();
345 let mut same_direction: Vec<CachedKLine> = bucket
346 .iter()
347 .filter(|point| point.direction.unwrap_or(0) == direction)
348 .cloned()
349 .collect();
350
351 match same_direction
352 .binary_search_by(|point| point.timestamp.total_cmp(&incoming.timestamp))
353 {
354 Ok(index) => same_direction[index] = incoming,
355 Err(index) => same_direction.insert(index, incoming),
356 }
357 if same_direction.len() > Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION {
358 let drain = same_direction.len() - Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION;
359 same_direction.drain(0..drain);
360 }
361
362 bucket.retain(|point| point.direction.unwrap_or(0) != direction);
363 bucket.extend(same_direction);
364 bucket.sort_by(|left, right| {
365 left.timestamp.total_cmp(&right.timestamp).then_with(|| {
366 left.direction
367 .unwrap_or(0)
368 .cmp(&right.direction.unwrap_or(0))
369 })
370 });
371 }
372
373 pub fn update_klines_broker(
374 &self,
375 key: &QotSecurityKey,
376 rehab: i32,
377 kl_type: i32,
378 session: KlineAggregateSession,
379 klines: Vec<CachedKLine>,
380 ) {
381 self.update_klines_broker_by_dims(key, KlineDims::new(rehab, kl_type, session), klines);
382 }
383
384 pub fn get_klines_broker_by_dims(
386 &self,
387 key: &QotSecurityKey,
388 dims: KlineDims,
389 ) -> Option<Vec<CachedKLine>> {
390 let _owner = self.kline_data_lock.read();
391 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
392 self.klines.get(&cache_key).map(|v| v.clone())
393 }
394
395 pub fn get_klines_broker(
396 &self,
397 key: &QotSecurityKey,
398 rehab: i32,
399 kl_type: i32,
400 session: KlineAggregateSession,
401 ) -> Option<Vec<CachedKLine>> {
402 self.get_klines_broker_by_dims(key, KlineDims::new(rehab, kl_type, session))
403 }
404}
405
406fn ordinary_kline_content_eq_like_cpp(left: &CachedKLine, right: &CachedKLine) -> bool {
407 left.time == right.time
408 && left.is_blank == right.is_blank
409 && left.open_price == right.open_price
410 && left.high_price == right.high_price
411 && left.low_price == right.low_price
412 && left.close_price == right.close_price
413 && left.last_close_price == right.last_close_price
414 && left.hp_volume == right.hp_volume
415 && left.turnover == right.turnover
416 && left.turnover_rate == right.turnover_rate
417 && left.pe == right.pe
418 && left.timestamp == right.timestamp
419 && left.is_replenish == right.is_replenish
420 && left.direction.unwrap_or(0) == right.direction.unwrap_or(0)
421}
422
423fn ordinary_kline_content_eq_except_last_close(left: &CachedKLine, right: &CachedKLine) -> bool {
424 left.time == right.time
425 && left.is_blank == right.is_blank
426 && left.open_price == right.open_price
427 && left.high_price == right.high_price
428 && left.low_price == right.low_price
429 && left.close_price == right.close_price
430 && left.hp_volume == right.hp_volume
431 && left.turnover == right.turnover
432 && left.turnover_rate == right.turnover_rate
433 && left.pe == right.pe
434 && left.timestamp == right.timestamp
435}
436
437fn fill_kline_last_close_like_cpp(points: &mut [CachedKLine]) {
438 for index in 1..points.len() {
439 if points[index].last_close_price == 0.0 {
440 points[index].last_close_price = points[index - 1].close_price;
441 }
442 }
443}