1use 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#[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 pub fn new() -> Self {
47 YahooFin {}
48 }
49
50 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 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 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 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 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}