1use crate::{fetch, DataError};
4use pine_core::{Data, DataProvider, Ohlcv, ProviderError, SymInfo, Timeframe};
5use serde::Deserialize;
6
7#[derive(Debug, Clone)]
22pub struct YahooSource {
23 range: String,
24}
25
26impl Default for YahooSource {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl YahooSource {
33 pub fn new() -> Self {
34 Self {
35 range: "1mo".to_string(),
36 }
37 }
38
39 pub fn range(mut self, range: &str) -> Self {
41 self.range = range.to_string();
42 self
43 }
44
45 fn interval(tf: &Timeframe) -> String {
48 match tf.as_minutes() {
49 Some(minutes) if tf.is_minutes() && minutes % 60 == 0 => format!("{}h", minutes / 60),
50 _ if tf.is_minutes() => format!("{}m", tf.multiplier),
51 _ if tf.is_daily() => format!("{}d", tf.multiplier),
52 _ if tf.is_weekly() => format!("{}wk", tf.multiplier),
53 _ if tf.is_monthly() => format!("{}mo", tf.multiplier),
54 _ => format!("{}m", tf.multiplier),
55 }
56 }
57}
58
59#[derive(Debug, Deserialize)]
60struct HttpResult {
61 chart: Chart,
62}
63
64#[derive(Debug, Deserialize)]
65struct Chart {
66 result: Option<Vec<Res>>,
67 error: Option<ChartError>,
68}
69
70#[derive(Debug, Deserialize)]
71struct ChartError {
72 code: String,
73 description: String,
74}
75
76#[derive(Debug, Deserialize)]
77struct Res {
78 meta: Metadata,
79 #[serde(default)]
80 timestamp: Vec<i64>,
81 indicators: Indicators,
82}
83
84#[derive(Debug, Deserialize)]
85#[serde(rename_all = "camelCase")]
86struct Metadata {
87 exchange_name: Option<String>,
88 currency: Option<String>,
89}
90
91#[derive(Debug, Deserialize)]
92struct Indicators {
93 #[serde(default)]
94 quote: Vec<Quote>,
95}
96
97#[derive(Debug, Default, Deserialize)]
98struct Quote {
99 #[serde(default)]
100 open: Vec<Option<f64>>,
101 #[serde(default)]
102 high: Vec<Option<f64>>,
103 #[serde(default)]
104 low: Vec<Option<f64>>,
105 #[serde(default)]
106 close: Vec<Option<f64>>,
107 #[serde(default)]
108 volume: Vec<Option<f64>>,
109}
110
111impl DataProvider for YahooSource {
112 fn request(&self, symbol: &str, timeframe: Timeframe) -> Result<Data, ProviderError> {
113 let url = format!(
114 "https://query1.finance.yahoo.com/v8/finance/chart/{}?interval={}&range={}",
115 symbol,
116 Self::interval(&timeframe),
117 self.range
118 );
119 let body = fetch(&url)?;
120
121 let bad = |message: String| DataError::Provider {
122 provider: "yahoo",
123 message,
124 };
125
126 let response: HttpResult =
127 serde_json::from_str(&body).map_err(|e| bad(format!("{e}: {body:.200}")))?;
128
129 if let Some(error) = response.chart.error {
130 return Err(bad(format!("{}: {}", error.code, error.description)).into());
131 }
132
133 let result = response
134 .chart
135 .result
136 .and_then(|results| results.into_iter().next())
137 .ok_or_else(|| bad(format!("no data for {symbol}")))?;
138 let quote = result
139 .indicators
140 .quote
141 .into_iter()
142 .next()
143 .unwrap_or_default();
144
145 let rows = (0..result.timestamp.len())
146 .filter_map(|i| {
147 let at = |column: &[Option<f64>]| column.get(i).copied().flatten();
148 Some(Ohlcv {
149 time: result.timestamp.get(i)? * 1000,
151 open: at("e.open)?,
152 high: at("e.high)?,
153 low: at("e.low)?,
154 close: at("e.close)?,
155 volume: at("e.volume).unwrap_or(0.0),
156 })
157 })
158 .collect::<Vec<_>>();
159
160 let exchange = result.meta.exchange_name.unwrap_or("YAHOO".to_string());
161 let currency = result.meta.currency.unwrap_or_default();
162
163 let data = Data::from_ohlcv(rows).with_syminfo(SymInfo {
164 ticker: symbol.to_string(),
165 tickerid: format!("{exchange}:{symbol}"),
166 prefix: exchange,
167 currency,
168 ..SymInfo::default()
169 });
170
171 Ok(data)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_yahoo() {
183 let data = YahooSource::new()
184 .range("6mo")
185 .request("AAPL", "1D".parse().unwrap())
186 .unwrap();
187
188 assert_ne!(data.bars.len(), 0);
189 }
190}