1use super::{
2 CachedSecurityInfo, CryptoPairInfo, CryptoTradeConfig, OnDemandSecurityPublishOutcome,
3 OptionContractInfo, StaticDataCache,
4};
5use std::collections::HashSet;
6use std::sync::Arc;
7
8impl StaticDataCache {
9 #[must_use]
10 pub fn stock_list_publication_generation(&self) -> u64 {
11 self.stock_list_publication_generation
12 .load(std::sync::atomic::Ordering::Acquire)
13 }
14
15 pub fn upsert_full_security_info(&self, key: &str, info: CachedSecurityInfo) {
26 debug_assert!(
27 info.source.is_complete(),
28 "upsert_full_security_info called with non-complete source ({:?})",
29 info.source
30 );
31 let _identity_write = match self.zero_id_repair.lock() {
32 Ok(guard) => guard,
33 Err(_) => return,
34 };
35 self.upsert_with_owner_index_maintenance(key, info);
36 }
37
38 pub fn upsert_full_security_info_with_crypto_pair(
41 &self,
42 key: &str,
43 info: CachedSecurityInfo,
44 pair: CryptoPairInfo,
45 ) {
46 debug_assert!(
47 info.source.is_complete(),
48 "upsert_full_security_info_with_crypto_pair called with non-complete source ({:?})",
49 info.source
50 );
51 let _identity_write = match self.zero_id_repair.lock() {
52 Ok(guard) => guard,
53 Err(_) => return,
54 };
55 let stock_id = info.stock_id;
56 if stock_id > 0 {
57 self.upsert_crypto_pair_by_stock_id_locked(stock_id, pair);
58 self.upsert_with_owner_index_maintenance(key, info);
59 } else {
60 self.upsert_with_owner_index_maintenance(key, info);
61 self.upsert_crypto_pair_info_locked(key, pair);
62 }
63 self.stock_list_publication_generation
64 .fetch_add(1, std::sync::atomic::Ordering::Release);
65 }
66
67 pub fn publish_bootstrap_security_if_stock_list_generation(
73 &self,
74 expected_generation: u64,
75 key: &str,
76 info: CachedSecurityInfo,
77 ) -> bool {
78 if info.stock_id == 0
79 || info.market <= 0
80 || info.code.is_empty()
81 || !info.source.is_complete()
82 {
83 return false;
84 }
85 let _identity_write = match self.zero_id_repair.lock() {
86 Ok(guard) => guard,
87 Err(_) => return false,
88 };
89 if self
90 .stock_list_publication_generation
91 .load(std::sync::atomic::Ordering::Acquire)
92 != expected_generation
93 {
94 return false;
95 }
96 if self
97 .securities_by_stock_id
98 .get(&info.stock_id)
99 .is_some_and(|existing| existing.is_complete())
100 {
101 return false;
102 }
103 if self
104 .securities
105 .get(key)
106 .is_some_and(|existing| existing.stock_id > 0 && existing.stock_id != info.stock_id)
107 {
108 return false;
109 }
110 self.upsert_with_owner_index_maintenance(key, info);
111 true
112 }
113
114 pub fn upsert_crypto_pair_info(&self, key: &str, pair: CryptoPairInfo) {
116 let _identity_write = match self.zero_id_repair.lock() {
117 Ok(guard) => guard,
118 Err(_) => return,
119 };
120 self.upsert_crypto_pair_info_locked(key, pair);
121 }
122
123 fn upsert_crypto_pair_info_locked(&self, key: &str, pair: CryptoPairInfo) {
124 if let Some(stock_id) = self
125 .securities
126 .get(key)
127 .map(|entry| entry.stock_id)
128 .filter(|stock_id| *stock_id > 0)
129 {
130 self.upsert_crypto_pair_by_stock_id_locked(stock_id, pair.clone());
131 }
132 if pair.cc_origin.is_empty() && pair.cc_destination.is_empty() {
133 self.crypto_pairs.remove(key);
134 } else {
135 self.crypto_pairs.insert(key.to_string(), pair);
136 }
137 }
138
139 fn upsert_crypto_pair_by_stock_id_locked(&self, stock_id: u64, pair: CryptoPairInfo) {
140 if stock_id == 0 {
141 return;
142 }
143 if pair.cc_origin.is_empty() && pair.cc_destination.is_empty() {
144 self.crypto_pairs_by_stock_id.remove(&stock_id);
145 } else {
146 self.crypto_pairs_by_stock_id.insert(stock_id, pair);
147 }
148 }
149
150 fn refresh_public_crypto_pair_locked(&self, key: &str) {
151 let pair = self.securities.get(key).and_then(|security| {
152 (security.stock_id > 0)
153 .then_some(security.stock_id)
154 .and_then(|stock_id| {
155 self.crypto_pairs_by_stock_id
156 .get(&stock_id)
157 .map(|pair| pair.clone())
158 })
159 });
160 if let Some(pair) = pair {
161 self.crypto_pairs.insert(key.to_string(), pair);
162 } else {
163 self.crypto_pairs.remove(key);
164 }
165 }
166
167 pub fn set_option_contract_info(&self, stock_id: u64, info: OptionContractInfo) {
169 if stock_id == 0 {
170 return;
171 }
172 self.option_contracts.insert(stock_id, info);
173 }
174
175 pub fn get_option_contract_info_by_stock_id(
177 &self,
178 stock_id: u64,
179 ) -> Option<OptionContractInfo> {
180 self.option_contracts
181 .get(&stock_id)
182 .map(|entry| *entry.value())
183 }
184
185 pub fn get_crypto_pair_info(&self, key: &str) -> Option<CryptoPairInfo> {
187 self.crypto_pairs.get(key).map(|v| v.clone())
188 }
189
190 fn crypto_trade_config_key(broker_id: u32, symbol: &str, exchange: &str) -> String {
191 format!(
192 "{broker_id}:{}:{}",
193 symbol.trim().to_ascii_uppercase(),
194 exchange.trim().to_ascii_uppercase()
195 )
196 }
197
198 pub fn set_crypto_trade_configs_for_broker(
200 &self,
201 broker_id: u32,
202 configs: Vec<CryptoTradeConfig>,
203 ) {
204 let prefix = format!("{broker_id}:");
205 self.crypto_trade_configs
206 .retain(|key, _| !key.starts_with(&prefix));
207 for config in configs {
208 if config.symbol.trim().is_empty() || config.exchange.trim().is_empty() {
209 continue;
210 }
211 let key = Self::crypto_trade_config_key(broker_id, &config.symbol, &config.exchange);
212 self.crypto_trade_configs.insert(key, config);
213 }
214 }
215
216 pub fn get_crypto_trade_config(
218 &self,
219 broker_id: u32,
220 symbol: &str,
221 exchange: &str,
222 ) -> Option<CryptoTradeConfig> {
223 let key = Self::crypto_trade_config_key(broker_id, symbol, exchange);
224 self.crypto_trade_configs.get(&key).map(|v| v.clone())
225 }
226
227 pub fn upsert_basic_security_info(&self, key: &str, info: CachedSecurityInfo) {
233 debug_assert!(
234 !info.source.is_complete(),
235 "upsert_basic_security_info called with complete source ({:?}); use upsert_full",
236 info.source
237 );
238 debug_assert_eq!(
239 info.warrnt_stock_owner, 0,
240 "OnDemandBasic must have warrnt_stock_owner=0 (caller didn't query the field)"
241 );
242 let _identity_write = match self.zero_id_repair.lock() {
243 Ok(guard) => guard,
244 Err(_) => return,
245 };
246 self.upsert_basic_security_info_locked(key, info);
247 }
248
249 pub fn upsert_basic_security_info_by_stock_id_only(&self, info: CachedSecurityInfo) {
256 debug_assert!(
257 !info.source.is_complete(),
258 "ID-only on-demand rows must not replace complete stock-list rows"
259 );
260 if info.stock_id == 0 {
261 return;
262 }
263 let _identity_write = match self.zero_id_repair.lock() {
264 Ok(guard) => guard,
265 Err(_) => return,
266 };
267 if self
268 .securities_by_stock_id
269 .get(&info.stock_id)
270 .is_some_and(|existing| existing.is_complete() || !existing.code.is_empty())
271 {
272 return;
273 }
274 self.securities_by_stock_id
275 .insert(info.stock_id, Arc::new(info));
276 }
277
278 fn upsert_basic_security_info_locked(&self, key: &str, info: CachedSecurityInfo) {
279 if let Some(existing) = self.securities.get(key)
285 && existing.is_complete()
286 {
287 tracing::debug!(
288 key,
289 "upsert_basic_security_info skipped: existing complete row prevails"
290 );
291 return;
292 }
293 if info.stock_id > 0
294 && self
295 .securities_by_stock_id
296 .get(&info.stock_id)
297 .is_some_and(|existing| existing.is_complete())
298 {
299 tracing::debug!(
300 key,
301 stock_id = info.stock_id,
302 "upsert_basic_security_info skipped: exact complete row prevails"
303 );
304 return;
305 }
306 self.upsert_with_owner_index_maintenance(key, info);
307 }
308
309 pub fn publish_on_demand_security_info(
315 &self,
316 key: &str,
317 refreshed: CachedSecurityInfo,
318 crypto_pair: Option<CryptoPairInfo>,
319 ) -> OnDemandSecurityPublishOutcome {
320 self.publish_on_demand_security_info_with_pair_hook(key, refreshed, crypto_pair, || {})
321 }
322
323 pub(super) fn publish_on_demand_security_info_with_pair_hook(
324 &self,
325 key: &str,
326 refreshed: CachedSecurityInfo,
327 crypto_pair: Option<CryptoPairInfo>,
328 before_pair_publish: impl FnOnce(),
329 ) -> OnDemandSecurityPublishOutcome {
330 let _identity_write = match self.zero_id_repair.lock() {
331 Ok(guard) => guard,
332 Err(_) => return OnDemandSecurityPublishOutcome::Rejected,
333 };
334 if refreshed.stock_id == 0
335 || refreshed.source.is_complete()
336 || refreshed.warrnt_stock_owner != 0
337 {
338 return OnDemandSecurityPublishOutcome::Rejected;
339 }
340
341 let stock_id = refreshed.stock_id;
342 if self
343 .id_to_key
344 .get(&stock_id)
345 .is_some_and(|mapped| mapped.as_str() != key)
346 {
347 return OnDemandSecurityPublishOutcome::Rejected;
348 }
349
350 let existing = self
351 .securities
352 .get(key)
353 .map(|entry| Arc::clone(entry.value()));
354 match existing {
355 Some(existing) if existing.stock_id == 0 => {
356 let stock_id = refreshed.stock_id;
357 if self.repair_zero_stock_id_from_basic_locked(key, refreshed, || {}) {
358 if let Some(pair) = crypto_pair {
359 self.upsert_crypto_pair_by_stock_id_locked(stock_id, pair);
360 self.refresh_public_crypto_pair_locked(key);
361 }
362 OnDemandSecurityPublishOutcome::ZeroIdRepaired
363 } else {
364 OnDemandSecurityPublishOutcome::Rejected
365 }
366 }
367 Some(existing) if existing.stock_id != stock_id => {
368 OnDemandSecurityPublishOutcome::Rejected
369 }
370 Some(existing) if existing.is_complete() => {
371 if self.update_mkt_id_locked(key, refreshed.mkt_id) {
372 OnDemandSecurityPublishOutcome::CompleteMktIdUpdated
373 } else {
374 OnDemandSecurityPublishOutcome::Rejected
375 }
376 }
377 Some(_) | None => {
378 let stock_id = refreshed.stock_id;
379 self.upsert_basic_security_info_locked(key, refreshed);
380 before_pair_publish();
381 if let Some(pair) = crypto_pair {
382 self.upsert_crypto_pair_by_stock_id_locked(stock_id, pair);
383 self.refresh_public_crypto_pair_locked(key);
384 }
385 OnDemandSecurityPublishOutcome::BasicPublished
386 }
387 }
388 }
389
390 pub fn repair_zero_stock_id_from_basic(
398 &self,
399 key: &str,
400 refreshed: CachedSecurityInfo,
401 ) -> bool {
402 self.repair_zero_stock_id_from_basic_with_security_hook(key, refreshed, || {})
403 }
404
405 pub(super) fn repair_zero_stock_id_from_basic_with_security_hook(
406 &self,
407 key: &str,
408 refreshed: CachedSecurityInfo,
409 after_security_locked: impl FnOnce(),
410 ) -> bool {
411 let _repair = match self.zero_id_repair.lock() {
416 Ok(guard) => guard,
417 Err(_) => return false,
418 };
419
420 self.repair_zero_stock_id_from_basic_locked(key, refreshed, after_security_locked)
421 }
422
423 fn repair_zero_stock_id_from_basic_locked(
424 &self,
425 key: &str,
426 refreshed: CachedSecurityInfo,
427 after_security_locked: impl FnOnce(),
428 ) -> bool {
429 if refreshed.stock_id == 0
430 || refreshed.source.is_complete()
431 || refreshed.warrnt_stock_owner != 0
432 {
433 return false;
434 }
435
436 let stock_id = refreshed.stock_id;
437 let old_public_keys = self.public_keys_for_stock_id_locked(stock_id);
438 if self
439 .id_to_key
440 .get(&stock_id)
441 .is_some_and(|mapped| mapped.as_str() != key)
442 {
443 return false;
444 }
445
446 let Some(preliminary_ref) = self.securities.get(key) else {
447 return false;
448 };
449 let preliminary = Arc::clone(preliminary_ref.value());
450 drop(preliminary_ref);
451 if preliminary.stock_id != 0
452 || preliminary.market != refreshed.market
453 || preliminary.code != refreshed.code
454 {
455 return false;
456 }
457
458 let preliminary_owner = preliminary.warrnt_stock_owner;
463 let mut owner_index = if preliminary_owner != 0 {
464 match self.owner_to_warrants.write() {
465 Ok(index) => Some(index),
466 Err(_) => return false,
467 }
468 } else {
469 None
470 };
471
472 let Some(mut entry) = self.securities.get_mut(key) else {
473 return false;
474 };
475 after_security_locked();
476 let old = Arc::clone(entry.value());
477 if old.stock_id != 0
478 || old.market != refreshed.market
479 || old.code != refreshed.code
480 || old.warrnt_stock_owner != preliminary_owner
481 {
482 return false;
483 }
484
485 let mut updated = if old.is_complete() {
486 old.as_ref().clone()
487 } else {
488 refreshed.clone()
489 };
490 updated.stock_id = stock_id;
491 updated.mkt_id = refreshed.mkt_id;
492
493 let old_owner = old.warrnt_stock_owner;
494 let new_owner = updated.warrnt_stock_owner;
495 debug_assert_eq!(old_owner, preliminary_owner);
496 debug_assert!(new_owner == 0 || owner_index.is_some());
497
498 let stock_id_reservation = match self.id_to_key.entry(stock_id) {
503 dashmap::mapref::entry::Entry::Occupied(occupied) => {
504 if occupied.get() != key {
505 return false;
506 }
507 occupied.into_ref()
508 }
509 dashmap::mapref::entry::Entry::Vacant(vacant) => vacant.insert(key.to_string()),
510 };
511
512 self.remove_future_main_link_aliases(key, &old);
513 let updated = Arc::new(updated);
514 *entry = Arc::clone(&updated);
515 self.securities_by_stock_id
516 .insert(stock_id, Arc::clone(&updated));
517 if let Some(pair) = self.crypto_pairs.get(key).map(|pair| pair.clone()) {
518 self.crypto_pairs_by_stock_id.insert(stock_id, pair);
519 }
520 self.add_future_main_link_aliases(key, &updated);
521 drop(entry);
522
523 if let Some(index) = owner_index.as_mut() {
524 if old_owner != 0
525 && !self.securities.iter().any(|candidate| {
526 candidate.stock_id == 0 && candidate.warrnt_stock_owner == old_owner
527 })
528 && let Some(warrants) = index.get_mut(&old_owner)
529 {
530 warrants.remove(&0);
531 if warrants.is_empty() {
532 index.remove(&old_owner);
533 }
534 }
535 if new_owner != 0 {
536 index.entry(new_owner).or_default().insert(stock_id);
537 }
538 }
539
540 drop(stock_id_reservation);
541 self.id_to_key.remove_if(&0, |_, mapped| mapped == key);
542 let new_public_keys = self.public_keys_for_stock_id_locked(stock_id);
543 self.reconcile_public_stock_id_keys_locked(stock_id, &old_public_keys, &new_public_keys);
544 true
545 }
546
547 pub fn delete_security_info(&self, stock_id: u64) -> bool {
553 let _identity_write = match self.zero_id_repair.lock() {
554 Ok(guard) => guard,
555 Err(_) => return false,
556 };
557 self.stock_list_publication_generation
558 .fetch_add(1, std::sync::atomic::Ordering::Release);
559 let old_public_keys = self.public_keys_for_stock_id_locked(stock_id);
560 let Some((_, key)) = self.id_to_key.remove(&stock_id) else {
561 return false;
562 };
563 self.option_contracts.remove(&stock_id);
564 if stock_id == 0 {
565 if let Some((_, old)) = self
566 .securities
567 .remove_if(&key, |_, info| info.stock_id == 0)
568 {
569 self.remove_future_main_link_aliases(&key, &old);
570 self.remove_owner_relation(&old);
571 }
572 self.crypto_pairs.remove(&key);
573 self.stale_mkt_ids.remove(&key);
574 return true;
575 }
576
577 self.security_request_aliases.remove(&stock_id);
578
579 if let Some((_, old)) = self.securities_by_stock_id.remove(&stock_id) {
580 self.remove_owner_relation(&old);
581 self.remove_future_main_link_aliases_if_unreferenced(&key, &old);
582 }
583 self.crypto_pairs_by_stock_id.remove(&stock_id);
584 self.reconcile_public_stock_id_keys_locked(stock_id, &old_public_keys, &HashSet::new());
585 for public_key in old_public_keys {
586 self.refresh_public_security_locked(&public_key);
587 }
588
589 if let Ok(mut map) = self.owner_to_warrants.write() {
591 map.remove(&stock_id);
592 }
593 true
594 }
595
596 fn upsert_with_owner_index_maintenance(&self, key: &str, info: CachedSecurityInfo) {
598 let stock_id = info.stock_id;
599 if stock_id == 0 {
600 self.upsert_zero_id_security_locked(key, info);
601 return;
602 }
603
604 let old_public_keys = self.public_keys_for_stock_id_locked(stock_id);
605 let old_key = self.id_to_key.get(&stock_id).map(|mapped| mapped.clone());
606 let old_exact = self
607 .securities_by_stock_id
608 .remove(&stock_id)
609 .map(|(_, old)| old);
610 self.id_to_key.remove(&stock_id);
611
612 if let (Some(old_key), Some(old)) = (old_key.as_deref(), old_exact.as_ref())
613 && !old.is_complete()
614 && old_key != key
615 {
616 self.security_request_aliases
617 .entry(stock_id)
618 .or_default()
619 .insert(old_key.to_string());
620 }
621 self.remove_security_request_alias_locked(stock_id, key);
622
623 if let (Some(old_key), Some(old)) = (old_key.as_deref(), old_exact.as_ref()) {
624 self.remove_owner_relation(old);
625 self.remove_future_main_link_aliases_if_unreferenced(old_key, old);
626 }
627
628 self.remove_zero_id_public_row_locked(key);
629
630 let info = Arc::new(info);
631 self.securities_by_stock_id
632 .insert(stock_id, Arc::clone(&info));
633 self.id_to_key.insert(stock_id, key.to_string());
634 self.add_owner_relation(&info);
635 self.add_future_main_link_aliases(key, &info);
636
637 let new_public_keys = self.public_keys_for_stock_id_locked(stock_id);
638 self.reconcile_public_stock_id_keys_locked(stock_id, &old_public_keys, &new_public_keys);
639 let mut public_keys_to_refresh = old_public_keys;
640 public_keys_to_refresh.extend(new_public_keys);
641 for public_key in public_keys_to_refresh {
642 self.refresh_public_security_locked(&public_key);
643 }
644 }
645
646 fn upsert_zero_id_security_locked(&self, key: &str, info: CachedSecurityInfo) {
647 if self.public_security_candidate_locked(key).is_some() {
648 return;
649 }
650 self.remove_zero_id_public_row_locked(key);
651 let info = Arc::new(info);
652 self.securities.insert(key.to_string(), Arc::clone(&info));
653 self.id_to_key.insert(0, key.to_string());
654 self.add_owner_relation(&info);
655 self.add_future_main_link_aliases(key, &info);
656 }
657
658 fn remove_zero_id_public_row_locked(&self, key: &str) {
659 let Some((_, old)) = self.securities.remove_if(key, |_, info| info.stock_id == 0) else {
660 return;
661 };
662 self.id_to_key.remove_if(&0, |_, mapped| mapped == key);
663 self.remove_future_main_link_aliases(key, &old);
664 self.remove_owner_relation(&old);
665 }
666
667 fn public_security_candidate_locked(&self, key: &str) -> Option<Arc<CachedSecurityInfo>> {
672 let stock_ids = self.public_stock_ids_by_key.get(key)?;
673 stock_ids.iter().find_map(|stock_id| {
674 #[cfg(test)]
675 self.public_security_candidate_inspections
676 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
677 self.securities_by_stock_id
678 .get(stock_id)
679 .map(|info| Arc::clone(info.value()))
680 .filter(|info| !info.no_search)
681 })
682 }
683
684 fn public_keys_for_stock_id_locked(&self, stock_id: u64) -> HashSet<String> {
685 if stock_id == 0 {
686 return HashSet::new();
687 }
688 let mut keys = self.security_request_alias_keys_locked(stock_id);
689 if let Some(canonical) = self.id_to_key.get(&stock_id) {
690 keys.insert(canonical.clone());
691 }
692 keys
693 }
694
695 fn reconcile_public_stock_id_keys_locked(
696 &self,
697 stock_id: u64,
698 old_keys: &HashSet<String>,
699 new_keys: &HashSet<String>,
700 ) {
701 debug_assert!(stock_id > 0);
702 for key in old_keys.difference(new_keys) {
703 let remove_entry =
704 if let Some(mut stock_ids) = self.public_stock_ids_by_key.get_mut(key) {
705 stock_ids.remove(&stock_id);
706 stock_ids.is_empty()
707 } else {
708 false
709 };
710 if remove_entry {
711 self.public_stock_ids_by_key.remove(key);
712 }
713 }
714 for key in new_keys.difference(old_keys) {
715 self.public_stock_ids_by_key
716 .entry(key.clone())
717 .or_default()
718 .insert(stock_id);
719 }
720 }
721
722 #[cfg(test)]
723 pub(super) fn public_identity_index_matches_sources(&self) -> bool {
724 let mut expected =
725 std::collections::HashMap::<String, std::collections::BTreeSet<u64>>::new();
726 for mapped in &self.id_to_key {
727 if *mapped.key() > 0 {
728 expected
729 .entry(mapped.value().clone())
730 .or_default()
731 .insert(*mapped.key());
732 }
733 }
734 for aliases in &self.security_request_aliases {
735 for alias in aliases.value() {
736 expected
737 .entry(alias.clone())
738 .or_default()
739 .insert(*aliases.key());
740 }
741 }
742 let actual = self
743 .public_stock_ids_by_key
744 .iter()
745 .map(|entry| (entry.key().clone(), entry.value().clone()))
746 .collect::<std::collections::HashMap<_, _>>();
747 actual == expected
748 }
749
750 #[cfg(test)]
751 pub(super) fn reset_public_security_candidate_inspections(&self) {
752 self.public_security_candidate_inspections
753 .store(0, std::sync::atomic::Ordering::Relaxed);
754 }
755
756 #[cfg(test)]
757 pub(super) fn public_security_candidate_inspections(&self) -> u64 {
758 self.public_security_candidate_inspections
759 .load(std::sync::atomic::Ordering::Relaxed)
760 }
761
762 fn security_request_alias_keys_locked(&self, stock_id: u64) -> HashSet<String> {
763 self.security_request_aliases
764 .get(&stock_id)
765 .map(|aliases| aliases.clone())
766 .unwrap_or_default()
767 }
768
769 fn remove_security_request_alias_locked(&self, stock_id: u64, key: &str) {
770 let remove_entry =
771 if let Some(mut aliases) = self.security_request_aliases.get_mut(&stock_id) {
772 aliases.remove(key);
773 aliases.is_empty()
774 } else {
775 false
776 };
777 if remove_entry {
778 self.security_request_aliases.remove(&stock_id);
779 }
780 }
781
782 pub(super) fn refresh_public_keys_for_stock_id_locked(&self, stock_id: u64) {
783 for key in self.public_keys_for_stock_id_locked(stock_id) {
784 self.refresh_public_security_locked(&key);
785 }
786 }
787
788 fn refresh_public_security_locked(&self, key: &str) {
789 if let Some(candidate) = self.public_security_candidate_locked(key) {
790 if !candidate.needs_mkt_id_refresh() {
791 self.stale_mkt_ids.remove(key);
792 }
793 self.securities.insert(key.to_string(), candidate);
794 } else if self
795 .securities
796 .get(key)
797 .is_some_and(|current| current.stock_id > 0)
798 {
799 self.securities.remove(key);
800 self.stale_mkt_ids.remove(key);
801 }
802 self.refresh_public_crypto_pair_locked(key);
803 }
804}