Skip to main content

finance_solution/util/
primitives.rs

1//! Domain **newtypes** — zero-cost wrappers that make invalid values harder to pass by accident.
2//!
3//! # Design (see `rust_design_patterns` Newtype)
4//!
5//! - Private fields; construction is fallible ([`TryFrom`] / associated `try_` constructors).
6//! - **Not** type aliases: `Rate` is not interchangeable with bare `f64` at the type level.
7//! - **Additive for 0.1.x / 0.2:** existing free functions still take `f64` / `u32`. Newtypes
8//!   are for call sites and TA config that want compile-time clarity + one-time validation.
9//! - Extract with [`.get()`](Rate::get) / [`Into<f64>`] when calling f64-based APIs.
10//!
11//! # When to use
12//!
13//! | Type | Prefer when |
14//! |------|-------------|
15//! | [`Rate`] | You validated a rate once and pass it through several calls |
16//! | [`Periods`] | Period counts should not mix with money amounts |
17//! | [`PositivePrice`] | Equity prices / volumes that must be `> 0` |
18//! | [`Money`] | Finite signed amounts (loans, payments) without unit claims |
19//! | [`PeriodLength`] | TA lookbacks (`SMA(20)`, stoch `k_period`) — `usize ≥ 1` |
20//!
21//! # Examples
22//! ```
23//! use finance_solution::{future_value, PositivePrice, Rate, Periods, FinanceResult};
24//!
25//! fn grow(rate: Rate, n: Periods, pv: f64) -> FinanceResult<f64> {
26//!     future_value(rate.get(), n.get(), pv, false)
27//! }
28//!
29//! let r = Rate::tvm(0.05)?;
30//! let n = Periods::new(10)?;
31//! assert!(grow(r, n, -1_000.0).is_ok());
32//! assert!(Rate::tvm(-1.5).is_err());
33//! assert!(PositivePrice::new(0.0).is_err());
34//! # Ok::<(), finance_solution::FinanceError>(())
35//! ```
36
37use crate::util::error::{
38    require_finite, require_rate, require_rate_gt_minus_one, FinanceError, FinanceResult,
39};
40use std::fmt;
41
42// ---------------------------------------------------------------------------
43// Rate
44// ---------------------------------------------------------------------------
45
46/// Periodic or continuous **interest / return rate** as a decimal (e.g. `0.05` = 5%).
47///
48/// Domain depends on constructor: [`Rate::tvm`] (`≥ -1`), [`Rate::payment`] (`> -1`),
49/// [`Rate::positive`] (`> 0`), [`Rate::finite`] (any finite).
50#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
51pub struct Rate(f64);
52
53impl Rate {
54    /// Any finite rate (no lower bound). Prefer domain-specific constructors for TVM.
55    pub fn finite(value: f64) -> FinanceResult<Self> {
56        require_finite("rate", value)?;
57        Ok(Rate(value))
58    }
59
60    /// TVM-compatible rate: finite and `≥ -1.0`.
61    pub fn tvm(value: f64) -> FinanceResult<Self> {
62        require_rate(value)?;
63        Ok(Rate(value))
64    }
65
66    /// Payment / annuity rate: finite and `> -1.0`.
67    pub fn payment(value: f64) -> FinanceResult<Self> {
68        require_rate_gt_minus_one(value)?;
69        Ok(Rate(value))
70    }
71
72    /// Strictly positive finite rate (doubling rules, growth rates).
73    pub fn positive(value: f64) -> FinanceResult<Self> {
74        require_finite("rate", value)?;
75        if value == 0.0 {
76            return Err(FinanceError::ZeroValue { field: "rate" });
77        }
78        if value < 0.0 {
79            return Err(FinanceError::InvalidRate { rate: value });
80        }
81        Ok(Rate(value))
82    }
83
84    /// Inner `f64` (by value; type is `Copy`).
85    #[inline]
86    pub fn get(self) -> f64 {
87        self.0
88    }
89}
90
91impl From<Rate> for f64 {
92    #[inline]
93    fn from(r: Rate) -> f64 {
94        r.0
95    }
96}
97
98impl TryFrom<f64> for Rate {
99    type Error = FinanceError;
100    /// Defaults to [`Rate::tvm`] (most common finance domain).
101    fn try_from(value: f64) -> Result<Self, Self::Error> {
102        Rate::tvm(value)
103    }
104}
105
106impl fmt::Display for Rate {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Periods (TVM u32)
114// ---------------------------------------------------------------------------
115
116/// Count of compounding / payment **periods** (`u32`).
117#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct Periods(u32);
119
120impl Periods {
121    /// Any period count (including zero where formulas allow).
122    pub fn new(value: u32) -> FinanceResult<Self> {
123        Ok(Periods(value))
124    }
125
126    /// At least one period (annuities, many TA windows expressed as `u32`).
127    pub fn at_least_one(value: u32) -> FinanceResult<Self> {
128        if value == 0 {
129            return Err(FinanceError::InvalidPeriod {
130                period: 0,
131                periods: 0,
132                message: "periods must be at least 1",
133            });
134        }
135        Ok(Periods(value))
136    }
137
138    #[inline]
139    pub fn get(self) -> u32 {
140        self.0
141    }
142}
143
144impl From<Periods> for u32 {
145    #[inline]
146    fn from(p: Periods) -> u32 {
147        p.0
148    }
149}
150
151impl TryFrom<u32> for Periods {
152    type Error = FinanceError;
153    fn try_from(value: u32) -> Result<Self, Self::Error> {
154        Periods::new(value)
155    }
156}
157
158impl fmt::Display for Periods {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "{}", self.0)
161    }
162}
163
164// ---------------------------------------------------------------------------
165// PeriodLength (TA usize lookback)
166// ---------------------------------------------------------------------------
167
168/// Positive lookback / window length for technical indicators (`usize ≥ 1`).
169///
170/// Distinct from [`Periods`] (`u32` TVM counts) so TA windows do not silently mix with NPER.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
172pub struct PeriodLength(usize);
173
174impl PeriodLength {
175    /// Fallible constructor: `n >= 1`.
176    pub fn new(n: usize) -> FinanceResult<Self> {
177        if n == 0 {
178            return Err(FinanceError::InvalidPeriod {
179                period: 0,
180                periods: 0,
181                message: "period length must be at least 1",
182            });
183        }
184        Ok(PeriodLength(n))
185    }
186
187    /// `const` constructor for **known-valid** compile-time windows (e.g. `20` for SMA-20).
188    ///
189    /// Panics in debug if `n == 0`; release builds still store `0` — prefer [`PeriodLength::new`]
190    /// for runtime input. For `const` presets, only pass literals `≥ 1`.
191    pub const fn new_const(n: usize) -> Self {
192        assert!(n >= 1, "PeriodLength::new_const requires n >= 1");
193        PeriodLength(n)
194    }
195
196    #[inline]
197    pub const fn get(self) -> usize {
198        self.0
199    }
200}
201
202impl From<PeriodLength> for usize {
203    #[inline]
204    fn from(p: PeriodLength) -> usize {
205        p.0
206    }
207}
208
209impl TryFrom<usize> for PeriodLength {
210    type Error = FinanceError;
211    fn try_from(value: usize) -> Result<Self, Self::Error> {
212        PeriodLength::new(value)
213    }
214}
215
216impl fmt::Display for PeriodLength {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.0)
219    }
220}
221
222// ---------------------------------------------------------------------------
223// PositivePrice
224// ---------------------------------------------------------------------------
225
226/// Strictly **positive finite** price (or similar quantity used as a price level).
227#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
228pub struct PositivePrice(f64);
229
230impl PositivePrice {
231    pub fn new(value: f64) -> FinanceResult<Self> {
232        require_finite("price", value)?;
233        if value <= 0.0 {
234            return Err(FinanceError::InvalidCashflow {
235                message: "price must be strictly positive",
236            });
237        }
238        Ok(PositivePrice(value))
239    }
240
241    #[inline]
242    pub fn get(self) -> f64 {
243        self.0
244    }
245}
246
247impl From<PositivePrice> for f64 {
248    #[inline]
249    fn from(p: PositivePrice) -> f64 {
250        p.0
251    }
252}
253
254impl TryFrom<f64> for PositivePrice {
255    type Error = FinanceError;
256    fn try_from(value: f64) -> Result<Self, Self::Error> {
257        PositivePrice::new(value)
258    }
259}
260
261impl fmt::Display for PositivePrice {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        write!(f, "{}", self.0)
264    }
265}
266
267// ---------------------------------------------------------------------------
268// Money
269// ---------------------------------------------------------------------------
270
271/// Finite **signed** monetary amount (no currency unit — pure magnitude + sign).
272#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
273pub struct Money(f64);
274
275impl Money {
276    pub fn new(value: f64) -> FinanceResult<Self> {
277        require_finite("money", value)?;
278        Ok(Money(value))
279    }
280
281    /// Finite and nonzero.
282    pub fn nonzero(value: f64) -> FinanceResult<Self> {
283        require_finite("money", value)?;
284        if value == 0.0 {
285            return Err(FinanceError::ZeroValue { field: "money" });
286        }
287        Ok(Money(value))
288    }
289
290    #[inline]
291    pub fn get(self) -> f64 {
292        self.0
293    }
294}
295
296impl From<Money> for f64 {
297    #[inline]
298    fn from(m: Money) -> f64 {
299        m.0
300    }
301}
302
303impl TryFrom<f64> for Money {
304    type Error = FinanceError;
305    fn try_from(value: f64) -> Result<Self, Self::Error> {
306        Money::new(value)
307    }
308}
309
310impl fmt::Display for Money {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        write!(f, "{}", self.0)
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn rate_domains() {
322        assert!(Rate::tvm(-1.0).is_ok());
323        assert!(Rate::tvm(-1.1).is_err());
324        assert!(Rate::payment(-1.0).is_err());
325        assert!(Rate::positive(0.08).is_ok());
326        assert!(Rate::positive(0.0).is_err());
327    }
328
329    #[test]
330    fn periods_and_length() {
331        assert_eq!(Periods::new(0).unwrap().get(), 0);
332        assert!(Periods::at_least_one(0).is_err());
333        assert_eq!(PeriodLength::new(20).unwrap().get(), 20);
334        assert!(PeriodLength::new(0).is_err());
335        assert_eq!(PeriodLength::new_const(14).get(), 14);
336    }
337
338    #[test]
339    fn price_and_money() {
340        assert!(PositivePrice::new(100.0).is_ok());
341        assert!(PositivePrice::new(0.0).is_err());
342        assert!(Money::new(-50.0).is_ok());
343        assert!(Money::nonzero(0.0).is_err());
344    }
345}