1use std::sync::Arc;
2
3use futu_core::qot_stock_key::QotSecurityKey;
4use futu_domain_qot_klrt::KlineAggregateSession;
5use tokio::sync::watch;
6
7use super::QotCache;
8
9pub struct KlinePullFlightGuard {
16 cache: Arc<QotCache>,
17 cache_key: String,
18 completion: watch::Sender<bool>,
19}
20
21impl Drop for KlinePullFlightGuard {
22 fn drop(&mut self) {
23 self.cache.kline_pull_in_flight.remove(&self.cache_key);
24 self.completion.send_replace(true);
25 }
26}
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct CachedKLine {
31 pub time: String,
32 pub is_blank: bool,
33 pub open_price: f64,
34 pub high_price: f64,
35 pub low_price: f64,
36 pub close_price: f64,
37 pub last_close_price: f64,
38 pub volume: i64,
39 pub hp_volume: f64,
40 pub turnover: f64,
41 pub turnover_rate: f64,
42 pub pe: f64,
43 pub timestamp: f64,
44 pub is_replenish: Option<bool>,
46 pub direction: Option<i32>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct KlineDims {
57 pub rehab: i32,
58 pub kl_type: i32,
59 pub session: KlineAggregateSession,
60}
61
62impl KlineDims {
63 #[must_use]
64 pub const fn new(rehab: i32, kl_type: i32, session: KlineAggregateSession) -> Self {
65 Self {
66 rehab,
67 kl_type,
68 session,
69 }
70 }
71}
72
73impl QotCache {
74 const KLINE_PUSH_CACHE_MAX_POINTS: usize = 2000;
75 const EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION: usize = 1000;
76
77 pub fn make_kline_key_by_dims(sec_key: &str, dims: KlineDims) -> String {
93 format!(
94 "{sec_key}:r{}:k{}:s{}",
95 dims.rehab,
96 dims.kl_type,
97 dims.session.key_code()
98 )
99 }
100
101 pub fn make_kline_key(
102 sec_key: &str,
103 rehab: i32,
104 kl_type: i32,
105 session: KlineAggregateSession,
106 ) -> String {
107 Self::make_kline_key_by_dims(sec_key, KlineDims::new(rehab, kl_type, session))
108 }
109
110 pub fn update_klines_by_dims(&self, sec_key: &str, dims: KlineDims, klines: Vec<CachedKLine>) {
112 let _owner = self.kline_data_lock.write();
113 self.update_klines_by_dims_unlocked(sec_key, dims, klines);
114 }
115
116 fn update_klines_by_dims_unlocked(
117 &self,
118 sec_key: &str,
119 dims: KlineDims,
120 mut klines: Vec<CachedKLine>,
121 ) {
122 fill_kline_last_close_like_cpp(&mut klines);
123 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
124 self.klines.insert(cache_key, klines);
125 }
126
127 pub fn update_klines(
128 &self,
129 sec_key: &str,
130 rehab: i32,
131 kl_type: i32,
132 session: KlineAggregateSession,
133 klines: Vec<CachedKLine>,
134 ) {
135 self.update_klines_by_dims(sec_key, KlineDims::new(rehab, kl_type, session), klines);
136 }
137
138 pub fn upsert_kline_push_point(
145 &self,
146 sec_key: &str,
147 dims: KlineDims,
148 incoming: CachedKLine,
149 ) -> Option<CachedKLine> {
150 let _owner = self.kline_data_lock.write();
151 self.upsert_kline_push_point_unlocked(sec_key, dims, incoming)
152 }
153
154 fn upsert_kline_push_point_unlocked(
155 &self,
156 sec_key: &str,
157 dims: KlineDims,
158 mut incoming: CachedKLine,
159 ) -> Option<CachedKLine> {
160 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
161 let mut bucket = self.klines.entry(cache_key).or_default();
162
163 if let Some(index) = bucket
164 .iter()
165 .position(|point| point.timestamp == incoming.timestamp)
166 {
167 if incoming.hp_volume < bucket[index].hp_volume
168 || ordinary_kline_content_eq_like_cpp(&incoming, &bucket[index])
169 {
170 return None;
171 }
172 incoming.last_close_price = bucket[index].last_close_price;
173 let close_price = incoming.close_price;
174 bucket[index] = incoming;
175 if let Some(next) = bucket.get_mut(index + 1) {
176 next.last_close_price = close_price;
177 }
178 return Some(bucket[index].clone());
179 }
180
181 if let Some(last) = bucket.last() {
182 if incoming.timestamp <= last.timestamp {
183 return None;
184 }
185 incoming.last_close_price = last.close_price;
186 }
187 bucket.push(incoming);
188 if bucket.len() > Self::KLINE_PUSH_CACHE_MAX_POINTS {
189 let drain = bucket.len() - Self::KLINE_PUSH_CACHE_MAX_POINTS;
190 bucket.drain(0..drain);
191 }
192 bucket.last().cloned()
193 }
194
195 pub fn upsert_kline_push_aggregates(
201 &self,
202 sec_key: &str,
203 rehab: i32,
204 kl_type: i32,
205 sessions: &[KlineAggregateSession],
206 incoming: CachedKLine,
207 ) -> Option<CachedKLine> {
208 let _owner = self.kline_data_lock.write();
209 let mut final_result = None;
210 for session in sessions {
211 final_result = self.upsert_kline_push_point_unlocked(
212 sec_key,
213 KlineDims::new(rehab, kl_type, *session),
214 incoming.clone(),
215 );
216 }
217 final_result
218 }
219
220 pub fn get_klines_by_dims(&self, sec_key: &str, dims: KlineDims) -> Option<Vec<CachedKLine>> {
222 let _owner = self.kline_data_lock.read();
223 let cache_key = Self::make_kline_key_by_dims(sec_key, dims);
224 self.klines.get(&cache_key).map(|v| v.clone())
225 }
226
227 pub fn get_klines(
228 &self,
229 sec_key: &str,
230 rehab: i32,
231 kl_type: i32,
232 session: KlineAggregateSession,
233 ) -> Option<Vec<CachedKLine>> {
234 self.get_klines_by_dims(sec_key, KlineDims::new(rehab, kl_type, session))
235 }
236
237 pub fn update_klines_broker_by_dims(
243 &self,
244 key: &QotSecurityKey,
245 dims: KlineDims,
246 klines: Vec<CachedKLine>,
247 ) {
248 let _owner = self.kline_data_lock.write();
249 self.update_klines_by_dims_unlocked(&key.cache_key(), dims, klines);
250 }
251
252 pub fn merge_kline_pull_generation_broker_by_dims(
260 &self,
261 key: &QotSecurityKey,
262 dims: KlineDims,
263 mut incoming: Vec<CachedKLine>,
264 ) {
265 let _owner = self.kline_data_lock.write();
266 incoming.sort_by(|left, right| left.timestamp.total_cmp(&right.timestamp));
267 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
268 let existing = self
269 .klines
270 .get(&cache_key)
271 .map_or_else(Vec::new, |points| points.clone());
272 if existing.is_empty() {
273 fill_kline_last_close_like_cpp(&mut incoming);
274 self.klines.insert(cache_key, incoming);
275 return;
276 }
277
278 let mut merged = Vec::with_capacity(existing.len().max(incoming.len()));
279 let (mut old_index, mut new_index) = (0, 0);
280 while old_index < existing.len() && new_index < incoming.len() {
281 let old = &existing[old_index];
282 let new = &incoming[new_index];
283 match old.timestamp.total_cmp(&new.timestamp) {
284 std::cmp::Ordering::Less => {
285 merged.push(old.clone());
286 old_index += 1;
287 }
288 std::cmp::Ordering::Greater => {
289 merged.push(new.clone());
290 new_index += 1;
291 }
292 std::cmp::Ordering::Equal => {
293 let selected = if old.hp_volume > new.hp_volume
294 || (old.hp_volume == new.hp_volume
295 && ordinary_kline_content_eq_except_last_close(old, new))
296 {
297 old
298 } else {
299 new
300 };
301 merged.push(selected.clone());
302 old_index += 1;
303 new_index += 1;
304 }
305 }
306 }
307 merged.extend_from_slice(&existing[old_index..]);
308 merged.extend_from_slice(&incoming[new_index..]);
309 fill_kline_last_close_like_cpp(&mut merged);
310 if merged.len() > Self::KLINE_PUSH_CACHE_MAX_POINTS {
311 let drain = merged.len() - Self::KLINE_PUSH_CACHE_MAX_POINTS;
312 merged.drain(0..drain);
313 }
314 self.klines.insert(cache_key, merged);
315 }
316
317 pub fn merge_event_contract_klines_broker_by_direction(
326 &self,
327 key: &QotSecurityKey,
328 dims: KlineDims,
329 direction: i32,
330 klines: Vec<CachedKLine>,
331 ) {
332 let _owner = self.kline_data_lock.write();
333 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
334 let mut bucket = self.klines.entry(cache_key).or_default();
335 bucket.retain(|point| point.direction.unwrap_or(0) != direction);
336 let mut replacement = klines;
337 replacement.sort_by(|left, right| left.timestamp.total_cmp(&right.timestamp));
338 if replacement.len() > Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION {
339 let drain = replacement.len() - Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION;
340 replacement.drain(0..drain);
341 }
342 bucket.extend(replacement);
343 bucket.sort_by(|left, right| {
344 left.timestamp.total_cmp(&right.timestamp).then_with(|| {
345 left.direction
346 .unwrap_or(0)
347 .cmp(&right.direction.unwrap_or(0))
348 })
349 });
350 }
351
352 pub fn upsert_event_contract_kline_push_broker_by_direction(
358 &self,
359 key: &QotSecurityKey,
360 dims: KlineDims,
361 direction: i32,
362 incoming: CachedKLine,
363 ) {
364 let _owner = self.kline_data_lock.write();
365 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
366 let mut bucket = self.klines.entry(cache_key).or_default();
367 let mut same_direction: Vec<CachedKLine> = bucket
368 .iter()
369 .filter(|point| point.direction.unwrap_or(0) == direction)
370 .cloned()
371 .collect();
372
373 match same_direction
374 .binary_search_by(|point| point.timestamp.total_cmp(&incoming.timestamp))
375 {
376 Ok(index) => same_direction[index] = incoming,
377 Err(index) => same_direction.insert(index, incoming),
378 }
379 if same_direction.len() > Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION {
380 let drain = same_direction.len() - Self::EVENT_CONTRACT_KLINE_MAX_POINTS_PER_DIRECTION;
381 same_direction.drain(0..drain);
382 }
383
384 bucket.retain(|point| point.direction.unwrap_or(0) != direction);
385 bucket.extend(same_direction);
386 bucket.sort_by(|left, right| {
387 left.timestamp.total_cmp(&right.timestamp).then_with(|| {
388 left.direction
389 .unwrap_or(0)
390 .cmp(&right.direction.unwrap_or(0))
391 })
392 });
393 }
394
395 pub fn update_klines_broker(
396 &self,
397 key: &QotSecurityKey,
398 rehab: i32,
399 kl_type: i32,
400 session: KlineAggregateSession,
401 klines: Vec<CachedKLine>,
402 ) {
403 self.update_klines_broker_by_dims(key, KlineDims::new(rehab, kl_type, session), klines);
404 }
405
406 pub fn get_klines_broker_by_dims(
408 &self,
409 key: &QotSecurityKey,
410 dims: KlineDims,
411 ) -> Option<Vec<CachedKLine>> {
412 let _owner = self.kline_data_lock.read();
413 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
414 self.klines.get(&cache_key).map(|v| v.clone())
415 }
416
417 pub fn latest_authoritative_kline_broker_by_dims(
425 &self,
426 key: &QotSecurityKey,
427 dims: KlineDims,
428 ) -> Option<CachedKLine> {
429 let _owner = self.kline_data_lock.read();
430 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
431 self.klines.get(&cache_key).and_then(|points| {
432 let latest = points.iter().rev().find(|point| !point.is_blank)?;
433 (latest.last_close_price > 0.0).then(|| latest.clone())
434 })
435 }
436
437 pub fn get_klines_broker(
438 &self,
439 key: &QotSecurityKey,
440 rehab: i32,
441 kl_type: i32,
442 session: KlineAggregateSession,
443 ) -> Option<Vec<CachedKLine>> {
444 self.get_klines_broker_by_dims(key, KlineDims::new(rehab, kl_type, session))
445 }
446
447 pub fn kline_count_like_cpp_broker_by_dims(
452 &self,
453 key: &QotSecurityKey,
454 dims: KlineDims,
455 ) -> usize {
456 let _owner = self.kline_data_lock.read();
457 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
458 self.klines.get(&cache_key).map_or(0, |points| {
459 points
460 .iter()
461 .rposition(|point| !point.is_blank)
462 .map_or(0, |index| index + 1)
463 })
464 }
465
466 pub fn kline_has_older_broker_by_dims(&self, key: &QotSecurityKey, dims: KlineDims) -> bool {
472 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
473 self.kline_has_older
474 .get(&cache_key)
475 .is_none_or(|flag| *flag)
476 }
477
478 pub fn set_kline_has_older_broker_by_dims(
483 &self,
484 key: &QotSecurityKey,
485 dims: KlineDims,
486 has_older: bool,
487 ) {
488 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
489 self.kline_has_older.insert(cache_key, has_older);
490 }
491
492 pub fn kline_has_data_like_cpp_broker_by_dims(
498 &self,
499 key: &QotSecurityKey,
500 dims: KlineDims,
501 req_num: usize,
502 ) -> bool {
503 if req_num == 0 {
504 return false;
505 }
506 self.kline_count_like_cpp_broker_by_dims(key, dims) >= req_num
507 || !self.kline_has_older_broker_by_dims(key, dims)
508 }
509
510 pub fn begin_kline_pull_broker_by_dims(
513 self: &Arc<Self>,
514 key: &QotSecurityKey,
515 dims: KlineDims,
516 ) -> Option<KlinePullFlightGuard> {
517 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
518 match self.kline_pull_in_flight.entry(cache_key.clone()) {
519 dashmap::mapref::entry::Entry::Occupied(_) => None,
520 dashmap::mapref::entry::Entry::Vacant(entry) => {
521 let (completion, _) = watch::channel(false);
522 entry.insert(completion.clone());
523 Some(KlinePullFlightGuard {
524 cache: Arc::clone(self),
525 cache_key,
526 completion,
527 })
528 }
529 }
530 }
531
532 pub async fn wait_for_kline_pull_broker_by_dims(&self, key: &QotSecurityKey, dims: KlineDims) {
535 let cache_key = Self::make_kline_key_by_dims(&key.cache_key(), dims);
536 let completion = self
537 .kline_pull_in_flight
538 .get(&cache_key)
539 .map(|entry| entry.value().clone());
540 if let Some(completion) = completion {
541 let mut receiver = completion.subscribe();
542 if !*receiver.borrow() {
543 let _ = receiver.changed().await;
544 }
545 }
546 }
547}
548
549fn ordinary_kline_content_eq_like_cpp(left: &CachedKLine, right: &CachedKLine) -> bool {
550 left.time == right.time
551 && left.is_blank == right.is_blank
552 && left.open_price == right.open_price
553 && left.high_price == right.high_price
554 && left.low_price == right.low_price
555 && left.close_price == right.close_price
556 && left.last_close_price == right.last_close_price
557 && left.hp_volume == right.hp_volume
558 && left.turnover == right.turnover
559 && left.turnover_rate == right.turnover_rate
560 && left.pe == right.pe
561 && left.timestamp == right.timestamp
562 && left.is_replenish == right.is_replenish
563 && left.direction.unwrap_or(0) == right.direction.unwrap_or(0)
564}
565
566fn ordinary_kline_content_eq_except_last_close(left: &CachedKLine, right: &CachedKLine) -> bool {
567 left.time == right.time
568 && left.is_blank == right.is_blank
569 && left.open_price == right.open_price
570 && left.high_price == right.high_price
571 && left.low_price == right.low_price
572 && left.close_price == right.close_price
573 && left.hp_volume == right.hp_volume
574 && left.turnover == right.turnover
575 && left.turnover_rate == right.turnover_rate
576 && left.pe == right.pe
577 && left.timestamp == right.timestamp
578}
579
580fn fill_kline_last_close_like_cpp(points: &mut [CachedKLine]) {
581 for index in 1..points.len() {
582 if points[index].last_close_price == 0.0 {
583 points[index].last_close_price = points[index - 1].close_price;
584 }
585 }
586}