use crate::Decimal;
use crate::types::decimal::decimal_sqrt;
use crate::types::error::{MMError, MMResult};
use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AssetId(pub String);
impl AssetId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AssetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for AssetId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for AssetId {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CorrelationMatrix {
assets: Vec<AssetId>,
correlations: Vec<Decimal>,
last_update: u64,
}
impl CorrelationMatrix {
#[must_use]
pub fn new(assets: Vec<AssetId>) -> Self {
let n = assets.len();
let size = n * (n + 1) / 2;
let mut correlations = vec![Decimal::ZERO; size];
for i in 0..n {
let idx = Self::index_for(i, i, n);
correlations[idx] = Decimal::ONE;
}
Self {
assets,
correlations,
last_update: 0,
}
}
#[must_use]
pub fn identity(assets: Vec<AssetId>) -> Self {
Self::new(assets)
}
#[must_use]
pub fn asset_count(&self) -> usize {
self.assets.len()
}
#[must_use]
pub fn assets(&self) -> &[AssetId] {
&self.assets
}
#[must_use]
pub fn last_update(&self) -> u64 {
self.last_update
}
fn index_for(i: usize, j: usize, n: usize) -> usize {
let (row, col) = if i <= j { (i, j) } else { (j, i) };
row * n - row * (row + 1) / 2 + col
}
fn asset_index(&self, asset: &AssetId) -> Option<usize> {
self.assets.iter().position(|a| a == asset)
}
#[must_use]
pub fn get_correlation(&self, asset1: &AssetId, asset2: &AssetId) -> Option<Decimal> {
let i = self.asset_index(asset1)?;
let j = self.asset_index(asset2)?;
let idx = Self::index_for(i, j, self.assets.len());
Some(self.correlations[idx])
}
pub fn set_correlation(
&mut self,
asset1: &AssetId,
asset2: &AssetId,
correlation: Decimal,
) -> MMResult<()> {
if correlation < Decimal::NEGATIVE_ONE || correlation > Decimal::ONE {
return Err(MMError::InvalidConfiguration(format!(
"Correlation must be in [-1, 1], got {}",
correlation
)));
}
let i = self.asset_index(asset1).ok_or_else(|| {
MMError::InvalidConfiguration(format!("Asset {} not in matrix", asset1))
})?;
let j = self.asset_index(asset2).ok_or_else(|| {
MMError::InvalidConfiguration(format!("Asset {} not in matrix", asset2))
})?;
if i == j && correlation != Decimal::ONE {
return Err(MMError::InvalidConfiguration(
"Self-correlation must be 1.0".to_string(),
));
}
let idx = Self::index_for(i, j, self.assets.len());
self.correlations[idx] = correlation;
Ok(())
}
pub fn update_from_returns(
&mut self,
returns: &HashMap<AssetId, Vec<Decimal>>,
timestamp: u64,
) -> MMResult<()> {
let mut return_len: Option<usize> = None;
for asset in &self.assets {
let asset_returns = returns.get(asset).ok_or_else(|| {
MMError::InvalidConfiguration(format!("No returns for asset {}", asset))
})?;
match return_len {
None => return_len = Some(asset_returns.len()),
Some(len) if len != asset_returns.len() => {
return Err(MMError::InvalidConfiguration(
"Return vectors must have same length".to_string(),
));
}
_ => {}
}
}
let n_returns = return_len.unwrap_or(0);
if n_returns < 2 {
return Err(MMError::InvalidConfiguration(
"Need at least 2 return observations".to_string(),
));
}
for i in 0..self.assets.len() {
for j in i..self.assets.len() {
if i == j {
continue; }
let returns_i = returns.get(&self.assets[i]).unwrap();
let returns_j = returns.get(&self.assets[j]).unwrap();
let correlation = Self::calculate_correlation(returns_i, returns_j)?;
let idx = Self::index_for(i, j, self.assets.len());
self.correlations[idx] = correlation;
}
}
self.last_update = timestamp;
Ok(())
}
fn calculate_correlation(x: &[Decimal], y: &[Decimal]) -> MMResult<Decimal> {
let n = x.len();
if n < 2 {
return Ok(Decimal::ZERO);
}
let n_dec = Decimal::from(n);
let mean_x: Decimal = x.iter().copied().sum::<Decimal>() / n_dec;
let mean_y: Decimal = y.iter().copied().sum::<Decimal>() / n_dec;
let mut cov = Decimal::ZERO;
let mut var_x = Decimal::ZERO;
let mut var_y = Decimal::ZERO;
for i in 0..n {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
cov += dx * dy;
var_x += dx * dx;
var_y += dy * dy;
}
if var_x.is_zero() || var_y.is_zero() {
return Ok(Decimal::ZERO);
}
let denominator = decimal_sqrt(var_x * var_y)?;
if denominator.is_zero() {
return Ok(Decimal::ZERO);
}
let correlation = cov / denominator;
Ok(correlation.max(Decimal::NEGATIVE_ONE).min(Decimal::ONE))
}
#[must_use]
pub fn is_valid(&self) -> bool {
let n = self.assets.len();
for i in 0..n {
for j in i..n {
let idx = Self::index_for(i, j, n);
let corr = self.correlations[idx];
if i == j {
if corr != Decimal::ONE {
return false;
}
} else {
if corr < Decimal::NEGATIVE_ONE || corr > Decimal::ONE {
return false;
}
}
}
}
true
}
#[must_use]
pub fn to_matrix(&self) -> Vec<Vec<Decimal>> {
let n = self.assets.len();
let mut matrix = vec![vec![Decimal::ZERO; n]; n];
for (i, row) in matrix.iter_mut().enumerate() {
for (j, cell) in row.iter_mut().enumerate() {
let idx = Self::index_for(i, j, n);
*cell = self.correlations[idx];
}
}
matrix
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PortfolioPosition {
positions: HashMap<AssetId, Decimal>,
volatilities: HashMap<AssetId, Decimal>,
}
impl PortfolioPosition {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_position(&mut self, asset: AssetId, position: Decimal, volatility: Decimal) {
self.positions.insert(asset.clone(), position);
self.volatilities.insert(asset, volatility);
}
#[must_use]
pub fn get_position(&self, asset: &AssetId) -> Option<Decimal> {
self.positions.get(asset).copied()
}
#[must_use]
pub fn get_volatility(&self, asset: &AssetId) -> Option<Decimal> {
self.volatilities.get(asset).copied()
}
pub fn remove_asset(&mut self, asset: &AssetId) {
self.positions.remove(asset);
self.volatilities.remove(asset);
}
#[must_use]
pub fn assets(&self) -> Vec<&AssetId> {
self.positions.keys().collect()
}
#[must_use]
pub fn asset_count(&self) -> usize {
self.positions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
#[must_use]
pub fn total_absolute_position(&self) -> Decimal {
self.positions.values().map(|p| p.abs()).sum()
}
#[must_use]
pub fn net_position(&self) -> Decimal {
self.positions.values().copied().sum()
}
}
#[derive(Debug, Clone)]
pub struct PortfolioRiskCalculator {
correlation_matrix: CorrelationMatrix,
}
impl PortfolioRiskCalculator {
#[must_use]
pub fn new(correlation_matrix: CorrelationMatrix) -> Self {
Self { correlation_matrix }
}
#[must_use]
pub fn correlation_matrix(&self) -> &CorrelationMatrix {
&self.correlation_matrix
}
pub fn portfolio_variance(&self, portfolio: &PortfolioPosition) -> MMResult<Decimal> {
let assets = portfolio.assets();
if assets.is_empty() {
return Ok(Decimal::ZERO);
}
let mut variance = Decimal::ZERO;
for asset_i in &assets {
let pos_i = portfolio.get_position(asset_i).unwrap_or(Decimal::ZERO);
let vol_i = portfolio.get_volatility(asset_i).unwrap_or(Decimal::ZERO);
for asset_j in &assets {
let pos_j = portfolio.get_position(asset_j).unwrap_or(Decimal::ZERO);
let vol_j = portfolio.get_volatility(asset_j).unwrap_or(Decimal::ZERO);
let correlation = self
.correlation_matrix
.get_correlation(asset_i, asset_j)
.unwrap_or_else(|| {
if asset_i == asset_j {
Decimal::ONE
} else {
Decimal::ZERO
}
});
variance += pos_i * pos_j * vol_i * vol_j * correlation;
}
}
Ok(variance)
}
pub fn portfolio_volatility(&self, portfolio: &PortfolioPosition) -> MMResult<Decimal> {
let variance = self.portfolio_variance(portfolio)?;
if variance <= Decimal::ZERO {
return Ok(Decimal::ZERO);
}
decimal_sqrt(variance)
}
pub fn portfolio_var(
&self,
portfolio: &PortfolioPosition,
confidence: Decimal,
horizon_days: u32,
) -> MMResult<Decimal> {
let volatility = self.portfolio_volatility(portfolio)?;
let z_score = self.confidence_to_z_score(confidence);
let horizon_factor = decimal_sqrt(Decimal::from(horizon_days))?;
Ok(z_score * volatility * horizon_factor)
}
fn confidence_to_z_score(&self, confidence: Decimal) -> Decimal {
let conf_90 = Decimal::from_str_exact("0.90").unwrap();
let conf_95 = Decimal::from_str_exact("0.95").unwrap();
let conf_99 = Decimal::from_str_exact("0.99").unwrap();
if confidence >= conf_99 {
Decimal::from_str_exact("2.326").unwrap() } else if confidence >= conf_95 {
Decimal::from_str_exact("1.645").unwrap() } else if confidence >= conf_90 {
Decimal::from_str_exact("1.282").unwrap() } else {
Decimal::ONE }
}
pub fn marginal_risk_contribution(
&self,
portfolio: &PortfolioPosition,
) -> MMResult<HashMap<AssetId, Decimal>> {
let portfolio_vol = self.portfolio_volatility(portfolio)?;
if portfolio_vol.is_zero() {
return Ok(portfolio
.assets()
.into_iter()
.map(|a| (a.clone(), Decimal::ZERO))
.collect());
}
let mut contributions = HashMap::new();
for asset_i in portfolio.assets() {
let pos_i = portfolio.get_position(asset_i).unwrap_or(Decimal::ZERO);
let vol_i = portfolio.get_volatility(asset_i).unwrap_or(Decimal::ZERO);
let mut cov_contribution = Decimal::ZERO;
for asset_j in portfolio.assets() {
let pos_j = portfolio.get_position(asset_j).unwrap_or(Decimal::ZERO);
let vol_j = portfolio.get_volatility(asset_j).unwrap_or(Decimal::ZERO);
let correlation = self
.correlation_matrix
.get_correlation(asset_i, asset_j)
.unwrap_or_else(|| {
if asset_i == asset_j {
Decimal::ONE
} else {
Decimal::ZERO
}
});
cov_contribution += pos_j * vol_i * vol_j * correlation;
}
let marginal = (cov_contribution / portfolio_vol) * pos_i;
contributions.insert(asset_i.clone(), marginal);
}
Ok(contributions)
}
pub fn diversification_ratio(&self, portfolio: &PortfolioPosition) -> MMResult<Decimal> {
let portfolio_vol = self.portfolio_volatility(portfolio)?;
if portfolio_vol.is_zero() {
return Ok(Decimal::ONE);
}
let weighted_vol_sum: Decimal = portfolio
.assets()
.iter()
.map(|asset| {
let pos = portfolio.get_position(asset).unwrap_or(Decimal::ZERO);
let vol = portfolio.get_volatility(asset).unwrap_or(Decimal::ZERO);
pos.abs() * vol
})
.sum();
Ok(weighted_vol_sum / portfolio_vol)
}
}
#[derive(Debug, Clone)]
pub struct HedgeCalculator {
correlation_matrix: CorrelationMatrix,
}
impl HedgeCalculator {
#[must_use]
pub fn new(correlation_matrix: CorrelationMatrix) -> Self {
Self { correlation_matrix }
}
pub fn hedge_ratio(
&self,
target: &AssetId,
hedge: &AssetId,
target_vol: Decimal,
hedge_vol: Decimal,
) -> MMResult<Decimal> {
if hedge_vol.is_zero() {
return Err(MMError::InvalidConfiguration(
"Hedge asset volatility cannot be zero".to_string(),
));
}
let correlation = self
.correlation_matrix
.get_correlation(target, hedge)
.ok_or_else(|| {
MMError::InvalidConfiguration(format!(
"No correlation between {} and {}",
target, hedge
))
})?;
let ratio = -correlation * (target_vol / hedge_vol);
Ok(ratio)
}
pub fn find_best_hedge(
&self,
target: &AssetId,
available: &[AssetId],
volatilities: &HashMap<AssetId, Decimal>,
) -> Option<(AssetId, Decimal)> {
let target_vol = volatilities.get(target)?;
let mut best: Option<(AssetId, Decimal, Decimal)> = None;
for hedge_asset in available {
if hedge_asset == target {
continue;
}
let correlation = self
.correlation_matrix
.get_correlation(target, hedge_asset)?;
let hedge_vol = volatilities.get(hedge_asset)?;
if hedge_vol.is_zero() {
continue;
}
let abs_corr = correlation.abs();
match &best {
None => {
let ratio = -correlation * (*target_vol / *hedge_vol);
best = Some((hedge_asset.clone(), abs_corr, ratio));
}
Some((_, best_corr, _)) if abs_corr > *best_corr => {
let ratio = -correlation * (*target_vol / *hedge_vol);
best = Some((hedge_asset.clone(), abs_corr, ratio));
}
_ => {}
}
}
best.map(|(asset, _, ratio)| (asset, ratio))
}
pub fn residual_risk(
&self,
target: &AssetId,
hedge: &AssetId,
target_vol: Decimal,
) -> MMResult<Decimal> {
let correlation = self
.correlation_matrix
.get_correlation(target, hedge)
.ok_or_else(|| {
MMError::InvalidConfiguration(format!(
"No correlation between {} and {}",
target, hedge
))
})?;
let one_minus_rho_sq = Decimal::ONE - correlation * correlation;
if one_minus_rho_sq <= Decimal::ZERO {
return Ok(Decimal::ZERO); }
let residual_factor = decimal_sqrt(one_minus_rho_sq)?;
Ok(target_vol * residual_factor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dec;
#[test]
fn test_asset_id_new() {
let asset = AssetId::new("BTC");
assert_eq!(asset.as_str(), "BTC");
assert_eq!(asset.to_string(), "BTC");
}
#[test]
fn test_asset_id_from() {
let asset1: AssetId = "ETH".into();
let asset2: AssetId = String::from("SOL").into();
assert_eq!(asset1.as_str(), "ETH");
assert_eq!(asset2.as_str(), "SOL");
}
#[test]
fn test_asset_id_equality() {
let a1 = AssetId::new("BTC");
let a2 = AssetId::new("BTC");
let a3 = AssetId::new("ETH");
assert_eq!(a1, a2);
assert_ne!(a1, a3);
}
#[test]
fn test_correlation_matrix_new() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
assert_eq!(matrix.asset_count(), 2);
assert_eq!(matrix.get_correlation(&btc, &btc), Some(Decimal::ONE));
assert_eq!(matrix.get_correlation(ð, ð), Some(Decimal::ONE));
assert_eq!(matrix.get_correlation(&btc, ð), Some(Decimal::ZERO));
}
#[test]
fn test_correlation_matrix_set_get() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
assert_eq!(matrix.get_correlation(&btc, ð), Some(dec!(0.8)));
assert_eq!(matrix.get_correlation(ð, &btc), Some(dec!(0.8))); }
#[test]
fn test_correlation_matrix_invalid_range() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
assert!(matrix.set_correlation(&btc, ð, dec!(1.5)).is_err());
assert!(matrix.set_correlation(&btc, ð, dec!(-1.5)).is_err());
}
#[test]
fn test_correlation_matrix_self_correlation() {
let btc = AssetId::new("BTC");
let mut matrix = CorrelationMatrix::new(vec![btc.clone()]);
assert!(matrix.set_correlation(&btc, &btc, dec!(0.5)).is_err());
}
#[test]
fn test_correlation_matrix_is_valid() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
assert!(matrix.is_valid());
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
assert!(matrix.is_valid());
}
#[test]
fn test_correlation_matrix_to_matrix() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.7)).unwrap();
let m = matrix.to_matrix();
assert_eq!(m[0][0], Decimal::ONE);
assert_eq!(m[1][1], Decimal::ONE);
assert_eq!(m[0][1], dec!(0.7));
assert_eq!(m[1][0], dec!(0.7));
}
#[test]
fn test_correlation_matrix_update_from_returns() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
let mut returns = HashMap::new();
returns.insert(
btc.clone(),
vec![dec!(0.01), dec!(0.02), dec!(-0.01), dec!(0.03)],
);
returns.insert(
eth.clone(),
vec![dec!(0.015), dec!(0.025), dec!(-0.005), dec!(0.035)],
);
matrix.update_from_returns(&returns, 1000).unwrap();
let corr = matrix.get_correlation(&btc, ð).unwrap();
assert!(corr > dec!(0.9)); }
#[test]
fn test_portfolio_position_new() {
let portfolio = PortfolioPosition::new();
assert!(portfolio.is_empty());
assert_eq!(portfolio.asset_count(), 0);
}
#[test]
fn test_portfolio_position_set_get() {
let mut portfolio = PortfolioPosition::new();
let btc = AssetId::new("BTC");
portfolio.set_position(btc.clone(), dec!(1.5), dec!(0.05));
assert_eq!(portfolio.get_position(&btc), Some(dec!(1.5)));
assert_eq!(portfolio.get_volatility(&btc), Some(dec!(0.05)));
assert_eq!(portfolio.asset_count(), 1);
}
#[test]
fn test_portfolio_position_remove() {
let mut portfolio = PortfolioPosition::new();
let btc = AssetId::new("BTC");
portfolio.set_position(btc.clone(), dec!(1.0), dec!(0.05));
assert_eq!(portfolio.asset_count(), 1);
portfolio.remove_asset(&btc);
assert_eq!(portfolio.asset_count(), 0);
}
#[test]
fn test_portfolio_position_totals() {
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(AssetId::new("BTC"), dec!(1.0), dec!(0.05));
portfolio.set_position(AssetId::new("ETH"), dec!(-2.0), dec!(0.08));
assert_eq!(portfolio.total_absolute_position(), dec!(3.0));
assert_eq!(portfolio.net_position(), dec!(-1.0));
}
#[test]
fn test_portfolio_variance_single_asset() {
let btc = AssetId::new("BTC");
let matrix = CorrelationMatrix::new(vec![btc.clone()]);
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc, dec!(1.0), dec!(0.05));
let calculator = PortfolioRiskCalculator::new(matrix);
let variance = calculator.portfolio_variance(&portfolio).unwrap();
assert_eq!(variance, dec!(0.0025));
}
#[test]
fn test_portfolio_variance_two_assets() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.5)).unwrap();
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc, dec!(1.0), dec!(0.10));
portfolio.set_position(eth, dec!(1.0), dec!(0.10));
let calculator = PortfolioRiskCalculator::new(matrix);
let variance = calculator.portfolio_variance(&portfolio).unwrap();
assert_eq!(variance, dec!(0.03));
}
#[test]
fn test_portfolio_volatility() {
let btc = AssetId::new("BTC");
let matrix = CorrelationMatrix::new(vec![btc.clone()]);
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc, dec!(1.0), dec!(0.04));
let calculator = PortfolioRiskCalculator::new(matrix);
let volatility = calculator.portfolio_volatility(&portfolio).unwrap();
assert_eq!(volatility, dec!(0.04));
}
#[test]
fn test_portfolio_var() {
let btc = AssetId::new("BTC");
let matrix = CorrelationMatrix::new(vec![btc.clone()]);
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc, dec!(1.0), dec!(0.10));
let calculator = PortfolioRiskCalculator::new(matrix);
let var = calculator.portfolio_var(&portfolio, dec!(0.95), 1).unwrap();
assert_eq!(var, dec!(0.1645));
}
#[test]
fn test_diversification_ratio() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.5)).unwrap();
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc, dec!(1.0), dec!(0.10));
portfolio.set_position(eth, dec!(1.0), dec!(0.10));
let calculator = PortfolioRiskCalculator::new(matrix);
let ratio = calculator.diversification_ratio(&portfolio).unwrap();
assert!(ratio > Decimal::ONE);
}
#[test]
fn test_hedge_ratio() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
let calculator = HedgeCalculator::new(matrix);
let ratio = calculator
.hedge_ratio(&btc, ð, dec!(0.05), dec!(0.10))
.unwrap();
assert_eq!(ratio, dec!(-0.4));
}
#[test]
fn test_hedge_ratio_zero_vol() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
let calculator = HedgeCalculator::new(matrix);
let result = calculator.hedge_ratio(&btc, ð, dec!(0.05), dec!(0.0));
assert!(result.is_err());
}
#[test]
fn test_find_best_hedge() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let sol = AssetId::new("SOL");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone(), sol.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
matrix.set_correlation(&btc, &sol, dec!(0.5)).unwrap();
matrix.set_correlation(ð, &sol, dec!(0.6)).unwrap();
let mut vols = HashMap::new();
vols.insert(btc.clone(), dec!(0.05));
vols.insert(eth.clone(), dec!(0.08));
vols.insert(sol.clone(), dec!(0.12));
let calculator = HedgeCalculator::new(matrix);
let best = calculator.find_best_hedge(&btc, &[eth.clone(), sol.clone()], &vols);
assert!(best.is_some());
let (best_asset, _ratio) = best.unwrap();
assert_eq!(best_asset, eth); }
#[test]
fn test_residual_risk() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.8)).unwrap();
let calculator = HedgeCalculator::new(matrix);
let residual = calculator.residual_risk(&btc, ð, dec!(0.10)).unwrap();
assert!(residual > dec!(0.05));
assert!(residual < dec!(0.07));
}
#[test]
fn test_residual_risk_perfect_correlation() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(1.0)).unwrap();
let calculator = HedgeCalculator::new(matrix);
let residual = calculator.residual_risk(&btc, ð, dec!(0.10)).unwrap();
assert_eq!(residual, Decimal::ZERO);
}
#[test]
fn test_marginal_risk_contribution() {
let btc = AssetId::new("BTC");
let eth = AssetId::new("ETH");
let mut matrix = CorrelationMatrix::new(vec![btc.clone(), eth.clone()]);
matrix.set_correlation(&btc, ð, dec!(0.5)).unwrap();
let mut portfolio = PortfolioPosition::new();
portfolio.set_position(btc.clone(), dec!(1.0), dec!(0.10));
portfolio.set_position(eth.clone(), dec!(1.0), dec!(0.10));
let calculator = PortfolioRiskCalculator::new(matrix);
let contributions = calculator.marginal_risk_contribution(&portfolio).unwrap();
assert!(contributions.get(&btc).unwrap() > &Decimal::ZERO);
assert!(contributions.get(ð).unwrap() > &Decimal::ZERO);
}
}