1use chrono::DateTime;
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7use crate::{
8 client::{Interval, MarketSeries, Series},
9 errors::{MarketError, MarketResult},
10 publishers::Publisher,
11};
12
13const BASE_URL: &str = "https://finnhub.io/api/v1/";
14
15#[derive(Debug)]
17pub struct Finnhub {
18 token: String,
19}
20
21#[derive(Debug, Clone)]
22pub enum FinnhubRequest {
23 Candle {
24 symbol: String,
25 resolution: String,
26 from: i64,
27 to: i64,
28 },
29 Quote {
30 symbol: String,
31 },
32}
33
34impl Finnhub {
35 pub fn new(token: impl Into<String>) -> Self {
36 Finnhub {
37 token: token.into(),
38 }
39 }
40
41 pub fn daily_series(&self, symbol: impl Into<String>, from: i64, to: i64) -> FinnhubRequest {
43 FinnhubRequest::Candle {
44 symbol: symbol.into(),
45 resolution: "D".to_string(),
46 from,
47 to,
48 }
49 }
50
51 pub fn weekly_series(&self, symbol: impl Into<String>, from: i64, to: i64) -> FinnhubRequest {
53 FinnhubRequest::Candle {
54 symbol: symbol.into(),
55 resolution: "W".to_string(),
56 from,
57 to,
58 }
59 }
60
61 pub fn monthly_series(&self, symbol: impl Into<String>, from: i64, to: i64) -> FinnhubRequest {
63 FinnhubRequest::Candle {
64 symbol: symbol.into(),
65 resolution: "M".to_string(),
66 from,
67 to,
68 }
69 }
70
71 pub fn intraday_series(
73 &self,
74 symbol: impl Into<String>,
75 from: i64,
76 to: i64,
77 interval: Interval,
78 ) -> MarketResult<FinnhubRequest> {
79 let resolution = match interval {
80 Interval::Min1 => "1".to_string(),
81 Interval::Min5 => "5".to_string(),
82 Interval::Min15 => "15".to_string(),
83 Interval::Min30 => "30".to_string(),
84 Interval::Hour1 => "60".to_string(),
85 _ => {
86 return Err(MarketError::UnsuportedInterval(format!(
87 "{} interval is not supported by Finnhub",
88 interval
89 )))
90 }
91 };
92 Ok(FinnhubRequest::Candle {
93 symbol: symbol.into(),
94 resolution,
95 from,
96 to,
97 })
98 }
99
100 pub fn quote(&self, symbol: impl Into<String>) -> FinnhubRequest {
102 FinnhubRequest::Quote {
103 symbol: symbol.into(),
104 }
105 }
106}
107
108impl Publisher for Finnhub {
109 type Request = FinnhubRequest;
110
111 fn create_endpoint(&self, request: &Self::Request) -> MarketResult<Url> {
112 let base_url = Url::parse(BASE_URL)?;
113 match request {
114 FinnhubRequest::Candle {
115 symbol,
116 resolution,
117 from,
118 to,
119 } => {
120 let mut url = base_url.join("stock/candle")?;
121 url.query_pairs_mut()
122 .append_pair("symbol", symbol)
123 .append_pair("resolution", resolution)
124 .append_pair("from", &from.to_string())
125 .append_pair("to", &to.to_string())
126 .append_pair("token", &self.token);
127 Ok(url)
128 }
129 FinnhubRequest::Quote { symbol } => {
130 let mut url = base_url.join("quote")?;
131 url.query_pairs_mut()
132 .append_pair("symbol", symbol)
133 .append_pair("token", &self.token);
134 Ok(url)
135 }
136 }
137 }
138
139 fn transform_data(&self, data: String, request: &Self::Request) -> MarketResult<MarketSeries> {
140 match request {
141 FinnhubRequest::Candle {
142 symbol, resolution, ..
143 } => {
144 let candles: FinnhubCandles = serde_json::from_str(&data)?;
145
146 let status = match candles.status {
147 Some(ref s) => s.as_str(),
148 None => {
149 if let Some(ref err) = candles.error {
150 return Err(MarketError::DownloadedData(format!(
151 "Finnhub error: {}",
152 err
153 )));
154 }
155 return Err(MarketError::DownloadedData(format!(
156 "Finnhub response missing status. Response: {}",
157 data
158 )));
159 }
160 };
161
162 if status != "ok" {
163 return Err(MarketError::DownloadedData(format!(
164 "Error returned from Finnhub: {}",
165 candles.error.unwrap_or_else(|| status.to_string())
166 )));
167 }
168
169 let t = candles
170 .t
171 .ok_or_else(|| MarketError::DownloadedData("Missing timestamps".to_string()))?;
172 let o = candles.o.ok_or_else(|| {
173 MarketError::DownloadedData("Missing open prices".to_string())
174 })?;
175 let h = candles.h.ok_or_else(|| {
176 MarketError::DownloadedData("Missing high prices".to_string())
177 })?;
178 let l = candles
179 .l
180 .ok_or_else(|| MarketError::DownloadedData("Missing low prices".to_string()))?;
181 let c = candles.c.ok_or_else(|| {
182 MarketError::DownloadedData("Missing close prices".to_string())
183 })?;
184 let v = candles
185 .v
186 .ok_or_else(|| MarketError::DownloadedData("Missing volumes".to_string()))?;
187
188 let mut data_series: Vec<Series> = Vec::with_capacity(t.len());
189 for i in 0..t.len() {
190 let datetime = DateTime::from_timestamp(t[i], 0).ok_or_else(|| {
191 MarketError::ParsingError(format!("Unable to parse timestamp: {}", t[i]))
192 })?;
193
194 data_series.push(Series {
195 datetime: datetime.naive_utc(),
196 open: o[i],
197 close: c[i],
198 high: h[i],
199 low: l[i],
200 volume: v[i] as f64,
201 });
202 }
203
204 data_series.sort_by_key(|item| item.datetime);
205
206 Ok(MarketSeries {
207 symbol: symbol.clone(),
208 interval: match resolution.as_str() {
209 "D" => Interval::Daily,
210 "W" => Interval::Weekly,
211 "M" => Interval::Monthly,
212 _ => Interval::Daily,
213 },
214 data: data_series,
215 })
216 }
217 FinnhubRequest::Quote { symbol } => {
218 let quote: FinnhubQuote = serde_json::from_str(&data)?;
219
220 if quote.t == 0 {
222 return Err(MarketError::DownloadedData(format!(
223 "Finnhub quote returned no data for symbol: {}",
224 symbol
225 )));
226 }
227
228 let datetime = DateTime::from_timestamp(quote.t, 0).ok_or_else(|| {
229 MarketError::ParsingError(format!("Unable to parse timestamp: {}", quote.t))
230 })?;
231
232 let series = Series {
233 datetime: datetime.naive_utc(),
234 open: quote.o,
235 close: quote.c,
236 high: quote.h,
237 low: quote.l,
238 volume: 0.0, };
240
241 Ok(MarketSeries {
242 symbol: symbol.clone(),
243 interval: Interval::Daily,
244 data: vec![series],
245 })
246 }
247 }
248 }
249}
250
251#[derive(Debug, Deserialize, Serialize)]
252struct FinnhubCandles {
253 #[serde(rename = "c")]
254 c: Option<Vec<f32>>,
255 #[serde(rename = "h")]
256 h: Option<Vec<f32>>,
257 #[serde(rename = "l")]
258 l: Option<Vec<f32>>,
259 #[serde(rename = "o")]
260 o: Option<Vec<f32>>,
261 #[serde(rename = "s")]
262 status: Option<String>,
263 #[serde(rename = "t")]
264 t: Option<Vec<i64>>,
265 #[serde(rename = "v")]
266 v: Option<Vec<u64>>,
267 #[serde(rename = "error")]
268 error: Option<String>,
269}
270
271#[derive(Debug, Deserialize, Serialize)]
272struct FinnhubQuote {
273 #[serde(rename = "c")]
274 c: f32,
275 #[serde(rename = "h")]
276 h: f32,
277 #[serde(rename = "l")]
278 l: f32,
279 #[serde(rename = "o")]
280 o: f32,
281 #[serde(rename = "pc")]
282 pc: f32,
283 #[serde(rename = "t")]
284 t: i64,
285}