Skip to main content

use_price_series/
lib.rs

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
10/// Common price-series primitives.
11pub mod prelude {
12    pub use crate::{PricePoint, PriceSeries, PriceSeriesError, SeriesName, SeriesNameError};
13}
14
15/// A non-empty price series name.
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub struct SeriesName(String);
18
19impl SeriesName {
20    /// Creates a series name from non-empty text.
21    ///
22    /// # Errors
23    ///
24    /// Returns [`SeriesNameError::Empty`] when the trimmed name is empty.
25    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    /// Returns the series name.
35    #[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/// Errors returned while constructing series names.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum SeriesNameError {
64    /// The name was empty after trimming whitespace.
65    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/// A price point with an optional label.
79#[derive(Clone, Debug, PartialEq)]
80pub struct PricePoint {
81    label: Option<String>,
82    price: MarketPrice,
83}
84
85impl PricePoint {
86    /// Creates an unlabeled price point.
87    #[must_use]
88    pub const fn new(price: MarketPrice) -> Self {
89        Self { label: None, price }
90    }
91
92    /// Attaches a non-empty point label.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`PriceSeriesError::EmptyLabel`] when the trimmed label is empty.
97    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    /// Returns the optional point label.
108    #[must_use]
109    pub fn label(&self) -> Option<&str> {
110        self.label.as_deref()
111    }
112
113    /// Returns the price value.
114    #[must_use]
115    pub const fn price(&self) -> MarketPrice {
116        self.price
117    }
118}
119
120/// A lightweight insertion-ordered price series.
121#[derive(Clone, Debug, Default, PartialEq)]
122pub struct PriceSeries {
123    name: Option<SeriesName>,
124    points: Vec<PricePoint>,
125}
126
127impl PriceSeries {
128    /// Creates an empty unnamed price series.
129    #[must_use]
130    pub const fn new() -> Self {
131        Self {
132            name: None,
133            points: Vec::new(),
134        }
135    }
136
137    /// Creates an empty named price series.
138    #[must_use]
139    pub const fn named(name: SeriesName) -> Self {
140        Self {
141            name: Some(name),
142            points: Vec::new(),
143        }
144    }
145
146    /// Creates a series from insertion-ordered points.
147    #[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    /// Returns the optional series name.
156    #[must_use]
157    pub const fn name(&self) -> Option<&SeriesName> {
158        self.name.as_ref()
159    }
160
161    /// Appends a point, preserving insertion order.
162    pub fn push(&mut self, point: PricePoint) {
163        self.points.push(point);
164    }
165
166    /// Returns the number of points.
167    #[must_use]
168    pub const fn len(&self) -> usize {
169        self.points.len()
170    }
171
172    /// Returns whether the series contains no points.
173    #[must_use]
174    pub const fn is_empty(&self) -> bool {
175        self.points.is_empty()
176    }
177
178    /// Returns the first point.
179    #[must_use]
180    pub fn first(&self) -> Option<&PricePoint> {
181        self.points.first()
182    }
183
184    /// Returns the last point.
185    #[must_use]
186    pub fn last(&self) -> Option<&PricePoint> {
187        self.points.last()
188    }
189
190    /// Iterates over points in insertion order.
191    pub fn iter(&self) -> slice::Iter<'_, PricePoint> {
192        self.points.iter()
193    }
194
195    /// Computes adjacent simple returns in insertion order.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`PriceSeriesError::Return`] when a price pair cannot produce a simple return.
200    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/// Errors returned by price-series helpers.
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub enum PriceSeriesError {
232    /// Labels must be non-empty after trimming whitespace.
233    EmptyLabel,
234    /// A return calculation failed.
235    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}