finance_query/models/chart/
stock_bars.rs1use crate::{FinanceError, Interval, Result, SortType};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[non_exhaustive]
9pub enum PriceAdjustment {
10 Unadjusted,
12 SplitAdjusted,
14}
15
16#[derive(Debug, Clone, Serialize)]
18pub struct StockBarsRequest {
19 pub(crate) symbol: String,
20 pub(crate) from: String,
21 pub(crate) to: String,
22 pub(crate) timespan: String,
23 pub(crate) adjustment: PriceAdjustment,
24 pub(crate) sort: SortType,
25 pub(crate) limit: u32,
26}
27
28impl StockBarsRequest {
29 pub fn new(
32 symbol: &str,
33 from: &str,
34 to: &str,
35 interval: Interval,
36 adjustment: PriceAdjustment,
37 ) -> Result<Self> {
38 crate::models::discovery::listings::date(from)?;
39 crate::models::discovery::listings::date(to)?;
40 if symbol.trim().is_empty() || symbol == "." || symbol == ".." || from > to {
41 return Err(FinanceError::InvalidParameter {
42 param: "stock_bars".into(),
43 reason: "symbol and ordered date bounds are required".into(),
44 });
45 }
46 let timespan = match interval {
47 Interval::OneMinute => "minute",
48 Interval::OneDay => "day",
49 _ => {
50 return Err(FinanceError::InvalidParameter {
51 param: "interval".into(),
52 reason: "stock pages support OneMinute and OneDay".into(),
53 });
54 }
55 };
56 Ok(Self {
57 symbol: symbol.into(),
58 from: from.into(),
59 to: to.into(),
60 timespan: timespan.into(),
61 adjustment,
62 sort: SortType::Asc,
63 limit: 50_000,
64 })
65 }
66
67 pub fn with_sort(mut self, order: SortType) -> Self {
69 self.sort = order;
70 self
71 }
72 pub fn with_page_size(mut self, limit: u32) -> Result<Self> {
74 if !(1..=50_000).contains(&limit) {
75 return Err(FinanceError::InvalidParameter {
76 param: "page_size".into(),
77 reason: "expected 1..=50000".into(),
78 });
79 }
80 self.limit = limit;
81 Ok(self)
82 }
83
84 pub fn symbol(&self) -> &str {
86 &self.symbol
87 }
88
89 pub fn from(&self) -> &str {
91 &self.from
92 }
93
94 pub fn to(&self) -> &str {
96 &self.to
97 }
98
99 pub fn interval(&self) -> Interval {
101 if self.timespan == "minute" {
102 Interval::OneMinute
103 } else {
104 Interval::OneDay
105 }
106 }
107
108 pub fn adjustment(&self) -> PriceAdjustment {
110 self.adjustment
111 }
112
113 pub fn sort(&self) -> SortType {
115 self.sort
116 }
117
118 pub fn page_size(&self) -> u32 {
120 self.limit
121 }
122
123 #[cfg(any(feature = "polygon", feature = "fmp"))]
124 pub(crate) fn split_adjusted(&self) -> bool {
125 self.adjustment == PriceAdjustment::SplitAdjusted
126 }
127
128 pub(crate) fn sort_param(&self) -> &'static str {
129 match self.sort {
130 SortType::Asc => "asc",
131 SortType::Desc => "desc",
132 }
133 }
134
135 pub(crate) fn cursor_identity(&self) -> String {
137 let adjustment = match self.adjustment {
138 PriceAdjustment::Unadjusted => "unadjusted",
139 PriceAdjustment::SplitAdjusted => "split",
140 };
141 serde_json::json!([
142 self.symbol,
143 self.from,
144 self.to,
145 self.timespan,
146 adjustment,
147 self.sort_param(),
148 self.limit
149 ])
150 .to_string()
151 }
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156#[non_exhaustive]
157pub struct StockBar {
158 pub timestamp_ms: i64,
160 pub open: f64,
162 pub high: f64,
164 pub low: f64,
166 pub close: f64,
168 pub volume: f64,
170 pub transactions: Option<u64>,
172 pub vwap: Option<f64>,
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn cursor_identity_is_pinned_to_the_query_fields() {
182 let request = StockBarsRequest::new(
183 "AAPL",
184 "2020-01-02",
185 "2020-01-03",
186 Interval::OneDay,
187 PriceAdjustment::Unadjusted,
188 )
189 .unwrap();
190 assert_eq!(
191 request.cursor_identity(),
192 r#"["AAPL","2020-01-02","2020-01-03","day","unadjusted","asc",50000]"#
193 );
194 let changed = request
195 .with_sort(SortType::Desc)
196 .with_page_size(10)
197 .unwrap();
198 assert_eq!(
199 changed.cursor_identity(),
200 r#"["AAPL","2020-01-02","2020-01-03","day","unadjusted","desc",10]"#
201 );
202 }
203}