1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::{error::Error, slice};
6
7use use_market_price::MarketPrice;
8use use_return::{ReturnError, SimpleReturn};
9
10pub mod prelude {
12 pub use crate::{PricePoint, PriceSeries, PriceSeriesError, SeriesName, SeriesNameError};
13}
14
15#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub struct SeriesName(String);
18
19impl SeriesName {
20 pub fn new(value: impl AsRef<str>) -> Result<Self, SeriesNameError> {
26 let trimmed = value.as_ref().trim();
27 if trimmed.is_empty() {
28 Err(SeriesNameError::Empty)
29 } else {
30 Ok(Self(trimmed.to_string()))
31 }
32 }
33
34 #[must_use]
36 pub fn as_str(&self) -> &str {
37 &self.0
38 }
39}
40
41impl AsRef<str> for SeriesName {
42 fn as_ref(&self) -> &str {
43 self.as_str()
44 }
45}
46
47impl fmt::Display for SeriesName {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49 formatter.write_str(self.as_str())
50 }
51}
52
53impl FromStr for SeriesName {
54 type Err = SeriesNameError;
55
56 fn from_str(value: &str) -> Result<Self, Self::Err> {
57 Self::new(value)
58 }
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum SeriesNameError {
64 Empty,
66}
67
68impl fmt::Display for SeriesNameError {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Self::Empty => formatter.write_str("series name cannot be empty"),
72 }
73 }
74}
75
76impl Error for SeriesNameError {}
77
78#[derive(Clone, Debug, PartialEq)]
80pub struct PricePoint {
81 label: Option<String>,
82 price: MarketPrice,
83}
84
85impl PricePoint {
86 #[must_use]
88 pub const fn new(price: MarketPrice) -> Self {
89 Self { label: None, price }
90 }
91
92 pub fn with_label(mut self, label: impl AsRef<str>) -> Result<Self, PriceSeriesError> {
98 let trimmed = label.as_ref().trim();
99 if trimmed.is_empty() {
100 return Err(PriceSeriesError::EmptyLabel);
101 }
102
103 self.label = Some(trimmed.to_string());
104 Ok(self)
105 }
106
107 #[must_use]
109 pub fn label(&self) -> Option<&str> {
110 self.label.as_deref()
111 }
112
113 #[must_use]
115 pub const fn price(&self) -> MarketPrice {
116 self.price
117 }
118}
119
120#[derive(Clone, Debug, Default, PartialEq)]
122pub struct PriceSeries {
123 name: Option<SeriesName>,
124 points: Vec<PricePoint>,
125}
126
127impl PriceSeries {
128 #[must_use]
130 pub const fn new() -> Self {
131 Self {
132 name: None,
133 points: Vec::new(),
134 }
135 }
136
137 #[must_use]
139 pub const fn named(name: SeriesName) -> Self {
140 Self {
141 name: Some(name),
142 points: Vec::new(),
143 }
144 }
145
146 #[must_use]
148 pub fn from_points(points: impl IntoIterator<Item = PricePoint>) -> Self {
149 Self {
150 name: None,
151 points: points.into_iter().collect(),
152 }
153 }
154
155 #[must_use]
157 pub const fn name(&self) -> Option<&SeriesName> {
158 self.name.as_ref()
159 }
160
161 pub fn push(&mut self, point: PricePoint) {
163 self.points.push(point);
164 }
165
166 #[must_use]
168 pub const fn len(&self) -> usize {
169 self.points.len()
170 }
171
172 #[must_use]
174 pub const fn is_empty(&self) -> bool {
175 self.points.is_empty()
176 }
177
178 #[must_use]
180 pub fn first(&self) -> Option<&PricePoint> {
181 self.points.first()
182 }
183
184 #[must_use]
186 pub fn last(&self) -> Option<&PricePoint> {
187 self.points.last()
188 }
189
190 pub fn iter(&self) -> slice::Iter<'_, PricePoint> {
192 self.points.iter()
193 }
194
195 pub fn adjacent_simple_returns(&self) -> Result<Vec<SimpleReturn>, PriceSeriesError> {
201 self.points
202 .windows(2)
203 .map(|pair| {
204 SimpleReturn::from_prices(pair[0].price().value(), pair[1].price().value())
205 .map_err(PriceSeriesError::Return)
206 })
207 .collect()
208 }
209}
210
211impl IntoIterator for PriceSeries {
212 type Item = PricePoint;
213 type IntoIter = std::vec::IntoIter<PricePoint>;
214
215 fn into_iter(self) -> Self::IntoIter {
216 self.points.into_iter()
217 }
218}
219
220impl<'a> IntoIterator for &'a PriceSeries {
221 type Item = &'a PricePoint;
222 type IntoIter = slice::Iter<'a, PricePoint>;
223
224 fn into_iter(self) -> Self::IntoIter {
225 self.iter()
226 }
227}
228
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub enum PriceSeriesError {
232 EmptyLabel,
234 Return(ReturnError),
236}
237
238impl fmt::Display for PriceSeriesError {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 Self::EmptyLabel => formatter.write_str("price point label cannot be empty"),
242 Self::Return(error) => write!(formatter, "{error}"),
243 }
244 }
245}
246
247impl Error for PriceSeriesError {
248 fn source(&self) -> Option<&(dyn Error + 'static)> {
249 match self {
250 Self::EmptyLabel => None,
251 Self::Return(error) => Some(error),
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::{PricePoint, PriceSeries, SeriesName};
259 use use_market_price::MarketPrice;
260
261 fn price(value: f64) -> PricePoint {
262 PricePoint::new(MarketPrice::new(value).expect("price should be valid"))
263 }
264
265 #[test]
266 fn creates_empty_series() {
267 let series = PriceSeries::new();
268
269 assert!(series.is_empty());
270 assert_eq!(series.len(), 0);
271 assert!(series.first().is_none());
272 }
273
274 #[test]
275 fn appends_price_point() {
276 let mut series = PriceSeries::named(SeriesName::new("ABC").expect("name should be valid"));
277 series.push(price(100.0));
278
279 assert_eq!(series.name().expect("name should exist").as_str(), "ABC");
280 assert_eq!(series.len(), 1);
281 }
282
283 #[test]
284 fn exposes_first_and_last() {
285 let mut series = PriceSeries::new();
286 series.push(price(100.0));
287 series.push(price(105.0));
288
289 assert!(
290 (series.first().expect("first should exist").price().value() - 100.0).abs()
291 < f64::EPSILON
292 );
293 assert!(
294 (series.last().expect("last should exist").price().value() - 105.0).abs()
295 < f64::EPSILON
296 );
297 }
298
299 #[test]
300 fn preserves_iteration_order() {
301 let series = PriceSeries::from_points([price(100.0), price(101.0), price(99.0)]);
302 let values: Vec<f64> = series.iter().map(|point| point.price().value()).collect();
303
304 assert_eq!(values, vec![100.0, 101.0, 99.0]);
305 }
306
307 #[test]
308 fn computes_adjacent_returns() {
309 let series = PriceSeries::from_points([price(100.0), price(105.0), price(103.0)]);
310 let returns = series
311 .adjacent_simple_returns()
312 .expect("returns should compute");
313
314 assert_eq!(returns.len(), 2);
315 assert!((returns[0].value() - 0.05).abs() < 1.0e-12);
316 }
317}