Skip to main content

finance_solution/stocks/
returns.rs

1//! Simple and logarithmic returns from prices.
2//!
3//! # Error handling (v0.1+)
4//!
5//! All public functions return [`FinanceResult`]. Empty series, zero start prices, and
6//! non-positive prices for log/CAGR paths are structured errors.
7use crate::util::error::{require_finite, FinanceError, FinanceResult};
8
9/// Simple return between two prices: `(p1 - p0) / p0`.
10///
11/// # Examples
12/// ```
13/// use finance_solution::{simple_return, FinanceError};
14///
15/// assert!(simple_return(100.0, 110.0).is_ok());
16/// match simple_return(0.0, 110.0) {
17///     Err(FinanceError::ZeroValue { field }) => assert_eq!(field, "price_start"),
18///     other => panic!("expected ZeroValue, got {other:?}"),
19/// }
20/// ```
21pub fn simple_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
22    require_finite("price_start", price_start)?;
23    require_finite("price_end", price_end)?;
24    if price_start == 0.0 {
25        return Err(FinanceError::ZeroValue {
26            field: "price_start",
27        });
28    }
29    Ok((price_end - price_start) / price_start)
30}
31
32/// Logarithmic return: `ln(p1 / p0)`. Requires strictly positive prices.
33///
34/// # Examples
35/// ```
36/// use finance_solution::{log_return, FinanceError};
37///
38/// assert!(log_return(100.0, 110.0).is_ok());
39/// match log_return(-1.0, 110.0) {
40///     Err(FinanceError::InvalidCashflow { message }) => {
41///         assert!(message.contains("positive"));
42///     }
43///     other => panic!("expected InvalidCashflow, got {other:?}"),
44/// }
45/// ```
46pub fn log_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
47    require_finite("price_start", price_start)?;
48    require_finite("price_end", price_end)?;
49    if price_start <= 0.0 || price_end <= 0.0 {
50        return Err(FinanceError::InvalidCashflow {
51            message: "log_return requires strictly positive prices",
52        });
53    }
54    Ok((price_end / price_start).ln())
55}
56
57/// Simple returns for consecutive prices: length `prices.len() - 1`.
58///
59/// # Examples
60/// ```
61/// use finance_solution::{simple_returns, FinanceError};
62///
63/// assert_eq!(simple_returns(&[100.0, 110.0]).unwrap().len(), 1);
64/// match simple_returns(&[100.0]) {
65///     Err(FinanceError::Unsolvable { message }) => assert!(message.contains("two")),
66///     other => panic!("expected Unsolvable, got {other:?}"),
67/// }
68/// ```
69pub fn simple_returns(prices: &[f64]) -> FinanceResult<Vec<f64>> {
70    if prices.len() < 2 {
71        return Err(FinanceError::Unsolvable {
72            message: "simple_returns requires at least two prices",
73        });
74    }
75    let mut out = Vec::with_capacity(prices.len() - 1);
76    for window in prices.windows(2) {
77        out.push(simple_return(window[0], window[1])?);
78    }
79    Ok(out)
80}
81
82/// Log returns for consecutive prices: length `prices.len() - 1`.
83///
84/// # Examples
85/// ```
86/// use finance_solution::{log_returns, FinanceError};
87///
88/// assert!(log_returns(&[100.0, 110.0]).is_ok());
89/// assert!(matches!(
90///     log_returns(&[100.0, 0.0]),
91///     Err(FinanceError::InvalidCashflow { .. })
92/// ));
93/// ```
94pub fn log_returns(prices: &[f64]) -> FinanceResult<Vec<f64>> {
95    if prices.len() < 2 {
96        return Err(FinanceError::Unsolvable {
97            message: "log_returns requires at least two prices",
98        });
99    }
100    let mut out = Vec::with_capacity(prices.len() - 1);
101    for window in prices.windows(2) {
102        out.push(log_return(window[0], window[1])?);
103    }
104    Ok(out)
105}
106
107/// Compound annual growth rate: `(end / start)^(1/years) - 1`.
108///
109/// # Examples
110/// ```
111/// use finance_solution::{cagr, FinanceError};
112///
113/// assert!(cagr(100.0, 121.0, 2.0).is_ok());
114/// match cagr(100.0, 121.0, 0.0) {
115///     Err(FinanceError::Unsolvable { message }) => assert!(message.contains("years")),
116///     other => panic!("expected Unsolvable, got {other:?}"),
117/// }
118/// ```
119pub fn cagr(price_start: f64, price_end: f64, years: f64) -> FinanceResult<f64> {
120    require_finite("price_start", price_start)?;
121    require_finite("price_end", price_end)?;
122    require_finite("years", years)?;
123    if price_start <= 0.0 || price_end <= 0.0 {
124        return Err(FinanceError::InvalidCashflow {
125            message: "cagr requires strictly positive prices",
126        });
127    }
128    if years <= 0.0 {
129        return Err(FinanceError::Unsolvable {
130            message: "cagr requires years > 0",
131        });
132    }
133    Ok((price_end / price_start).powf(1.0 / years) - 1.0)
134}
135
136/// Arithmetic mean of a return series.
137///
138/// # Examples
139/// ```
140/// use finance_solution::{mean_return, FinanceError};
141///
142/// assert!(mean_return(&[0.01, 0.02]).is_ok());
143/// match mean_return(&[]) {
144///     Err(FinanceError::Unsolvable { message }) => assert!(message.contains("non-empty")),
145///     other => panic!("expected Unsolvable, got {other:?}"),
146/// }
147/// ```
148pub fn mean_return(returns: &[f64]) -> FinanceResult<f64> {
149    if returns.is_empty() {
150        return Err(FinanceError::Unsolvable {
151            message: "mean_return requires a non-empty series",
152        });
153    }
154    for r in returns {
155        require_finite("returns", *r)?;
156    }
157    Ok(returns.iter().sum::<f64>() / returns.len() as f64)
158}
159
160/// Total simple return from first to last price: `(end - start) / start`.
161///
162/// # Examples
163/// ```
164/// use finance_solution::{total_return, FinanceError};
165///
166/// let r = total_return(100.0, 125.0).unwrap();
167/// assert!((r - 0.25).abs() < 1e-12);
168/// assert!(matches!(
169///     total_return(0.0, 125.0),
170///     Err(FinanceError::ZeroValue { .. })
171/// ));
172/// ```
173pub fn total_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
174    simple_return(price_start, price_end)
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::*;
181
182    #[test]
183    fn test_simple_and_log_return() {
184        assert_approx_equal!(simple_return(100.0, 110.0).unwrap(), 0.1);
185        assert!(log_return(100.0, 110.0).unwrap() < 0.1);
186        assert!(log_return(100.0, 110.0).unwrap() > 0.09);
187    }
188
189    #[test]
190    fn test_returns_series() {
191        let prices = [100.0, 110.0, 105.0];
192        let r = simple_returns(&prices).unwrap();
193        assert_eq!(r.len(), 2);
194        assert_approx_equal!(r[0], 0.1);
195    }
196
197    #[test]
198    fn test_cagr() {
199        assert_approx_equal!(cagr(100.0, 121.0, 2.0).unwrap(), 0.1);
200    }
201
202    #[test]
203    fn test_round_trip_product() {
204        let prices = [100.0, 110.0, 99.0, 108.9];
205        let rets = simple_returns(&prices).unwrap();
206        let mut wealth = 1.0;
207        for r in rets {
208            wealth *= 1.0 + r;
209        }
210        assert_approx_equal!(wealth, prices[prices.len() - 1] / prices[0]);
211    }
212}