use crate::error::Result;
use crate::utils::CacheEntry;
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
pub(crate) struct DomainCache<V> {
ttl: Option<Duration>,
entries: RwLock<HashMap<String, CacheEntry<V>>>,
guard: Mutex<()>,
}
impl<V: Clone> DomainCache<V> {
pub(crate) fn new(ttl: Option<Duration>) -> Self {
Self {
ttl,
entries: RwLock::new(HashMap::new()),
guard: Mutex::new(()),
}
}
pub(crate) async fn get_or_try<F, Fut>(&self, key: String, f: F) -> Result<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<V>>,
{
let Some(ttl) = self.ttl else {
return f().await;
};
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(ttl)
{
return Ok(entry.value.clone());
}
let _g = self.guard.lock().await;
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(ttl)
{
return Ok(entry.value.clone());
}
let value = f().await?;
self.entries
.write()
.await
.insert(key, CacheEntry::new(value.clone()));
Ok(value)
}
}
macro_rules! domain_handle {
($(#[$meta:meta])* pub struct $name:ident { $field:ident, $accessor:ident } cache: $val:ty, chart) => {
$(#[$meta])*
pub struct $name {
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
chart_cache: crate::domains::DomainCache<crate::models::chart::Chart>,
}
impl $name {
pub(crate) fn with_providers(
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field,
providers,
cache: crate::domains::DomainCache::new(None),
chart_cache: crate::domains::DomainCache::new(None),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
self.cache = crate::domains::DomainCache::new(Some(ttl));
self.chart_cache = crate::domains::DomainCache::new(Some(ttl));
self
}
pub fn $accessor(&self) -> &str {
&self.$field
}
}
};
(
$(#[$meta:meta])*
pub struct $name:ident {
$(#[$meta1:meta])* $field1:ident, $accessor1:ident,
$(#[$meta2:meta])* $field2:ident, $accessor2:ident $(,)?
} cache: $val:ty, chart
) => {
$(#[$meta])*
pub struct $name {
$field1: std::sync::Arc<str>,
$field2: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
chart_cache: crate::domains::DomainCache<crate::models::chart::Chart>,
}
impl $name {
pub(crate) fn with_providers(
$field1: std::sync::Arc<str>,
$field2: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field1,
$field2,
providers,
cache: crate::domains::DomainCache::new(None),
chart_cache: crate::domains::DomainCache::new(None),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
self.cache = crate::domains::DomainCache::new(Some(ttl));
self.chart_cache = crate::domains::DomainCache::new(Some(ttl));
self
}
$(#[$meta1])*
pub fn $accessor1(&self) -> &str {
&self.$field1
}
$(#[$meta2])*
pub fn $accessor2(&self) -> &str {
&self.$field2
}
}
};
($(#[$meta:meta])* pub struct $name:ident { $field:ident, $accessor:ident } cache: $val:ty) => {
$(#[$meta])*
pub struct $name {
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
}
impl $name {
pub(crate) fn with_providers(
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field,
providers,
cache: crate::domains::DomainCache::new(None),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
self.cache = crate::domains::DomainCache::new(Some(ttl));
self
}
pub fn $accessor(&self) -> &str {
&self.$field
}
}
};
}
macro_rules! fetch_via {
($self:expr, $field:ident, $cap:ident, $fetch:ident, $ret:ty) => {{
let __sym = $self.$field.clone();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(String::new(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let p = p.clone();
async move { p.$fetch(&__s).await }
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_via_two {
($self:expr, $field1:ident, $field2:ident, $cap:ident, $fetch:ident, $ret:ty) => {{
let __a = $self.$field1.clone();
let __b = $self.$field2.clone();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(String::new(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __x = __a.clone();
let __y = __b.clone();
let p = p.clone();
async move { p.$fetch(&__x, &__y).await }
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_via_with {
($self:expr, $field:ident, $cap:ident, $fetch:ident, $arg:expr, $ret:ty) => {{
let __sym = $self.$field.clone();
let __arg = ($arg).to_string();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(__arg.clone(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let __a = __arg.clone();
let p = p.clone();
async move { p.$fetch(&__s, &__a).await }
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_chart_via {
($self:expr, $sym:expr, $interval:expr, $range:expr) => {{
let __sym: String = $sym;
let __interval = $interval;
let __range = $range;
let __providers = std::sync::Arc::clone(&$self.providers);
let __key = format!("{}:{}:{}", __sym, __interval, __range);
$self
.chart_cache
.get_or_try(__key, move || async move {
__providers
.fetch(crate::providers::Capability::CHART, move |p| {
let __s = __sym.clone();
let p = p.clone();
async move { p.fetch_chart(&__s, __interval, __range).await }
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! impl_chartable_analytics {
($name:ident, $cal:expr) => {
impl $name {
#[cfg(feature = "indicators")]
pub async fn indicators(
&self,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::indicators::IndicatorsSummary> {
let chart = self.chart(interval, range).await?;
Ok(crate::indicators::summary::calculate_indicators(
&chart.candles,
))
}
#[cfg(feature = "indicators")]
pub async fn indicator(
&self,
indicator: crate::indicators::Indicator,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::indicators::IndicatorResult> {
let chart = self.chart(interval, range).await?;
Ok(crate::indicators::compute_indicator(indicator, &chart)?)
}
#[cfg(feature = "risk")]
pub async fn risk(
&self,
interval: crate::Interval,
range: crate::TimeRange,
) -> crate::error::Result<crate::risk::RiskSummary> {
let chart = self.chart(interval, range).await?;
Ok(crate::risk::compute_risk_summary_with_periods(
&chart.candles,
None,
crate::risk::periods_per_year(interval, $cal),
))
}
}
};
}
#[cfg(any(feature = "fmp", feature = "alphavantage"))]
pub(crate) mod commodities;
#[cfg(any(
feature = "alphavantage",
feature = "crypto",
feature = "fmp",
feature = "polygon"
))]
pub(crate) mod crypto;
#[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
pub(crate) mod economic;
pub(crate) mod filings;
#[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
pub(crate) mod forex;
#[cfg(feature = "polygon")]
pub(crate) mod futures;
#[cfg(any(feature = "polygon", feature = "fmp"))]
pub(crate) mod indices;
#[cfg(any(feature = "fmp", feature = "alphavantage"))]
pub use commodities::Commodity;
#[cfg(any(
feature = "alphavantage",
feature = "crypto",
feature = "fmp",
feature = "polygon"
))]
pub use crypto::CryptoCoin;
#[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
pub use economic::EconomicIndicator;
pub use filings::Filings;
#[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
pub use forex::ForexPair;
#[cfg(feature = "polygon")]
pub use futures::FuturesContract;
#[cfg(any(feature = "polygon", feature = "fmp"))]
pub use indices::Index;
#[cfg(test)]
mod tests {
use super::*;
use crate::error::FinanceError;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn err() -> FinanceError {
FinanceError::NoProviderAvailable {
operation: crate::providers::Capability::QUOTE,
candidates: Vec::new(),
}
}
#[tokio::test]
async fn no_ttl_fetches_every_call() {
let cache: DomainCache<u32> = DomainCache::new(None);
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(42)
})
.await
.unwrap();
assert_eq!(v, 42);
}
assert_eq!(calls.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn ttl_caches_within_window() {
let cache: DomainCache<u32> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(7)
})
.await
.unwrap();
assert_eq!(v, 7);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn distinct_keys_cached_separately() {
let cache: DomainCache<String> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for key in ["usd", "eur", "usd", "eur"] {
let c = calls.clone();
let owned = key.to_string();
let v = cache
.get_or_try(key.to_string(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<String, FinanceError>(owned)
})
.await
.unwrap();
assert_eq!(v, key);
}
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn errors_are_not_cached() {
let cache: DomainCache<u32> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
let c = calls.clone();
let first = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Err::<u32, FinanceError>(err())
})
.await;
assert!(first.is_err());
let c = calls.clone();
let second = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(5)
})
.await
.unwrap();
assert_eq!(second, 5);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn concurrent_misses_dedup_to_one_fetch() {
let cache: Arc<DomainCache<u32>> =
Arc::new(DomainCache::new(Some(Duration::from_secs(60))));
let calls = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let cache = Arc::clone(&cache);
let c = calls.clone();
handles.push(tokio::spawn(async move {
cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
Ok::<u32, FinanceError>(1)
})
.await
.unwrap()
}));
}
for h in handles {
assert_eq!(h.await.unwrap(), 1);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
}