1use dashmap::DashMap;
4use std::collections::{BTreeSet, HashSet};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Mutex, RwLock};
7
8mod readiness;
9mod security_identity;
10mod sync_status;
11mod types;
12
13pub use readiness::{StaticDataReadiness, StockListSyncStatus};
14use sync_status::StockListSyncCounters;
15pub use types::{
16 CachedPlateInfo, CachedSecurityInfo, CachedTradeDate, CryptoPairInfo, CryptoTradeConfig,
17 OptionContractInfo, SecurityInfoSource,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum OnDemandSecurityPublishOutcome {
22 BasicPublished,
23 ZeroIdRepaired,
24 CompleteMktIdUpdated,
25 Rejected,
26}
27
28pub struct StaticDataCache {
30 securities: DashMap<String, Arc<CachedSecurityInfo>>,
36 securities_by_stock_id: DashMap<u64, Arc<CachedSecurityInfo>>,
42 id_to_key: DashMap<u64, String>,
45 security_request_aliases: DashMap<u64, HashSet<String>>,
55 public_stock_ids_by_key: DashMap<String, BTreeSet<u64>>,
63 future_main_link_aliases: DashMap<u64, HashSet<String>>,
70 option_contracts: DashMap<u64, OptionContractInfo>,
72 crypto_pairs: DashMap<String, CryptoPairInfo>,
74 crypto_pairs_by_stock_id: DashMap<u64, CryptoPairInfo>,
77 crypto_trade_configs: DashMap<String, CryptoTradeConfig>,
79 pub trade_dates: DashMap<String, Vec<CachedTradeDate>>,
81 pub plates: DashMap<String, Vec<CachedPlateInfo>>,
83 owner_to_warrants: RwLock<std::collections::HashMap<u64, HashSet<u64>>>,
90
91 zero_id_repair: Mutex<()>,
95 stock_list_publication_generation: AtomicU64,
99
100 #[cfg(test)]
104 public_security_candidate_inspections: AtomicU64,
105
106 pub stale_mkt_ids: DashMap<String, ()>,
114
115 pub mkt_id_refresh_marked_total: AtomicU64,
121 pub mkt_id_refresh_done_total: AtomicU64,
122 pub mkt_id_refresh_failed_total: AtomicU64,
123
124 stock_list_sync: StockListSyncCounters,
130}
131
132impl StaticDataCache {
133 pub fn new() -> Self {
134 Self {
135 securities: DashMap::new(),
136 securities_by_stock_id: DashMap::new(),
137 id_to_key: DashMap::new(),
138 security_request_aliases: DashMap::new(),
139 public_stock_ids_by_key: DashMap::new(),
140 future_main_link_aliases: DashMap::new(),
141 option_contracts: DashMap::new(),
142 crypto_pairs: DashMap::new(),
143 crypto_pairs_by_stock_id: DashMap::new(),
144 crypto_trade_configs: DashMap::new(),
145 trade_dates: DashMap::new(),
146 plates: DashMap::new(),
147 owner_to_warrants: RwLock::new(std::collections::HashMap::new()),
148 zero_id_repair: Mutex::new(()),
149 stock_list_publication_generation: AtomicU64::new(0),
150 #[cfg(test)]
151 public_security_candidate_inspections: AtomicU64::new(0),
152 stale_mkt_ids: DashMap::new(),
153 mkt_id_refresh_marked_total: AtomicU64::new(0),
154 mkt_id_refresh_done_total: AtomicU64::new(0),
155 mkt_id_refresh_failed_total: AtomicU64::new(0),
156 stock_list_sync: StockListSyncCounters::new(),
157 }
158 }
159
160 pub fn record_stock_list_sync_started(&self) {
161 self.stock_list_sync.record_started();
162 }
163
164 pub fn record_stock_list_sync_finished(
165 &self,
166 version: u64,
167 total_stocks: u64,
168 cached_count: u64,
169 finished_ms: u64,
170 ) {
171 self.stock_list_sync
172 .record_finished(version, total_stocks, cached_count, finished_ms);
173 }
174
175 pub fn record_stock_list_sync_failed(&self) {
176 self.stock_list_sync.record_failed();
177 }
178
179 pub fn record_stock_list_sync_recoverable_retry(&self) {
180 self.stock_list_sync.record_recoverable_retry();
181 }
182
183 pub fn stock_list_sync_status(&self) -> StockListSyncStatus {
184 self.stock_list_sync.status()
185 }
186
187 pub fn security_info_count(&self) -> usize {
188 let mut positive_ids = HashSet::new();
189 let mut zero_id_rows = 0usize;
190 for info in &self.securities {
191 if info.stock_id == 0 {
192 zero_id_rows += 1;
193 } else {
194 positive_ids.insert(info.stock_id);
195 }
196 }
197 positive_ids.len() + zero_id_rows
198 }
199
200 pub fn clear_stock_list_security_info(&self) -> usize {
207 let _identity_write = match self.zero_id_repair.lock() {
208 Ok(guard) => guard,
209 Err(_) => return 0,
210 };
211 self.stock_list_publication_generation
212 .fetch_add(1, Ordering::Release);
213 let removed = self.security_info_count();
214 self.securities.clear();
215 self.securities_by_stock_id.clear();
216 self.id_to_key.clear();
217 self.security_request_aliases.clear();
218 self.public_stock_ids_by_key.clear();
219 self.future_main_link_aliases.clear();
220 self.option_contracts.clear();
221 self.crypto_pairs.clear();
222 self.crypto_pairs_by_stock_id.clear();
223 self.stale_mkt_ids.clear();
224 if let Ok(mut owners) = self.owner_to_warrants.write() {
225 owners.clear();
226 }
227 removed
228 }
229
230 pub fn stock_list_readiness(&self) -> StaticDataReadiness {
231 self.stock_list_sync_status()
232 .readiness_for_security_count(self.security_info_count())
233 }
234
235 pub fn get_security_info_trigger_refresh(&self, key: &str) -> Option<CachedSecurityInfo> {
245 let info = self.get_security_info(key)?;
246 if info.needs_mkt_id_refresh() {
247 self.mark_stale_mkt_id(key);
248 }
249 Some(info)
250 }
251
252 pub fn mark_stale_mkt_id(&self, key: &str) {
257 self.stale_mkt_ids.insert(key.to_string(), ());
258 self.mkt_id_refresh_marked_total
259 .fetch_add(1, Ordering::Relaxed);
260 }
261
262 pub fn drain_stale_mkt_ids(&self) -> Vec<String> {
280 let keys: Vec<String> = self.stale_mkt_ids.iter().map(|e| e.key().clone()).collect();
281 for k in &keys {
282 self.stale_mkt_ids.remove(k);
283 }
284 keys
285 }
286
287 pub fn update_mkt_id(&self, key: &str, new_mkt_id: u32) -> bool {
292 let _identity_write = match self.zero_id_repair.lock() {
293 Ok(guard) => guard,
294 Err(_) => return false,
295 };
296 self.update_mkt_id_locked(key, new_mkt_id)
297 }
298
299 fn update_mkt_id_locked(&self, key: &str, new_mkt_id: u32) -> bool {
300 let Some(public) = self
301 .securities
302 .get(key)
303 .map(|entry| Arc::clone(entry.value()))
304 else {
305 return false;
306 };
307
308 if public.stock_id == 0 {
309 if let Some(mut entry) = self.securities.get_mut(key) {
310 Arc::make_mut(&mut entry).mkt_id = new_mkt_id;
311 } else {
312 return false;
313 }
314 } else {
315 let Some(mut exact) = self.securities_by_stock_id.get_mut(&public.stock_id) else {
316 return false;
317 };
318 Arc::make_mut(&mut exact).mkt_id = new_mkt_id;
319 drop(exact);
320 self.refresh_public_keys_for_stock_id_locked(public.stock_id);
321 }
322
323 self.mkt_id_refresh_done_total
324 .fetch_add(1, Ordering::Relaxed);
325 true
326 }
327
328 pub fn record_mkt_id_refresh_failed(&self) {
330 self.mkt_id_refresh_failed_total
331 .fetch_add(1, Ordering::Relaxed);
332 }
333
334 #[must_use]
336 pub fn stale_mkt_ids_count(&self) -> usize {
337 self.stale_mkt_ids.len()
338 }
339
340 fn add_owner_relation(&self, info: &CachedSecurityInfo) {
341 if info.warrnt_stock_owner == 0 {
342 return;
343 }
344 if let Ok(mut map) = self.owner_to_warrants.write() {
345 map.entry(info.warrnt_stock_owner)
346 .or_default()
347 .insert(info.stock_id);
348 }
349 }
350
351 fn remove_owner_relation(&self, info: &CachedSecurityInfo) {
352 let owner = info.warrnt_stock_owner;
353 if owner == 0 {
354 return;
355 }
356 if info.stock_id == 0
357 && self
358 .securities
359 .iter()
360 .any(|candidate| candidate.stock_id == 0 && candidate.warrnt_stock_owner == owner)
361 {
362 return;
363 }
364 if let Ok(mut map) = self.owner_to_warrants.write()
365 && let Some(set) = map.get_mut(&owner)
366 {
367 set.remove(&info.stock_id);
368 if set.is_empty() {
369 map.remove(&owner);
370 }
371 }
372 }
373
374 fn remove_future_main_link_aliases_if_unreferenced(
375 &self,
376 key: &str,
377 removed: &CachedSecurityInfo,
378 ) {
379 for target in Self::future_main_link_target_ids(removed) {
380 let still_referenced = self.id_to_key.iter().any(|mapped| {
381 mapped.value() == key
382 && self
383 .securities_by_stock_id
384 .get(mapped.key())
385 .is_some_and(|info| {
386 Self::future_main_link_target_ids(&info).contains(&target)
387 })
388 });
389 if still_referenced {
390 continue;
391 }
392 if let Some(mut aliases) = self.future_main_link_aliases.get_mut(&target) {
393 aliases.remove(key);
394 let empty = aliases.is_empty();
395 drop(aliases);
396 if empty {
397 self.future_main_link_aliases.remove(&target);
398 }
399 }
400 }
401 }
402
403 fn future_main_link_target_ids(info: &CachedSecurityInfo) -> Vec<u64> {
404 let mut ids = Vec::with_capacity(2);
405 for target in [info.future_origin_id, info.zhuli_id] {
406 if target != 0 && target != info.stock_id && !ids.contains(&target) {
407 ids.push(target);
408 }
409 }
410 ids
411 }
412
413 fn add_future_main_link_aliases(&self, key: &str, info: &CachedSecurityInfo) {
414 for target in Self::future_main_link_target_ids(info) {
415 self.future_main_link_aliases
416 .entry(target)
417 .or_default()
418 .insert(key.to_string());
419 }
420 }
421
422 fn remove_future_main_link_aliases(&self, key: &str, info: &CachedSecurityInfo) {
423 for target in Self::future_main_link_target_ids(info) {
424 if let Some(mut aliases) = self.future_main_link_aliases.get_mut(&target) {
425 aliases.remove(key);
426 let empty = aliases.is_empty();
427 drop(aliases);
428 if empty {
429 self.future_main_link_aliases.remove(&target);
430 }
431 }
432 }
433 }
434
435 #[must_use]
441 pub fn get_future_main_link_alias_keys(&self, stock_id: u64) -> Vec<String> {
442 let Some(aliases) = self.future_main_link_aliases.get(&stock_id) else {
443 return Vec::new();
444 };
445 let mut keys: Vec<String> = aliases.iter().cloned().collect();
446 keys.sort();
447 keys
448 }
449
450 #[must_use]
457 pub fn quote_push_targets_for_stock_id(
458 &self,
459 stock_id: u64,
460 ) -> Vec<(String, Arc<CachedSecurityInfo>)> {
461 let mut targets = Vec::new();
462
463 if let Some(sec_key_ref) = self.id_to_key.get(&stock_id) {
464 let sec_key = sec_key_ref.clone();
465 drop(sec_key_ref);
466 if let Some(info) = self
467 .securities_by_stock_id
468 .get(&stock_id)
469 .map(|entry| Arc::clone(entry.value()))
470 {
471 targets.push((sec_key, info));
472 }
473 }
474
475 for alias_key in self.get_future_main_link_alias_keys(stock_id) {
476 if targets.iter().any(|(key, _)| key == &alias_key) {
477 continue;
478 }
479 if let Some(info) = self.get_security_info_arc(&alias_key) {
480 targets.push((alias_key, info));
481 }
482 }
483
484 targets
485 }
486
487 #[must_use]
506 pub fn quote_push_targets_for_stock_key(
507 &self,
508 stock_id: u64,
509 broker_id: Option<std::num::NonZeroU32>,
510 ) -> Vec<(
511 futu_core::qot_stock_key::QotSecurityKey,
512 Arc<CachedSecurityInfo>,
513 )> {
514 let bare = self.quote_push_targets_for_stock_id(stock_id);
518 bare.into_iter()
519 .map(|(public_sec_key, info)| {
520 let key = match broker_id {
521 Some(nz) => futu_core::qot_stock_key::QotSecurityKey::from_broker_id(
522 public_sec_key,
523 stock_id,
524 nz.get(),
525 ),
526 None => futu_core::qot_stock_key::QotSecurityKey::no_broker(
527 public_sec_key,
528 stock_id,
529 ),
530 };
531 (key, info)
532 })
533 .collect()
534 }
535
536 #[deprecated(
547 since = "1.4.106",
548 note = "use upsert_full_security_info / upsert_basic_security_info / delete_security_info"
549 )]
550 pub fn set_security_info(&self, key: &str, info: CachedSecurityInfo) {
551 if info.source.is_complete() {
552 self.upsert_full_security_info(key, info);
553 } else {
554 self.upsert_basic_security_info(key, info);
555 }
556 }
557
558 pub fn get_security_info(&self, key: &str) -> Option<CachedSecurityInfo> {
559 self.get_security_info_arc(key)
560 .map(|info| info.as_ref().clone())
561 }
562
563 pub fn get_security_info_arc(&self, key: &str) -> Option<Arc<CachedSecurityInfo>> {
564 self.securities.get(key).map(|v| Arc::clone(v.value()))
565 }
566
567 pub fn security_id_for_key(&self, key: &str) -> Option<u64> {
568 self.get_security_info(key)
569 .map(|info| info.stock_id)
570 .filter(|stock_id| *stock_id > 0)
571 }
572
573 pub fn security_info_snapshot_matching(
574 &self,
575 mut predicate: impl FnMut(&CachedSecurityInfo) -> bool,
576 ) -> Vec<CachedSecurityInfo> {
577 let mut seen_positive_ids = HashSet::new();
578 self.securities
579 .iter()
580 .filter_map(|entry| {
581 let info = entry.value();
582 if !predicate(info.as_ref())
583 || (info.stock_id > 0 && !seen_positive_ids.insert(info.stock_id))
584 {
585 return None;
586 }
587 Some(info.as_ref().clone())
588 })
589 .collect()
590 }
591
592 pub fn security_key_by_stock_id(&self, stock_id: u64) -> Option<String> {
593 self.id_to_key.get(&stock_id).map(|key| key.value().clone())
594 }
595
596 pub fn get_security_info_by_stock_id(&self, stock_id: u64) -> Option<CachedSecurityInfo> {
598 self.securities_by_stock_id
599 .get(&stock_id)
600 .map(|info| info.as_ref().clone())
601 }
602
603 pub fn get_security_info_by_stock_id_trigger_refresh(
604 &self,
605 stock_id: u64,
606 ) -> Option<CachedSecurityInfo> {
607 let info = self.get_security_info_by_stock_id(stock_id)?;
608 if info.needs_mkt_id_refresh()
609 && let Some(key) = self.security_key_by_stock_id(stock_id)
610 {
611 self.mark_stale_mkt_id(&key);
612 }
613 Some(info)
614 }
615
616 pub fn add_warrant_owner(&self, warrant_stock_id: u64, owner_stock_id: u64) {
621 if owner_stock_id == 0 {
622 return;
623 }
624 if let Ok(mut map) = self.owner_to_warrants.write() {
625 map.entry(owner_stock_id)
626 .or_default()
627 .insert(warrant_stock_id);
628 }
629 }
630
631 #[must_use]
643 pub fn search_warrants_by_owner(&self, owner_stock_id: u64) -> Vec<u64> {
644 match self.owner_to_warrants.read() {
645 Ok(map) => map
646 .get(&owner_stock_id)
647 .map(|set| set.iter().copied().collect())
648 .unwrap_or_default(),
649 _ => Vec::new(),
650 }
651 }
652}
653
654impl Default for StaticDataCache {
655 fn default() -> Self {
656 Self::new()
657 }
658}
659
660#[cfg(test)]
661mod tests;