1use dashmap::DashMap;
4use std::collections::HashSet;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, RwLock};
7
8mod readiness;
9mod types;
10
11pub use readiness::{StaticDataReadiness, StockListSyncStatus};
12pub use types::{
13 CachedPlateInfo, CachedSecurityInfo, CachedTradeDate, CryptoPairInfo, CryptoTradeConfig,
14 OptionContractInfo, SecurityInfoSource,
15};
16
17pub struct StaticDataCache {
19 securities: DashMap<String, Arc<CachedSecurityInfo>>,
25 id_to_key: DashMap<u64, String>,
28 future_main_link_aliases: DashMap<u64, HashSet<String>>,
35 option_contracts: DashMap<u64, OptionContractInfo>,
37 crypto_pairs: DashMap<String, CryptoPairInfo>,
39 crypto_trade_configs: DashMap<String, CryptoTradeConfig>,
41 pub trade_dates: DashMap<String, Vec<CachedTradeDate>>,
43 pub plates: DashMap<String, Vec<CachedPlateInfo>>,
45 owner_to_warrants: RwLock<std::collections::HashMap<u64, HashSet<u64>>>,
52
53 pub stale_mkt_ids: DashMap<String, ()>,
61
62 pub mkt_id_refresh_marked_total: AtomicU64,
68 pub mkt_id_refresh_done_total: AtomicU64,
69 pub mkt_id_refresh_failed_total: AtomicU64,
70
71 stock_list_first_sync_done: AtomicBool,
77 stock_list_sync_started_total: AtomicU64,
78 stock_list_sync_finished_total: AtomicU64,
79 stock_list_sync_failed_total: AtomicU64,
80 stock_list_sync_recoverable_retry_total: AtomicU64,
81 stock_list_sync_zero_delta_total: AtomicU64,
82 stock_list_converged: AtomicBool,
83 stock_list_sync_last_version: AtomicU64,
84 stock_list_sync_last_total_stocks: AtomicU64,
85 stock_list_sync_last_cached_count: AtomicU64,
86 stock_list_sync_last_finished_ms: AtomicU64,
87}
88
89impl StaticDataCache {
90 pub fn new() -> Self {
91 Self {
92 securities: DashMap::new(),
93 id_to_key: DashMap::new(),
94 future_main_link_aliases: DashMap::new(),
95 option_contracts: DashMap::new(),
96 crypto_pairs: DashMap::new(),
97 crypto_trade_configs: DashMap::new(),
98 trade_dates: DashMap::new(),
99 plates: DashMap::new(),
100 owner_to_warrants: RwLock::new(std::collections::HashMap::new()),
101 stale_mkt_ids: DashMap::new(),
102 mkt_id_refresh_marked_total: AtomicU64::new(0),
103 mkt_id_refresh_done_total: AtomicU64::new(0),
104 mkt_id_refresh_failed_total: AtomicU64::new(0),
105 stock_list_first_sync_done: AtomicBool::new(false),
106 stock_list_sync_started_total: AtomicU64::new(0),
107 stock_list_sync_finished_total: AtomicU64::new(0),
108 stock_list_sync_failed_total: AtomicU64::new(0),
109 stock_list_sync_recoverable_retry_total: AtomicU64::new(0),
110 stock_list_sync_zero_delta_total: AtomicU64::new(0),
111 stock_list_converged: AtomicBool::new(false),
112 stock_list_sync_last_version: AtomicU64::new(0),
113 stock_list_sync_last_total_stocks: AtomicU64::new(0),
114 stock_list_sync_last_cached_count: AtomicU64::new(0),
115 stock_list_sync_last_finished_ms: AtomicU64::new(0),
116 }
117 }
118
119 pub fn record_stock_list_sync_started(&self) {
120 self.stock_list_sync_started_total
121 .fetch_add(1, Ordering::Relaxed);
122 }
123
124 pub fn record_stock_list_sync_finished(
125 &self,
126 version: u64,
127 total_stocks: u64,
128 cached_count: u64,
129 finished_ms: u64,
130 ) {
131 self.stock_list_first_sync_done
132 .store(true, Ordering::Release);
133 if total_stocks == 0 {
137 self.stock_list_sync_zero_delta_total
138 .fetch_add(1, Ordering::Relaxed);
139 self.stock_list_converged.store(true, Ordering::Release);
140 }
141 self.stock_list_sync_finished_total
142 .fetch_add(1, Ordering::Relaxed);
143 self.stock_list_sync_last_version
144 .store(version, Ordering::Relaxed);
145 self.stock_list_sync_last_total_stocks
146 .store(total_stocks, Ordering::Relaxed);
147 self.stock_list_sync_last_cached_count
148 .store(cached_count, Ordering::Relaxed);
149 self.stock_list_sync_last_finished_ms
150 .store(finished_ms, Ordering::Relaxed);
151 }
152
153 pub fn record_stock_list_sync_failed(&self) {
154 self.stock_list_sync_failed_total
155 .fetch_add(1, Ordering::Relaxed);
156 }
157
158 pub fn record_stock_list_sync_recoverable_retry(&self) {
159 self.stock_list_sync_recoverable_retry_total
160 .fetch_add(1, Ordering::Relaxed);
161 }
162
163 pub fn stock_list_sync_status(&self) -> StockListSyncStatus {
164 let finished_ms = self
165 .stock_list_sync_last_finished_ms
166 .load(Ordering::Relaxed);
167 StockListSyncStatus {
168 first_sync_done: self.stock_list_first_sync_done.load(Ordering::Acquire),
169 started_total: self.stock_list_sync_started_total.load(Ordering::Relaxed),
170 finished_total: self.stock_list_sync_finished_total.load(Ordering::Relaxed),
171 failed_total: self.stock_list_sync_failed_total.load(Ordering::Relaxed),
172 recoverable_retry_total: self
173 .stock_list_sync_recoverable_retry_total
174 .load(Ordering::Relaxed),
175 zero_delta_total: self
176 .stock_list_sync_zero_delta_total
177 .load(Ordering::Relaxed),
178 converged: self.stock_list_converged.load(Ordering::Acquire),
179 last_version: self.stock_list_sync_last_version.load(Ordering::Relaxed),
180 last_total_stocks: self
181 .stock_list_sync_last_total_stocks
182 .load(Ordering::Relaxed),
183 last_cached_count: self
184 .stock_list_sync_last_cached_count
185 .load(Ordering::Relaxed),
186 last_finished_ms: (finished_ms > 0).then_some(finished_ms),
187 }
188 }
189
190 pub fn security_info_count(&self) -> usize {
191 self.securities.len()
192 }
193
194 pub fn stock_list_readiness(&self) -> StaticDataReadiness {
195 self.stock_list_sync_status()
196 .readiness_for_security_count(self.security_info_count())
197 }
198
199 pub fn get_security_info_trigger_refresh(&self, key: &str) -> Option<CachedSecurityInfo> {
209 let info = self.get_security_info(key)?;
210 if info.needs_mkt_id_refresh() {
211 self.mark_stale_mkt_id(key);
212 }
213 Some(info)
214 }
215
216 pub fn mark_stale_mkt_id(&self, key: &str) {
221 self.stale_mkt_ids.insert(key.to_string(), ());
222 self.mkt_id_refresh_marked_total
223 .fetch_add(1, Ordering::Relaxed);
224 }
225
226 pub fn drain_stale_mkt_ids(&self) -> Vec<String> {
244 let keys: Vec<String> = self.stale_mkt_ids.iter().map(|e| e.key().clone()).collect();
245 for k in &keys {
246 self.stale_mkt_ids.remove(k);
247 }
248 keys
249 }
250
251 pub fn update_mkt_id(&self, key: &str, new_mkt_id: u32) -> bool {
256 if let Some(mut entry) = self.securities.get_mut(key) {
257 Arc::make_mut(&mut entry).mkt_id = new_mkt_id;
258 self.mkt_id_refresh_done_total
259 .fetch_add(1, Ordering::Relaxed);
260 true
261 } else {
262 false
263 }
264 }
265
266 pub fn record_mkt_id_refresh_failed(&self) {
268 self.mkt_id_refresh_failed_total
269 .fetch_add(1, Ordering::Relaxed);
270 }
271
272 #[must_use]
274 pub fn stale_mkt_ids_count(&self) -> usize {
275 self.stale_mkt_ids.len()
276 }
277
278 pub fn upsert_full_security_info(&self, key: &str, info: CachedSecurityInfo) {
289 debug_assert!(
290 info.source.is_complete(),
291 "upsert_full_security_info called with non-complete source ({:?})",
292 info.source
293 );
294 self.upsert_with_owner_index_maintenance(key, info);
295 }
296
297 pub fn upsert_crypto_pair_info(&self, key: &str, pair: CryptoPairInfo) {
299 if pair.cc_origin.is_empty() && pair.cc_destination.is_empty() {
300 self.crypto_pairs.remove(key);
301 } else {
302 self.crypto_pairs.insert(key.to_string(), pair);
303 }
304 }
305
306 pub fn set_option_contract_info(&self, stock_id: u64, info: OptionContractInfo) {
308 if stock_id == 0 {
309 return;
310 }
311 self.option_contracts.insert(stock_id, info);
312 }
313
314 pub fn get_option_contract_info_by_stock_id(
316 &self,
317 stock_id: u64,
318 ) -> Option<OptionContractInfo> {
319 self.option_contracts
320 .get(&stock_id)
321 .map(|entry| *entry.value())
322 }
323
324 pub fn get_crypto_pair_info(&self, key: &str) -> Option<CryptoPairInfo> {
326 self.crypto_pairs.get(key).map(|v| v.clone())
327 }
328
329 fn crypto_trade_config_key(broker_id: u32, symbol: &str, exchange: &str) -> String {
330 format!(
331 "{broker_id}:{}:{}",
332 symbol.trim().to_ascii_uppercase(),
333 exchange.trim().to_ascii_uppercase()
334 )
335 }
336
337 pub fn set_crypto_trade_configs_for_broker(
339 &self,
340 broker_id: u32,
341 configs: Vec<CryptoTradeConfig>,
342 ) {
343 let prefix = format!("{broker_id}:");
344 self.crypto_trade_configs
345 .retain(|key, _| !key.starts_with(&prefix));
346 for config in configs {
347 if config.symbol.trim().is_empty() || config.exchange.trim().is_empty() {
348 continue;
349 }
350 let key = Self::crypto_trade_config_key(broker_id, &config.symbol, &config.exchange);
351 self.crypto_trade_configs.insert(key, config);
352 }
353 }
354
355 pub fn get_crypto_trade_config(
357 &self,
358 broker_id: u32,
359 symbol: &str,
360 exchange: &str,
361 ) -> Option<CryptoTradeConfig> {
362 let key = Self::crypto_trade_config_key(broker_id, symbol, exchange);
363 self.crypto_trade_configs.get(&key).map(|v| v.clone())
364 }
365
366 pub fn upsert_basic_security_info(&self, key: &str, info: CachedSecurityInfo) {
372 debug_assert!(
373 !info.source.is_complete(),
374 "upsert_basic_security_info called with complete source ({:?}); use upsert_full",
375 info.source
376 );
377 debug_assert_eq!(
378 info.warrnt_stock_owner, 0,
379 "OnDemandBasic must have warrnt_stock_owner=0 (caller didn't query the field)"
380 );
381 let old_info = if let Some(existing) = self.securities.get(key) {
387 if existing.is_complete() {
388 tracing::debug!(
389 key,
390 "upsert_basic_security_info skipped: existing complete row prevails"
391 );
392 return;
393 }
394 Some(Arc::clone(existing.value()))
395 } else {
396 None
397 };
398 if let Some(old_info) = old_info {
399 self.remove_future_main_link_aliases(key, &old_info);
400 }
401 self.securities
402 .insert(key.to_string(), Arc::new(info.clone()));
403 self.id_to_key.insert(info.stock_id, key.to_string());
404 self.add_future_main_link_aliases(key, &info);
405 }
406
407 pub fn delete_security_info(&self, stock_id: u64) -> bool {
413 let Some((_, key)) = self.id_to_key.remove(&stock_id) else {
414 return false;
415 };
416 self.option_contracts.remove(&stock_id);
417 let old_info = self.securities.remove(&key).map(|(_, info)| info);
419 let old_owner = old_info.as_ref().map(|r| r.warrnt_stock_owner).unwrap_or(0);
420 if let Some(old_info) = old_info.as_ref() {
421 self.remove_future_main_link_aliases(&key, old_info);
422 }
423 self.crypto_pairs.remove(&key);
424 if old_owner != 0
426 && let Ok(mut map) = self.owner_to_warrants.write()
427 && let Some(set) = map.get_mut(&old_owner)
428 {
429 set.remove(&stock_id);
430 if set.is_empty() {
431 map.remove(&old_owner);
432 }
433 }
434 if let Ok(mut map) = self.owner_to_warrants.write() {
436 map.remove(&stock_id);
437 }
438 true
439 }
440
441 fn upsert_with_owner_index_maintenance(&self, key: &str, info: CachedSecurityInfo) {
443 let old_info = self.securities.get(key).map(|r| Arc::clone(r.value()));
445 let old_owner = old_info.as_ref().map(|r| r.warrnt_stock_owner).unwrap_or(0);
446 let new_owner = info.warrnt_stock_owner;
447
448 if let Some(old) = old_info.as_ref() {
449 self.remove_future_main_link_aliases(key, old);
450 }
451
452 let stock_id = info.stock_id;
454 self.securities
455 .insert(key.to_string(), Arc::new(info.clone()));
456 self.id_to_key.insert(stock_id, key.to_string());
457 self.add_future_main_link_aliases(key, &info);
458
459 if old_owner != new_owner {
461 if let Ok(mut map) = self.owner_to_warrants.write() {
463 if old_owner != 0
464 && let Some(set) = map.get_mut(&old_owner)
465 {
466 set.remove(&stock_id);
467 if set.is_empty() {
468 map.remove(&old_owner);
469 }
470 }
471 if new_owner != 0 {
472 map.entry(new_owner).or_default().insert(stock_id);
473 }
474 }
475 } else if new_owner != 0 {
476 if let Ok(mut map) = self.owner_to_warrants.write() {
478 map.entry(new_owner).or_default().insert(stock_id);
479 }
480 }
481 }
482
483 fn future_main_link_target_ids(info: &CachedSecurityInfo) -> Vec<u64> {
484 let mut ids = Vec::with_capacity(2);
485 for target in [info.future_origin_id, info.zhuli_id] {
486 if target != 0 && target != info.stock_id && !ids.contains(&target) {
487 ids.push(target);
488 }
489 }
490 ids
491 }
492
493 fn add_future_main_link_aliases(&self, key: &str, info: &CachedSecurityInfo) {
494 for target in Self::future_main_link_target_ids(info) {
495 self.future_main_link_aliases
496 .entry(target)
497 .or_default()
498 .insert(key.to_string());
499 }
500 }
501
502 fn remove_future_main_link_aliases(&self, key: &str, info: &CachedSecurityInfo) {
503 for target in Self::future_main_link_target_ids(info) {
504 if let Some(mut aliases) = self.future_main_link_aliases.get_mut(&target) {
505 aliases.remove(key);
506 let empty = aliases.is_empty();
507 drop(aliases);
508 if empty {
509 self.future_main_link_aliases.remove(&target);
510 }
511 }
512 }
513 }
514
515 #[must_use]
521 pub fn get_future_main_link_alias_keys(&self, stock_id: u64) -> Vec<String> {
522 let Some(aliases) = self.future_main_link_aliases.get(&stock_id) else {
523 return Vec::new();
524 };
525 let mut keys: Vec<String> = aliases.iter().cloned().collect();
526 keys.sort();
527 keys
528 }
529
530 #[must_use]
537 pub fn quote_push_targets_for_stock_id(
538 &self,
539 stock_id: u64,
540 ) -> Vec<(String, Arc<CachedSecurityInfo>)> {
541 let mut targets = Vec::new();
542
543 if let Some(sec_key_ref) = self.id_to_key.get(&stock_id) {
544 let sec_key = sec_key_ref.clone();
545 drop(sec_key_ref);
546 if let Some(info) = self.get_security_info_arc(&sec_key) {
547 targets.push((sec_key, info));
548 }
549 }
550
551 for alias_key in self.get_future_main_link_alias_keys(stock_id) {
552 if targets.iter().any(|(key, _)| key == &alias_key) {
553 continue;
554 }
555 if let Some(info) = self.get_security_info_arc(&alias_key) {
556 targets.push((alias_key, info));
557 }
558 }
559
560 targets
561 }
562
563 #[must_use]
582 pub fn quote_push_targets_for_stock_key(
583 &self,
584 stock_id: u64,
585 broker_id: Option<std::num::NonZeroU32>,
586 ) -> Vec<(
587 futu_core::qot_stock_key::QotSecurityKey,
588 Arc<CachedSecurityInfo>,
589 )> {
590 let bare = self.quote_push_targets_for_stock_id(stock_id);
594 bare.into_iter()
595 .map(|(public_sec_key, info)| {
596 let key = match broker_id {
597 Some(nz) => futu_core::qot_stock_key::QotSecurityKey::from_broker_id(
598 public_sec_key,
599 stock_id,
600 nz.get(),
601 ),
602 None => futu_core::qot_stock_key::QotSecurityKey::no_broker(
603 public_sec_key,
604 stock_id,
605 ),
606 };
607 (key, info)
608 })
609 .collect()
610 }
611
612 #[deprecated(
623 since = "1.4.106",
624 note = "use upsert_full_security_info / upsert_basic_security_info / delete_security_info"
625 )]
626 pub fn set_security_info(&self, key: &str, info: CachedSecurityInfo) {
627 if info.source.is_complete() {
628 self.upsert_full_security_info(key, info);
629 } else {
630 self.upsert_basic_security_info(key, info);
631 }
632 }
633
634 pub fn get_security_info(&self, key: &str) -> Option<CachedSecurityInfo> {
635 self.get_security_info_arc(key)
636 .map(|info| info.as_ref().clone())
637 }
638
639 pub fn get_security_info_arc(&self, key: &str) -> Option<Arc<CachedSecurityInfo>> {
640 self.securities.get(key).map(|v| Arc::clone(v.value()))
641 }
642
643 pub fn security_id_for_key(&self, key: &str) -> Option<u64> {
644 self.get_security_info(key)
645 .map(|info| info.stock_id)
646 .filter(|stock_id| *stock_id > 0)
647 }
648
649 pub fn security_info_snapshot(&self) -> Vec<CachedSecurityInfo> {
650 self.security_info_snapshot_matching(|_| true)
651 }
652
653 pub fn security_info_snapshot_matching(
654 &self,
655 mut predicate: impl FnMut(&CachedSecurityInfo) -> bool,
656 ) -> Vec<CachedSecurityInfo> {
657 self.securities
658 .iter()
659 .filter_map(|entry| {
660 let info = entry.value();
661 predicate(info.as_ref()).then(|| info.as_ref().clone())
662 })
663 .collect()
664 }
665
666 pub fn security_key_by_stock_id(&self, stock_id: u64) -> Option<String> {
667 self.id_to_key.get(&stock_id).map(|key| key.value().clone())
668 }
669
670 pub fn get_security_info_by_stock_id(&self, stock_id: u64) -> Option<CachedSecurityInfo> {
672 let key = self.security_key_by_stock_id(stock_id)?;
673 self.get_security_info(&key)
674 }
675
676 pub fn get_security_info_by_stock_id_trigger_refresh(
677 &self,
678 stock_id: u64,
679 ) -> Option<CachedSecurityInfo> {
680 let key = self.security_key_by_stock_id(stock_id)?;
681 self.get_security_info_trigger_refresh(&key)
682 }
683
684 pub fn add_warrant_owner(&self, warrant_stock_id: u64, owner_stock_id: u64) {
689 if owner_stock_id == 0 {
690 return;
691 }
692 if let Ok(mut map) = self.owner_to_warrants.write() {
693 map.entry(owner_stock_id)
694 .or_default()
695 .insert(warrant_stock_id);
696 }
697 }
698
699 #[must_use]
711 pub fn search_warrants_by_owner(&self, owner_stock_id: u64) -> Vec<u64> {
712 match self.owner_to_warrants.read() {
713 Ok(map) => map
714 .get(&owner_stock_id)
715 .map(|set| set.iter().copied().collect())
716 .unwrap_or_default(),
717 _ => Vec::new(),
718 }
719 }
720}
721
722impl Default for StaticDataCache {
723 fn default() -> Self {
724 Self::new()
725 }
726}
727
728#[cfg(test)]
729mod tests;