use ndarray::Array1;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::{compare_len, FeatureCollection};
use crate::statistics::{covariance, LinearRegressionSettings, LinearRegressionResult, LinearRegressionAnalysis};
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CAPMParams {
pub alpha: Option<f64>,
pub beta: f64,
pub beta_s: Option<f64>,
pub beta_h: Option<f64>,
pub beta_r: Option<f64>,
pub beta_c: Option<f64>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CAPMType {
Standard,
ThreeFactorFamaFrench {
smb: Array1<f64>,
hml: Array1<f64>,
},
FiveFactorFamaFrench {
smb: Array1<f64>,
hml: Array1<f64>,
rmw: Array1<f64>,
cma: Array1<f64>,
},
}
impl CAPMType {
pub fn self_validate(&self) -> Result<(), DigiFiError> {
match self {
Self::Standard => Ok(()),
Self::ThreeFactorFamaFrench { smb, hml } => {
compare_len(&smb.iter(), &hml.iter(), "smb", "hml")
},
Self::FiveFactorFamaFrench { smb, hml, rmw, cma } => {
compare_len(&smb.iter(), &hml.iter(), "smb", "hml")?;
compare_len(&smb.iter(), &rmw.iter(), "smb", "rmw")?;
compare_len(&smb.iter(), &cma.iter(), "smb", "cma")
},
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CAPMSolutionType {
LinearRegression,
Covariance,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CAPM {
market_returns: Array1<f64>,
rf: Array1<f64>,
capm_type: CAPMType,
solution_type: CAPMSolutionType,
}
impl CAPM {
pub fn build(market_returns: Array1<f64>, rf: Array1<f64>, capm_type: CAPMType, solution_type: CAPMSolutionType) -> Result<Self, DigiFiError> {
compare_len(&market_returns.iter(), &rf.iter(), "market_returns", "rf")?;
capm_type.self_validate()?;
match &capm_type {
CAPMType::Standard => (),
CAPMType::ThreeFactorFamaFrench { smb, .. } => {
compare_len(&market_returns.iter(), &smb.iter(), "market_returns", "smb")?;
},
CAPMType::FiveFactorFamaFrench { smb, ..} => {
compare_len(&market_returns.iter(), &smb.iter(), "market_returns", "smb")?;
},
};
match capm_type {
CAPMType::Standard => (),
_ => {
match solution_type {
CAPMSolutionType::Covariance => {
return Err(DigiFiError::ValidationError { title: "CAPM".to_owned(), details: "The `covariance` solution method is only available for the standard CAPM.".to_owned(), });
},
CAPMSolutionType::LinearRegression => (),
}
}
}
Ok(Self { market_returns, rf, capm_type, solution_type })
}
pub fn predict_asset_return(&self, alpha: f64, beta: f64, beta_s: f64, beta_h: f64, beta_r: f64, beta_c: f64) -> Result<Array1<f64>, DigiFiError> {
let mut lin_reg: Array1<f64> = alpha + beta * (&self.market_returns - &self.rf);
match &self.capm_type {
CAPMType::Standard => (),
CAPMType::ThreeFactorFamaFrench { smb, hml } => {
lin_reg = lin_reg + (beta_s * smb) + (beta_h * hml);
},
CAPMType::FiveFactorFamaFrench { smb, hml, rmw, cma } => {
lin_reg = lin_reg + (beta_s * smb) + (beta_h * hml) + (beta_r * rmw) + (beta_c * cma);
},
}
Ok(lin_reg)
}
fn train(&self, risk_premium: &Array1<f64>) -> Result<CAPMParams, DigiFiError> {
let mut fc: FeatureCollection = FeatureCollection::new();
fc.add_constant = true;
fc.add_feature((&self.market_returns - &self.rf).into_iter(), "Market Premium")?;
match &self.capm_type {
CAPMType::Standard => (),
CAPMType::ThreeFactorFamaFrench { smb, hml } => {
fc.add_feature(smb.iter(), "SMB")?;
fc.add_feature(hml.iter(), "HML")?;
},
CAPMType::FiveFactorFamaFrench { smb, hml, rmw, cma } => {
fc.add_feature(smb.iter(), "SMB")?;
fc.add_feature(hml.iter(), "HML")?;
fc.add_feature(rmw.iter(), "RMW")?;
fc.add_feature(cma.iter(), "CMA")?;
},
}
let lra_result: LinearRegressionResult = LinearRegressionAnalysis::new(LinearRegressionSettings::disable_all()).run(&fc, &risk_premium)?;
self.unstack_parameters(lra_result.all_coefficients.into_iter())
}
fn unstack_parameters(&self, mut params: impl Iterator<Item = f64>) -> Result<CAPMParams, DigiFiError> {
let mut reg_params: CAPMParams = CAPMParams { alpha: None, beta: 0.0, beta_s: None, beta_h: None, beta_r: None, beta_c: None };
reg_params.beta = match params.next() {
Some(v) => v,
None => return Err(DigiFiError::Other { title: Self::error_title(), details: "No `beta` was found.".to_owned(), }),
};
if let CAPMType::ThreeFactorFamaFrench { .. } | CAPMType::FiveFactorFamaFrench { .. } = &self.capm_type {
reg_params.beta_s = params.next();
reg_params.beta_h = params.next();
}
if let CAPMType::FiveFactorFamaFrench { .. } = &self.capm_type {
reg_params.beta_r = params.next();
reg_params.beta_c = params.next();
}
reg_params.alpha = params.next();
Ok(reg_params)
}
pub fn get_parameters(&self, risk_premium: &Array1<f64>) -> Result<CAPMParams, DigiFiError> {
compare_len(&risk_premium.iter(), &self.market_returns.iter(), "risk_premium", "market_returns")?;
match self.solution_type {
CAPMSolutionType::Covariance => {
let numerator: f64 = covariance(risk_premium, &self.market_returns, 0)?;
let denominator: f64 = covariance(&self.market_returns, &self.market_returns, 0)?;
Ok(CAPMParams { alpha: None, beta: numerator/denominator, beta_s: None, beta_h: None, beta_r: None, beta_c: None })
},
CAPMSolutionType::LinearRegression => {
self.train(risk_premium)
},
}
}
}
impl ErrorTitle for CAPM {
fn error_title() -> String {
String::from("CAPM")
}
}
#[cfg(all(test, feature = "sample_data"))]
mod tests {
use crate::utilities::{TEST_ACCURACY, sample_data::SampleData};
use crate::corporate_finance::capm::{CAPMParams, CAPMSolutionType, CAPMType, CAPM};
#[test]
fn unit_test_capm_get_parameters() -> () {
let sample: SampleData = SampleData::CAPM;
let (_, mut sample_data) = sample.load_sample_data();
let capm_type: CAPMType = CAPMType::FiveFactorFamaFrench {
smb: sample_data.remove("SMB").unwrap(), hml: sample_data.remove("HML").unwrap(),
rmw: sample_data.remove("RMW").unwrap(), cma: sample_data.remove("CMA").unwrap(),
};
let solution_type: CAPMSolutionType = CAPMSolutionType::LinearRegression;
let capm: CAPM = CAPM::build(sample_data.remove("Market").unwrap(), sample_data.remove("RF").unwrap(), capm_type, solution_type).unwrap();
let params: CAPMParams = capm.get_parameters(&sample_data.remove("Stock Returns").unwrap()).unwrap();
assert!((params.alpha.unwrap() - 0.01353015).abs() < TEST_ACCURACY);
assert!((params.beta - 1.37731033).abs() < TEST_ACCURACY);
assert!((params.beta_s.unwrap() - -0.38490771).abs() < TEST_ACCURACY);
assert!((params.beta_h.unwrap() - -0.58771487).abs() < TEST_ACCURACY);
assert!((params.beta_r.unwrap() - 0.11692186).abs() < TEST_ACCURACY);
assert!((params.beta_c.unwrap() - 0.4192746).abs() < TEST_ACCURACY);
}
}