Skip to main content

finance_query/domains/
crypto.rs

1//! Cryptocurrency coin query handle.
2//!
3//! Created via [`Providers::crypto`](crate::Providers::crypto).
4
5use crate::constants::{Interval, TimeRange};
6use crate::error::Result;
7use crate::models::chart::Chart;
8
9domain_handle! {
10    /// A cryptocurrency coin backed by configured data providers.
11    ///
12    /// Created via [`Providers::crypto`](crate::Providers::crypto).
13    pub struct CryptoCoin { id, id }
14    cache: crate::models::crypto::CryptoQuote, chart,
15    extra: {
16        #[cfg(feature = "defi")]
17        tvl_cache: crate::models::crypto::defi::ProtocolTvl,
18        #[cfg(feature = "defi")]
19        tvl_history_cache: Vec<crate::models::crypto::defi::TvlPoint>,
20    }
21}
22
23impl CryptoCoin {
24    /// Fetch the current quote for this coin priced in `vs_currency` (e.g., `"usd"`).
25    pub async fn quote(&self, vs_currency: &str) -> Result<crate::models::crypto::CryptoQuote> {
26        fetch_via_with!(
27            self,
28            id,
29            CRYPTO,
30            as_crypto,
31            CryptoQuote,
32            fetch_crypto_quote,
33            vs_currency,
34            crate::models::crypto::CryptoQuote
35        )
36    }
37
38    /// Fetch total value locked for this handle read as a **DeFi protocol
39    /// slug** (e.g. `providers.crypto("aave")`).
40    ///
41    /// Routed through `Capability::CRYPTO`; only DefiLlama serves it, so route
42    /// `CRYPTO` to include [`Provider::DefiLlama`](crate::Provider::DefiLlama).
43    /// The id is a protocol slug here, not a coin id — most DefiLlama slugs
44    /// happen to match their CoinGecko id, but not all do. The response is
45    /// cached on the handle, so a repeat call costs nothing.
46    #[cfg(feature = "defi")]
47    pub async fn tvl(&self) -> Result<crate::models::crypto::defi::ProtocolTvl> {
48        fetch_via!(
49            cache: tvl_cache,
50            self,
51            id,
52            CRYPTO,
53            as_crypto,
54            ProtocolTvl,
55            fetch_protocol_tvl,
56            crate::models::crypto::defi::ProtocolTvl
57        )
58    }
59
60    /// Fetch this protocol's full TVL history, oldest first.
61    ///
62    /// Same routing and slug semantics as [`tvl`](Self::tvl).
63    #[cfg(feature = "defi")]
64    pub async fn tvl_history(&self) -> Result<Vec<crate::models::crypto::defi::TvlPoint>> {
65        fetch_via!(
66            cache: tvl_history_cache,
67            self,
68            id,
69            CRYPTO,
70            as_crypto,
71            ProtocolTvlHistory,
72            fetch_protocol_tvl_history,
73            Vec<crate::models::crypto::defi::TvlPoint>
74        )
75    }
76
77    /// Fetch historical OHLCV candles for this coin priced in `vs_currency`.
78    ///
79    /// Unlike [`quote`](Self::quote) (which uses the coin *id*, e.g.
80    /// `"bitcoin"`), the `CHART` route is symbol-based, so the chart symbol is
81    /// built as `"{ID}-{VS}"` uppercased (e.g. `"BTC-USD"`). This resolves on
82    /// the default Yahoo route only when the handle's id is the coin's *ticker*
83    /// (`providers.crypto("BTC")`); coins identified by a CoinGecko id should
84    /// route `Capability::CHART` to a crypto-aware provider.
85    ///
86    /// With the `crypto` feature, `.route(Capability::CHART,
87    /// [Provider::CoinGecko])` serves history keylessly by coin id. CoinGecko
88    /// picks its own bar granularity from the requested range, so `interval` is
89    /// advisory there, and its OHLC endpoint carries no volume — those candles
90    /// report `volume: 0`.
91    pub async fn chart(
92        &self,
93        vs_currency: &str,
94        interval: Interval,
95        range: TimeRange,
96    ) -> Result<Chart> {
97        let symbol = chart_symbol(self.id(), vs_currency);
98        fetch_chart_via!(self, symbol, interval, range)
99    }
100
101    /// Fetch historical candles over `range` at a sensible default interval
102    /// ([`TimeRange::default_interval`]).
103    pub async fn history(&self, vs_currency: &str, range: TimeRange) -> Result<Chart> {
104        self.chart(vs_currency, range.default_interval(), range)
105            .await
106    }
107
108    /// Compute all technical indicators from this coin's chart data (priced in
109    /// `vs_currency`).
110    #[cfg(feature = "indicators")]
111    pub async fn indicators(
112        &self,
113        vs_currency: &str,
114        interval: Interval,
115        range: TimeRange,
116    ) -> Result<crate::indicators::IndicatorsSummary> {
117        let chart = self.chart(vs_currency, interval, range).await?;
118        Ok(crate::indicators::summary::calculate_indicators(
119            &chart.candles,
120        ))
121    }
122
123    /// Compute a single technical indicator from this coin's chart data.
124    #[cfg(feature = "indicators")]
125    pub async fn indicator(
126        &self,
127        indicator: crate::indicators::Indicator,
128        vs_currency: &str,
129        interval: Interval,
130        range: TimeRange,
131    ) -> Result<crate::indicators::IndicatorResult> {
132        let chart = self.chart(vs_currency, interval, range).await?;
133        Ok(crate::indicators::compute_indicator(indicator, &chart)?)
134    }
135
136    /// Fetch market-wide crypto news via `Capability::CRYPTO` (currently FMP
137    /// only). Not scoped to this handle's coin id.
138    pub async fn news(&self, limit: u32) -> Result<Vec<crate::models::corporate::news::News>> {
139        dispatch_via!(
140            self,
141            CRYPTO,
142            as_crypto,
143            CryptoNews,
144            fetch_crypto_news,
145            [],
146            limit
147        )
148    }
149
150    /// Compute a risk summary from this coin's chart data, annualised with the
151    /// 24/7 crypto calendar (365 days/year). `beta` is always `None`.
152    #[cfg(feature = "risk")]
153    pub async fn risk(
154        &self,
155        vs_currency: &str,
156        interval: Interval,
157        range: TimeRange,
158    ) -> Result<crate::risk::RiskSummary> {
159        let chart = self.chart(vs_currency, interval, range).await?;
160        Ok(crate::risk::compute_risk_summary_with_periods(
161            &chart.candles,
162            None,
163            crate::risk::periods_per_year(interval, crate::risk::TradingCalendar::Crypto),
164        ))
165    }
166}
167
168/// Build the `CHART` route symbol `"{ID}-{VS}"`, uppercased (e.g. `"BTC-USD"`)
169/// — the Yahoo crypto convention, valid when the handle id is the coin ticker.
170fn chart_symbol(id: &str, vs_currency: &str) -> String {
171    format!("{}-{}", id.to_uppercase(), vs_currency.to_uppercase())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn chart_symbol_uppercases_ticker_and_vs() {
180        assert_eq!(chart_symbol("BTC", "USD"), "BTC-USD");
181        assert_eq!(chart_symbol("eth", "eur"), "ETH-EUR");
182    }
183}