Skip to main content

use_market_price/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common market price primitives.
8pub mod prelude {
9    pub use crate::{MarketPrice, MarketPriceError, PriceKind, PriceKindParseError, PriceQuote};
10}
11
12/// A finite non-negative market price value.
13///
14/// Zero is accepted for descriptive use cases such as missing, halted, or placeholder prices.
15/// Positive prices can be checked with [`MarketPrice::is_positive`].
16#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
17pub struct MarketPrice {
18    value: f64,
19}
20
21impl MarketPrice {
22    /// Creates a market price from a finite non-negative `f64`.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`MarketPriceError::NonFinite`] for `NaN` or infinite values and
27    /// [`MarketPriceError::Negative`] for negative values.
28    pub fn new(value: f64) -> Result<Self, MarketPriceError> {
29        if !value.is_finite() {
30            return Err(MarketPriceError::NonFinite);
31        }
32
33        if value < 0.0 {
34            return Err(MarketPriceError::Negative);
35        }
36
37        Ok(Self { value })
38    }
39
40    /// Returns the stored price value.
41    #[must_use]
42    pub const fn value(self) -> f64 {
43        self.value
44    }
45
46    /// Returns whether the price is strictly positive.
47    #[must_use]
48    pub fn is_positive(self) -> bool {
49        self.value > 0.0
50    }
51}
52
53impl TryFrom<f64> for MarketPrice {
54    type Error = MarketPriceError;
55
56    fn try_from(value: f64) -> Result<Self, Self::Error> {
57        Self::new(value)
58    }
59}
60
61impl fmt::Display for MarketPrice {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        self.value.fmt(formatter)
64    }
65}
66
67/// Errors returned while constructing market price values.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum MarketPriceError {
70    /// Price values must be finite.
71    NonFinite,
72    /// Price values must not be negative.
73    Negative,
74}
75
76impl fmt::Display for MarketPriceError {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::NonFinite => formatter.write_str("market price must be finite"),
80            Self::Negative => formatter.write_str("market price cannot be negative"),
81        }
82    }
83}
84
85impl Error for MarketPriceError {}
86
87/// Descriptive market price kind vocabulary.
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub enum PriceKind {
90    /// Opening price.
91    Open,
92    /// High price.
93    High,
94    /// Low price.
95    Low,
96    /// Closing price.
97    Close,
98    /// Last traded price.
99    Last,
100    /// Bid price.
101    Bid,
102    /// Ask price.
103    Ask,
104    /// Mid price.
105    Mid,
106    /// Settlement price.
107    Settlement,
108    /// Adjusted close price.
109    AdjustedClose,
110    /// Unknown price kind.
111    Unknown,
112    /// Caller-defined price kind.
113    Custom(String),
114}
115
116impl fmt::Display for PriceKind {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        formatter.write_str(match self {
119            Self::Open => "open",
120            Self::High => "high",
121            Self::Low => "low",
122            Self::Close => "close",
123            Self::Last => "last",
124            Self::Bid => "bid",
125            Self::Ask => "ask",
126            Self::Mid => "mid",
127            Self::Settlement => "settlement",
128            Self::AdjustedClose => "adjusted-close",
129            Self::Unknown => "unknown",
130            Self::Custom(value) => value.as_str(),
131        })
132    }
133}
134
135impl FromStr for PriceKind {
136    type Err = PriceKindParseError;
137
138    fn from_str(value: &str) -> Result<Self, Self::Err> {
139        let trimmed = value.trim();
140        if trimmed.is_empty() {
141            return Err(PriceKindParseError::Empty);
142        }
143
144        match normalized_token(trimmed).as_str() {
145            "open" => Ok(Self::Open),
146            "high" => Ok(Self::High),
147            "low" => Ok(Self::Low),
148            "close" => Ok(Self::Close),
149            "last" => Ok(Self::Last),
150            "bid" => Ok(Self::Bid),
151            "ask" => Ok(Self::Ask),
152            "mid" => Ok(Self::Mid),
153            "settlement" => Ok(Self::Settlement),
154            "adjusted-close" => Ok(Self::AdjustedClose),
155            "unknown" => Ok(Self::Unknown),
156            _ => Ok(Self::Custom(trimmed.to_string())),
157        }
158    }
159}
160
161/// Errors returned while parsing price kinds.
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163pub enum PriceKindParseError {
164    /// The input was empty after trimming whitespace.
165    Empty,
166}
167
168impl fmt::Display for PriceKindParseError {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Self::Empty => formatter.write_str("price kind cannot be empty"),
172        }
173    }
174}
175
176impl Error for PriceKindParseError {}
177
178/// A market price paired with descriptive price-kind vocabulary.
179#[derive(Clone, Debug, PartialEq)]
180pub struct PriceQuote {
181    kind: PriceKind,
182    price: MarketPrice,
183}
184
185impl PriceQuote {
186    /// Creates a price quote from a kind and already validated price value.
187    #[must_use]
188    pub const fn new(kind: PriceKind, price: MarketPrice) -> Self {
189        Self { kind, price }
190    }
191
192    /// Returns the quote kind.
193    #[must_use]
194    pub const fn kind(&self) -> &PriceKind {
195        &self.kind
196    }
197
198    /// Returns the quote price.
199    #[must_use]
200    pub const fn price(&self) -> MarketPrice {
201        self.price
202    }
203}
204
205fn normalized_token(value: &str) -> String {
206    value
207        .trim()
208        .chars()
209        .map(|character| match character {
210            '_' | ' ' => '-',
211            other => other.to_ascii_lowercase(),
212        })
213        .collect()
214}
215
216#[cfg(test)]
217mod tests {
218    use super::{MarketPrice, MarketPriceError, PriceKind, PriceQuote};
219
220    #[test]
221    fn accepts_valid_positive_price() {
222        let price = MarketPrice::new(101.25).expect("price should be valid");
223
224        assert!((price.value() - 101.25).abs() < f64::EPSILON);
225        assert!(price.is_positive());
226        assert_eq!(price.to_string(), "101.25");
227    }
228
229    #[test]
230    fn rejects_negative_price() {
231        assert_eq!(MarketPrice::new(-0.01), Err(MarketPriceError::Negative));
232    }
233
234    #[test]
235    fn displays_and_parses_price_kind() {
236        let kind: PriceKind = "Adjusted Close".parse().expect("kind should parse");
237
238        assert_eq!(kind, PriceKind::AdjustedClose);
239        assert_eq!(kind.to_string(), "adjusted-close");
240    }
241
242    #[test]
243    fn supports_custom_price_kind() {
244        let kind: PriceKind = "auction-price".parse().expect("kind should parse");
245
246        assert_eq!(kind, PriceKind::Custom("auction-price".to_string()));
247        assert_eq!(kind.to_string(), "auction-price");
248    }
249
250    #[test]
251    fn constructs_quote() {
252        let price = MarketPrice::new(100.0).expect("price should be valid");
253        let quote = PriceQuote::new(PriceKind::Close, price);
254
255        assert_eq!(quote.kind(), &PriceKind::Close);
256        assert_eq!(quote.price(), price);
257    }
258}