1use 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#[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 interval: String,
41 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 pub fn new() -> Self {
64 YahooFin {
65 ..Default::default()
66 }
67 }
68
69 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 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 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 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.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.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.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.clear();
200 result
201 }
202}
203
204#[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 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 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 let date = datetime.date_naive();
317 timestamps.push(date);
318 }
319 Err(err) => {
320 result.push(Err(err));
321 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 }
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
358impl 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}