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}