Skip to main content

market_data/publishers/
massive.rs

1//! Fetch time series stock data from [Massive](https://massive.com/docs/rest/stocks/aggregates/custom-bars)
2
3use 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://api.massive.com/v2/aggs/ticker/";
14
15/// Fetch time series stock data from [Massive](https://massive.com/), implements Publisher trait
16#[derive(Debug)]
17pub struct Massive {
18    token: String,
19}
20
21#[derive(Debug, Clone)]
22pub struct MassiveRequest {
23    symbol: String,
24    timespan: String,
25    multiplier: i32,
26    from_date: String,
27    to_date: String,
28    limit: i32,
29    interval: Interval,
30}
31
32impl Massive {
33    /// create new instance of Massive
34    pub fn new(token: impl Into<String>) -> Self {
35        Massive {
36            token: token.into(),
37        }
38    }
39
40    /// Request for intraday series
41    pub fn intraday_series(
42        &self,
43        symbol: impl Into<String>,
44        from_date: impl Into<String>,
45        to_date: impl Into<String>,
46        interval: Interval,
47        limit: i32,
48    ) -> MarketResult<MassiveRequest> {
49        let (timespan, multiplier) = match interval {
50            Interval::Min1 => ("minute", 1),
51            Interval::Min5 => ("minute", 5),
52            Interval::Min15 => ("minute", 15),
53            Interval::Min30 => ("minute", 30),
54            Interval::Hour1 => ("hour", 1),
55            Interval::Hour2 => ("hour", 2),
56            Interval::Hour4 => ("hour", 4),
57            _ => {
58                return Err(MarketError::UnsuportedInterval(format!(
59                    "{} interval is not supported by Massive",
60                    interval
61                )))
62            }
63        };
64        Ok(MassiveRequest {
65            symbol: symbol.into(),
66            timespan: timespan.into(),
67            multiplier,
68            from_date: from_date.into(),
69            to_date: to_date.into(),
70            limit,
71            interval,
72        })
73    }
74
75    /// Request for daily series
76    pub fn daily_series(
77        &self,
78        symbol: impl Into<String>,
79        from_date: impl Into<String>,
80        to_date: impl Into<String>,
81        limit: i32,
82    ) -> MassiveRequest {
83        MassiveRequest {
84            symbol: symbol.into(),
85            timespan: "day".to_string(),
86            multiplier: 1,
87            from_date: from_date.into(),
88            to_date: to_date.into(),
89            limit,
90            interval: Interval::Daily,
91        }
92    }
93
94    /// Request for weekly series
95    pub fn weekly_series(
96        &self,
97        symbol: impl Into<String>,
98        from_date: impl Into<String>,
99        to_date: impl Into<String>,
100        limit: i32,
101    ) -> MassiveRequest {
102        MassiveRequest {
103            symbol: symbol.into(),
104            timespan: "week".to_string(),
105            multiplier: 1,
106            from_date: from_date.into(),
107            to_date: to_date.into(),
108            limit,
109            interval: Interval::Weekly,
110        }
111    }
112
113    /// Request for monthly series
114    pub fn monthly_series(
115        &self,
116        symbol: impl Into<String>,
117        from_date: impl Into<String>,
118        to_date: impl Into<String>,
119        limit: i32,
120    ) -> MassiveRequest {
121        MassiveRequest {
122            symbol: symbol.into(),
123            timespan: "month".to_string(),
124            multiplier: 1,
125            from_date: from_date.into(),
126            to_date: to_date.into(),
127            limit,
128            interval: Interval::Monthly,
129        }
130    }
131}
132
133impl Publisher for Massive {
134    type Request = MassiveRequest;
135
136    fn create_endpoint(&self, request: &Self::Request) -> MarketResult<Url> {
137        let base_url = Url::parse(BASE_URL)?;
138        let mut url = base_url.join(&format!(
139            "{}/range/{}/{}/{}/{}",
140            request.symbol,
141            request.multiplier,
142            request.timespan,
143            request.from_date,
144            request.to_date,
145        ))?;
146        {
147            let mut pairs = url.query_pairs_mut();
148            pairs.append_pair("sort", "asc");
149            pairs.append_pair("limit", &request.limit.to_string());
150            pairs.append_pair("apiKey", &self.token);
151        }
152        Ok(url)
153    }
154
155    fn transform_data(&self, data: String, request: &Self::Request) -> MarketResult<MarketSeries> {
156        let prices: MassivePrices = serde_json::from_str(&data)?;
157
158        if prices.status != "OK" {
159            return Err(MarketError::DownloadedData(format!(
160                "Downloaded data status is: {}",
161                prices.status
162            )));
163        }
164
165        let mut data_series: Vec<Series> = Vec::with_capacity(prices.time_series.len());
166
167        for series in prices.time_series.iter() {
168            let datetime = DateTime::from_timestamp_millis(series.t).ok_or_else(|| {
169                MarketError::ParsingError("Unable to parse the timestamp".to_string())
170            })?;
171
172            data_series.push(Series {
173                datetime: datetime.naive_utc(),
174                open: series.o,
175                close: series.c,
176                high: series.h,
177                low: series.l,
178                volume: series.v,
179            })
180        }
181
182        Ok(MarketSeries {
183            symbol: prices.ticker.clone(),
184            interval: request.interval.clone(),
185            data: data_series,
186        })
187    }
188}
189
190#[derive(Debug, Serialize, Deserialize)]
191struct MassivePrices {
192    #[serde(rename = "results")]
193    time_series: Vec<TimeSeriesData>,
194    status: String,
195    ticker: String,
196}
197
198#[derive(Debug, Serialize, Deserialize)]
199struct TimeSeriesData {
200    c: f32,
201    h: f32,
202    l: f32,
203    o: f32,
204    t: i64,
205    v: f64,
206}