Skip to main content

market_data/publishers/
yahoo_finance.rs

1//! Fetch time series stock data from [Yahoo Finance](https://finance.yahoo.com/)
2
3use chrono::DateTime;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use url::Url;
7
8use crate::{
9    client::{Interval, MarketSeries, Series},
10    errors::{MarketError, MarketResult},
11    publishers::Publisher,
12};
13
14const BASE_URL: &str = "https://query1.finance.yahoo.com/v8/finance/chart/";
15
16/// Fetch time series stock data from [Yahoo Finance](https://finance.yahoo.com/), implements Publisher trait
17#[derive(Debug, Default)]
18pub struct YahooFin {}
19
20#[derive(Debug, Clone)]
21pub struct YahooRequest {
22    symbol: String,
23    interval: String,
24    range: YahooRange,
25    interval_enum: Interval,
26}
27
28#[derive(Debug, Default, Clone, Copy)]
29pub enum YahooRange {
30    Day1,
31    Day5,
32    Month1,
33    Month3,
34    #[default]
35    Month6,
36    Year1,
37    Year2,
38    Year5,
39    Year10,
40    Ytd,
41    Max,
42}
43
44impl YahooFin {
45    /// create new instance of YahooFin
46    pub fn new() -> Self {
47        YahooFin {}
48    }
49
50    /// Request for intraday series
51    pub fn intraday_series(
52        &self,
53        symbol: impl Into<String>,
54        interval: Interval,
55        range: YahooRange,
56    ) -> MarketResult<YahooRequest> {
57        let interval_str = match interval {
58            Interval::Min1 => "1m".to_string(),
59            Interval::Min5 => "5m".to_string(),
60            Interval::Min15 => "15m".to_string(),
61            Interval::Min30 => "30m".to_string(),
62            Interval::Hour1 => "1h".to_string(),
63            _ => {
64                return Err(MarketError::UnsuportedInterval(format!(
65                    "{} interval is not supported by Yahoo Finance intraday",
66                    interval
67                )))
68            }
69        };
70        Ok(YahooRequest {
71            symbol: symbol.into(),
72            interval: interval_str,
73            range,
74            interval_enum: interval,
75        })
76    }
77
78    /// Request for daily series
79    pub fn daily_series(&self, symbol: impl Into<String>, range: YahooRange) -> YahooRequest {
80        YahooRequest {
81            symbol: symbol.into(),
82            interval: "1d".to_string(),
83            range,
84            interval_enum: Interval::Daily,
85        }
86    }
87
88    /// Request for weekly series
89    pub fn weekly_series(&self, symbol: impl Into<String>, range: YahooRange) -> YahooRequest {
90        YahooRequest {
91            symbol: symbol.into(),
92            interval: "1wk".to_string(),
93            range,
94            interval_enum: Interval::Weekly,
95        }
96    }
97
98    /// Request for monthly series
99    pub fn monthly_series(&self, symbol: impl Into<String>, range: YahooRange) -> YahooRequest {
100        YahooRequest {
101            symbol: symbol.into(),
102            interval: "1m".to_string(),
103            range,
104            interval_enum: Interval::Monthly,
105        }
106    }
107}
108
109impl Publisher for YahooFin {
110    type Request = YahooRequest;
111
112    fn create_endpoint(&self, request: &Self::Request) -> MarketResult<Url> {
113        let base_url = Url::parse(BASE_URL)?;
114        let mut url = base_url.join(&request.symbol)?;
115        {
116            let mut pairs = url.query_pairs_mut();
117            pairs.append_pair("metrics", "high");
118            pairs.append_pair("interval", &request.interval);
119            pairs.append_pair("range", &request.range.to_string());
120        }
121        Ok(url)
122    }
123
124    fn transform_data(&self, data: String, request: &Self::Request) -> MarketResult<MarketSeries> {
125        let yahoo_prices: YahooPrices = serde_json::from_str(&data)?;
126
127        if let Some(error) = &yahoo_prices.chart.error {
128            return Err(MarketError::DownloadedData(format!(
129                "Yahoo Finance error: {}",
130                error
131            )));
132        }
133
134        if yahoo_prices.chart.result.is_empty() {
135            return Err(MarketError::DownloadedData(
136                "Yahoo Finance returned empty result".to_string(),
137            ));
138        }
139
140        let result = &yahoo_prices.chart.result[0];
141        let mut data_series: Vec<Series> = Vec::new();
142
143        for (i, timestamp) in result.timestamp.iter().enumerate() {
144            let datetime = DateTime::from_timestamp(*timestamp, 0).ok_or_else(|| {
145                MarketError::ParsingError("Unable to parse timestamp".to_string())
146            })?;
147
148            if result.indicators.quote.is_empty() {
149                continue;
150            }
151
152            let quote = &result.indicators.quote[0];
153
154            // Check if values exist for this timestamp
155            let open = quote.open.get(i).and_then(|v| *v);
156            let high = quote.high.get(i).and_then(|v| *v);
157            let low = quote.low.get(i).and_then(|v| *v);
158            let close = quote.close.get(i).and_then(|v| *v);
159            let volume = quote.volume.get(i).and_then(|v| *v);
160
161            if let (Some(o), Some(h), Some(l), Some(c), Some(v)) = (open, high, low, close, volume)
162            {
163                data_series.push(Series {
164                    datetime: datetime.naive_utc(),
165                    open: o as f32,
166                    high: h as f32,
167                    low: l as f32,
168                    close: c as f32,
169                    volume: v as f64,
170                });
171            }
172        }
173
174        Ok(MarketSeries {
175            symbol: result.meta.symbol.clone(),
176            interval: request.interval_enum.clone(),
177            data: data_series,
178        })
179    }
180}
181
182#[derive(Debug, Deserialize, Serialize)]
183struct YahooPrices {
184    chart: Chart,
185}
186
187#[derive(Debug, Deserialize, Serialize)]
188struct Chart {
189    result: Vec<YahooResult>,
190    error: Option<serde_json::Value>,
191}
192
193#[derive(Debug, Deserialize, Serialize)]
194struct YahooResult {
195    meta: Meta,
196    timestamp: Vec<i64>,
197    indicators: Indicators,
198}
199
200#[derive(Debug, Deserialize, Serialize)]
201struct Meta {
202    symbol: String,
203}
204
205#[derive(Debug, Deserialize, Serialize)]
206struct Indicators {
207    quote: Vec<Quote>,
208}
209
210#[derive(Debug, Deserialize, Serialize)]
211struct Quote {
212    volume: Vec<Option<i64>>,
213    close: Vec<Option<f64>>,
214    low: Vec<Option<f64>>,
215    open: Vec<Option<f64>>,
216    high: Vec<Option<f64>>,
217}
218
219impl fmt::Display for YahooRange {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        let s = match self {
222            YahooRange::Day1 => "1d",
223            YahooRange::Day5 => "5d",
224            YahooRange::Month1 => "1mo",
225            YahooRange::Month3 => "3mo",
226            YahooRange::Month6 => "6mo",
227            YahooRange::Year1 => "1y",
228            YahooRange::Year2 => "2y",
229            YahooRange::Year5 => "5y",
230            YahooRange::Year10 => "10y",
231            YahooRange::Ytd => "ytd",
232            YahooRange::Max => "max",
233        };
234        write!(f, "{}", s)
235    }
236}