use crate::util::error::{require_finite, FinanceError, FinanceResult};
pub fn simple_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
require_finite("price_start", price_start)?;
require_finite("price_end", price_end)?;
if price_start == 0.0 {
return Err(FinanceError::ZeroValue {
field: "price_start",
});
}
Ok((price_end - price_start) / price_start)
}
pub fn log_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
require_finite("price_start", price_start)?;
require_finite("price_end", price_end)?;
if price_start <= 0.0 || price_end <= 0.0 {
return Err(FinanceError::InvalidCashflow {
message: "log_return requires strictly positive prices",
});
}
Ok((price_end / price_start).ln())
}
pub fn simple_returns(prices: &[f64]) -> FinanceResult<Vec<f64>> {
if prices.len() < 2 {
return Err(FinanceError::Unsolvable {
message: "simple_returns requires at least two prices",
});
}
let mut out = Vec::with_capacity(prices.len() - 1);
for window in prices.windows(2) {
out.push(simple_return(window[0], window[1])?);
}
Ok(out)
}
pub fn log_returns(prices: &[f64]) -> FinanceResult<Vec<f64>> {
if prices.len() < 2 {
return Err(FinanceError::Unsolvable {
message: "log_returns requires at least two prices",
});
}
let mut out = Vec::with_capacity(prices.len() - 1);
for window in prices.windows(2) {
out.push(log_return(window[0], window[1])?);
}
Ok(out)
}
pub fn cagr(price_start: f64, price_end: f64, years: f64) -> FinanceResult<f64> {
require_finite("price_start", price_start)?;
require_finite("price_end", price_end)?;
require_finite("years", years)?;
if price_start <= 0.0 || price_end <= 0.0 {
return Err(FinanceError::InvalidCashflow {
message: "cagr requires strictly positive prices",
});
}
if years <= 0.0 {
return Err(FinanceError::Unsolvable {
message: "cagr requires years > 0",
});
}
Ok((price_end / price_start).powf(1.0 / years) - 1.0)
}
pub fn mean_return(returns: &[f64]) -> FinanceResult<f64> {
if returns.is_empty() {
return Err(FinanceError::Unsolvable {
message: "mean_return requires a non-empty series",
});
}
for r in returns {
require_finite("returns", *r)?;
}
Ok(returns.iter().sum::<f64>() / returns.len() as f64)
}
pub fn total_return(price_start: f64, price_end: f64) -> FinanceResult<f64> {
simple_return(price_start, price_end)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
#[test]
fn test_simple_and_log_return() {
assert_approx_equal!(simple_return(100.0, 110.0).unwrap(), 0.1);
assert!(log_return(100.0, 110.0).unwrap() < 0.1);
assert!(log_return(100.0, 110.0).unwrap() > 0.09);
}
#[test]
fn test_returns_series() {
let prices = [100.0, 110.0, 105.0];
let r = simple_returns(&prices).unwrap();
assert_eq!(r.len(), 2);
assert_approx_equal!(r[0], 0.1);
}
#[test]
fn test_cagr() {
assert_approx_equal!(cagr(100.0, 121.0, 2.0).unwrap(), 0.1);
}
#[test]
fn test_round_trip_product() {
let prices = [100.0, 110.0, 99.0, 108.9];
let rets = simple_returns(&prices).unwrap();
let mut wealth = 1.0;
for r in rets {
wealth *= 1.0 + r;
}
assert_approx_equal!(wealth, prices[prices.len() - 1] / prices[0]);
}
}