use nalgebra::DMatrix;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ReturnsError {
#[error("need at least 2 price observations, got {0}")]
InsufficientPrices(usize),
#[error("empty price matrix")]
EmptyPrices,
#[error("zero or negative price at observation {row}, asset {col}")]
InvalidPrice {
row: usize,
col: usize,
},
}
pub fn log_returns(prices: &DMatrix<f64>) -> Result<DMatrix<f64>, ReturnsError> {
let (num_obs, num_assets) = prices.shape();
if num_obs == 0 || num_assets == 0 {
return Err(ReturnsError::EmptyPrices);
}
if num_obs < 2 {
return Err(ReturnsError::InsufficientPrices(num_obs));
}
for row in 0..num_obs {
for col in 0..num_assets {
if prices[(row, col)] <= 0.0 {
return Err(ReturnsError::InvalidPrice { row, col });
}
}
}
let mut returns = DMatrix::zeros(num_obs - 1, num_assets);
for row in 0..num_obs - 1 {
for col in 0..num_assets {
returns[(row, col)] = (prices[(row + 1, col)] / prices[(row, col)]).ln();
}
}
Ok(returns)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_basic_log_returns() {
#[rustfmt::skip]
let prices = DMatrix::from_row_slice(3, 2, &[
100.0, 50.0,
110.0, 55.0,
105.0, 52.0,
]);
let returns = log_returns(&prices).unwrap();
assert_eq!(returns.shape(), (2, 2));
assert_relative_eq!(returns[(0, 0)], (110.0_f64 / 100.0).ln(), epsilon = 1e-14);
assert_relative_eq!(returns[(0, 1)], (55.0_f64 / 50.0).ln(), epsilon = 1e-14);
assert_relative_eq!(returns[(1, 0)], (105.0_f64 / 110.0).ln(), epsilon = 1e-14);
}
#[test]
fn test_empty_prices() {
let prices = DMatrix::<f64>::zeros(0, 0);
assert!(log_returns(&prices).is_err());
}
#[test]
fn test_insufficient_prices() {
let prices = DMatrix::from_row_slice(1, 2, &[100.0, 50.0]);
assert!(log_returns(&prices).is_err());
}
#[test]
fn test_zero_price() {
#[rustfmt::skip]
let prices = DMatrix::from_row_slice(2, 2, &[
100.0, 50.0,
0.0, 55.0,
]);
assert!(log_returns(&prices).is_err());
}
}