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}