market_data/publishers/
yahoo_finance.rs

1//! Fetch time series stock data from [Yahoo Finance](https://finance.yahoo.com/)
2///
3/// The Yahoo Finance API is free to use for personal projects. However, commercial usage of the API requires a paid subscription.
4/// This means that developers working on commercial projects will need to pay for a Yahoo Finance API subscription.
5///
6/// The Yahoo Finance API is updated once per day. This means that developers will need to use other data sources if they want real-time data.
7///
8/// Example:
9/// validRanges: 1d, 5d, 1mo, 3mo , 6mo, 1y, 2y, 5y, 10y, ytd, max
10/// https://query1.finance.yahoo.com/v8/finance/chart/AAPL?metrics=high&interval=1d&range=5d
11///
12use chrono::{DateTime, NaiveDate};
13use serde::{Deserialize, Serialize};
14use std::fmt;
15use url::Url;
16
17use crate::{
18    client::{Interval, MarketSeries, Series},
19    errors::{MarketError, MarketResult},
20    publishers::Publisher,
21    rest_call::Client,
22};
23
24const BASE_URL: &str = "https://query1.finance.yahoo.com/v8/finance/chart/";
25
26/// Fetch time series stock data from [Yahoo Finance](https://finance.yahoo.com/), implements Publisher trait
27#[derive(Debug, Default)]
28pub struct YahooFin {
29    requests: Vec<YahooRequest>,
30    endpoints: Vec<url::Url>,
31    data: Vec<YahooPrices>,
32    interval: Vec<Interval>,
33}
34
35#[derive(Debug, Default)]
36pub struct YahooRequest {
37    symbol: String,
38    // The time interval between two data points supported by yahoo finance:1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo
39    // I'm mapping to lib Interval struct
40    interval: String,
41    // validRanges: 1d, 5d, 1mo, 3mo , 6mo, 1y, 2y, 5y, 10y, ytd, max
42    range: YahooRange,
43}
44
45#[derive(Debug, Default)]
46pub enum YahooRange {
47    Day1,
48    Day5,
49    Month1,
50    Month3,
51    #[default]
52    Month6,
53    Year1,
54    Year2,
55    Year5,
56    Year10,
57    Ytd,
58    Max,
59}
60
61impl YahooFin {
62    /// create new instance of Twelvedata
63    pub fn new() -> Self {
64        YahooFin {
65            ..Default::default()
66        }
67    }
68
69    /// Request for intraday series
70    /// supporting the following intervals: 1min, 5min, 15min, 30min, 1h for intraday
71    pub fn intraday_series(
72        &mut self,
73        symbol: impl Into<String>,
74        interval: Interval,
75        range: YahooRange,
76    ) -> MarketResult<()> {
77        self.interval.push(interval.clone());
78        let interval = match interval {
79            Interval::Min1 => "1m".to_string(),
80            Interval::Min5 => "5m".to_string(),
81            Interval::Min15 => "15m".to_string(),
82            Interval::Min30 => "30m".to_string(),
83            Interval::Hour1 => "1h".to_string(),
84            _ => Err(MarketError::UnsuportedInterval(format!(
85                "{} interval is not supported by AlphaVantage",
86                interval
87            )))?,
88        };
89        self.requests.push(YahooRequest {
90            symbol: symbol.into(),
91            interval,
92            range,
93        });
94        Ok(())
95    }
96
97    /// Request for daily series
98    pub fn daily_series(&mut self, symbol: impl Into<String>, range: YahooRange) -> () {
99        self.interval.push(Interval::Daily);
100        self.requests.push(YahooRequest {
101            symbol: symbol.into(),
102            interval: "1d".to_string(),
103            range,
104        });
105    }
106
107    /// Request for weekly series
108    pub fn weekly_series(&mut self, symbol: impl Into<String>, range: YahooRange) -> () {
109        self.interval.push(Interval::Weekly);
110        self.requests.push(YahooRequest {
111            symbol: symbol.into(),
112            interval: "1wk".to_string(),
113            range,
114        });
115    }
116
117    /// Request for monthly series
118    pub fn monthly_series(&mut self, symbol: impl Into<String>, range: YahooRange) -> () {
119        self.interval.push(Interval::Monthly);
120        self.requests.push(YahooRequest {
121            symbol: symbol.into(),
122            interval: "1m".to_string(),
123            range,
124        });
125    }
126}
127
128impl Publisher for YahooFin {
129    fn create_endpoint(&mut self) -> MarketResult<()> {
130        let base_url = Url::parse(BASE_URL)?;
131        self.endpoints = self
132            .requests
133            .iter()
134            .map(|request| {
135                let constructed_url = base_url
136                    .join(&format!(
137                        "{}?metrics=high&interval={}&range={}",
138                        request.symbol, request.interval, request.range,
139                    ))
140                    .unwrap();
141                constructed_url
142            })
143            .collect();
144        // self.requests have to be consumed once used for creating the endpoints
145        self.requests.clear();
146        Ok(())
147    }
148
149    #[cfg(feature = "use-sync")]
150    fn get_data(&mut self) -> MarketResult<()> {
151        let rest_client = Client::new();
152        for endpoint in &self.endpoints {
153            let response = rest_client.get_data(endpoint)?;
154            let body = response.into_string()?;
155
156            let prices: YahooPrices = serde_json::from_str(&body)?;
157            self.data.push(prices);
158        }
159        // self.endpoints have to be consumed once the data was downloaded for requested URL
160        self.endpoints.clear();
161
162        Ok(())
163    }
164
165    #[cfg(feature = "use-async")]
166    async fn get_data(&mut self) -> MarketResult<()> {
167        let client = Client::new();
168        for endpoint in &self.endpoints {
169            let response = client.get_data(endpoint).await?;
170            let body = response.text().await?;
171
172            let prices: YahooPrices = serde_json::from_str(&body)?;
173            self.data.push(prices);
174        }
175
176        // self.endpoints have to be consumed once the data was downloaded for requested URL
177        self.endpoints.clear();
178
179        Ok(())
180    }
181
182    fn to_writer(&self, writer: impl std::io::Write) -> MarketResult<()> {
183        serde_json::to_writer(writer, &self.data).map_err(|err| {
184            MarketError::ToWriter(format!("Unable to write to writer, got the error: {}", err))
185        })?;
186        Ok(())
187    }
188
189    fn transform_data(&mut self) -> Vec<MarketResult<MarketSeries>> {
190        let mut result = Vec::new();
191        for (i, data) in self.data.iter().enumerate() {
192            let parsed_data = transform(data, self.interval[i].clone());
193            for data in parsed_data.into_iter() {
194                result.push(data)
195            }
196        }
197
198        // self.data have to be consumed once the data is transformed to MarketSeries
199        self.data.clear();
200        result
201    }
202}
203
204// Yahoo API Deserialization
205
206#[derive(Debug, Serialize, Deserialize)]
207struct YahooPrices {
208    chart: Chart,
209}
210
211#[derive(Debug, Serialize, Deserialize)]
212struct Chart {
213    result: Vec<Result>,
214    error: Option<String>,
215}
216
217#[derive(Debug, Serialize, Deserialize)]
218struct Result {
219    meta: Meta,
220    timestamp: Vec<i64>,
221    indicators: Indicators,
222}
223
224#[derive(Debug, Serialize, Deserialize)]
225struct Meta {
226    currency: String,
227    symbol: String,
228    #[serde(rename = "exchangeName")]
229    exchange_name: String,
230    #[serde(rename = "instrumentType")]
231    instrument_type: String,
232    #[serde(rename = "firstTradeDate")]
233    first_trade_date: i64,
234    #[serde(rename = "regularMarketTime")]
235    regular_market_time: i64,
236    #[serde(rename = "hasPrePostMarketData")]
237    has_pre_post_market_data: bool,
238    gmtoffset: i64,
239    timezone: String,
240    #[serde(rename = "exchangeTimezoneName")]
241    exchange_timezone_name: String,
242    #[serde(rename = "regularMarketPrice")]
243    regular_market_price: f64,
244    #[serde(rename = "chartPreviousClose")]
245    chart_previous_close: f64,
246    #[serde(rename = "priceHint")]
247    price_hint: i32,
248    #[serde(rename = "currentTradingPeriod")]
249    current_trading_period: CurrentTradingPeriod,
250    #[serde(rename = "dataGranularity")]
251    data_granularity: String,
252    range: String,
253    #[serde(rename = "validRanges")]
254    valid_ranges: Vec<String>,
255}
256
257#[derive(Debug, Serialize, Deserialize)]
258struct CurrentTradingPeriod {
259    pre: TradingPeriod,
260    regular: TradingPeriod,
261    post: TradingPeriod,
262}
263
264#[derive(Debug, Serialize, Deserialize)]
265struct TradingPeriod {
266    timezone: String,
267    end: i64,
268    start: i64,
269    gmtoffset: i64,
270}
271
272#[derive(Debug, Serialize, Deserialize)]
273struct Indicators {
274    quote: Vec<Quote>,
275    adjclose: Option<Vec<AdjClose>>,
276}
277
278#[derive(Debug, Serialize, Deserialize)]
279struct Quote {
280    volume: Vec<i64>,
281    close: Vec<f64>,
282    low: Vec<f64>,
283    open: Vec<f64>,
284    high: Vec<f64>,
285}
286
287#[derive(Debug, Serialize, Deserialize)]
288struct AdjClose {
289    adjclose: Vec<f64>,
290}
291
292fn transform(data: &YahooPrices, interval: Interval) -> Vec<MarketResult<MarketSeries>> {
293    let mut result = Vec::new();
294
295    // validate the data, first check is status
296    if let Some(error) = &data.chart.error {
297        result.push(Err(MarketError::DownloadedData(format!(
298            "The return data has some error: {}",
299            error
300        ))));
301    }
302
303    for data in data.chart.result.iter() {
304        let mut data_series: Vec<Series> = Vec::new();
305        let mut timestamps: Vec<NaiveDate> = Vec::new();
306
307        for timestamp in data.timestamp.iter() {
308            // Create a NaiveDateTime from the Unix timestamp
309            let datetime = DateTime::from_timestamp(timestamp.clone(), 0).ok_or(
310                MarketError::ParsingError(format!("Unable to parse the timestamp")),
311            );
312
313            match datetime {
314                Ok(datetime) => {
315                    // Extract the date part
316                    let date = datetime.date_naive();
317                    timestamps.push(date);
318                }
319                Err(err) => {
320                    result.push(Err(err));
321                    // TO FIX !!!, need to continue with outer loop
322                    continue;
323                }
324            }
325        }
326
327        for series in data.indicators.quote.iter() {
328            for j in 1..series.open.len() - 1 {
329                let open: f32 = series.open[j] as f32;
330                let close: f32 = series.close[j] as f32;
331                let high: f32 = series.high[j] as f32;
332                let low: f32 = series.low[j] as f32;
333                let volume: f32 = series.volume[j] as f32;
334
335                data_series.push(Series {
336                    date: timestamps[j],
337                    open,
338                    close,
339                    high,
340                    low,
341                    volume,
342                })
343            }
344
345            // sort the series by date
346            //data_series.sort_by_key(|item| item.date);
347        }
348        result.push(Ok(MarketSeries {
349            symbol: data.meta.symbol.clone(),
350            interval: interval.clone(),
351            data: data_series,
352        }))
353    }
354
355    result
356}
357
358// validRanges: 1d, 5d, 1mo, 3mo , 6mo, 1y, 2y, 5y, 10y, ytd, max
359impl fmt::Display for YahooRange {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let range_str = match self {
362            YahooRange::Day1 => "1d",
363            YahooRange::Day5 => "5d",
364            YahooRange::Month1 => "1mo",
365            YahooRange::Month3 => "3mo",
366            YahooRange::Month6 => "6mo",
367            YahooRange::Year1 => "1y",
368            YahooRange::Year2 => "2y",
369            YahooRange::Year5 => "5y",
370            YahooRange::Year10 => "10y",
371            YahooRange::Ytd => "ytd",
372            YahooRange::Max => "max",
373        };
374
375        write!(f, "{}", range_str)
376    }
377}