use ndarray::Array1;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::{compare_len, FeatureCollection, time_value_utils::{Compounding, CompoundingType, Perpetuity}};
use crate::financial_instruments::{FinancialInstrument, FinancialInstrumentId};
use crate::corporate_finance;
use crate::portfolio_applications::{AssetHistData, PortfolioInstrument};
use crate::statistics::{LinearRegressionSettings, LinearRegressionResult, LinearRegressionAnalysis};
use crate::stochastic_processes::StochasticProcess;
#[derive(Clone, Copy, Debug)]
pub enum QuoteValues {
PerShare,
Total,
}
#[derive(Debug)]
pub enum StockValuationType {
DividendDiscountModel,
ValuationByComparables { params: ValuationByComparablesParams },
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ValuationByComparablesParams {
valuations: Array1<f64>,
pe_ratios: Option<Array1<f64>>,
pb_ratios: Option<Array1<f64>>,
ev_to_ebitda: Option<Array1<f64>>,
}
impl ValuationByComparablesParams {
pub fn build(valuations: Array1<f64>, pe_ratios: Option<Array1<f64>>, pb_ratios: Option<Array1<f64>>, ev_to_ebitda: Option<Array1<f64>>) -> Result<Self, DigiFiError> {
if valuations.len() < 5 {
return Err(DigiFiError::ValidationError { title: Self::error_title(), details: "Minimum number of datapoints required is `5`.".to_owned(), });
}
Self::validate_array(&valuations, &pe_ratios, "pe_ratios")?;
Self::validate_array(&valuations, &pb_ratios, "pb_ratios")?;
Self::validate_array(&valuations, &ev_to_ebitda, "ev_to_ebitda")?;
Ok(Self { valuations, pe_ratios, pb_ratios, ev_to_ebitda })
}
fn validate_array(valuations: &Array1<f64>, array: &Option<Array1<f64>>, array_name: &str) -> Result<(), DigiFiError> {
match array {
Some(v) => {
compare_len(&valuations.iter(), &v.iter(), "valuations", array_name)?;
Ok(())
},
None => Ok(()),
}
}
pub fn n_parameters(&self) -> usize {
1 + self.pe_ratios.is_some() as usize + self.pb_ratios.is_some() as usize + self.ev_to_ebitda.is_some() as usize
}
pub fn valuations(&self) -> &Array1<f64> {
&self.valuations
}
pub fn pe_ratios(&self) -> &Option<Array1<f64>> {
&self.pe_ratios
}
pub fn pb_ratios(&self) -> &Option<Array1<f64>> {
&self.pb_ratios
}
pub fn ev_to_ebitda(&self) ->& Option<Array1<f64>> {
&self.ev_to_ebitda
}
}
impl ErrorTitle for ValuationByComparablesParams {
fn error_title() -> String {
String::from("Valuation by Comparables Params")
}
}
pub struct Stock {
price_per_share: f64,
n_shares_outstanding: f64,
dividend_per_share: f64,
earnings_per_share: f64,
quote_values: QuoteValues,
initial_price: f64,
compounding_type: CompoundingType,
dividend_growth_rate: f64,
stock_valuation_type: StockValuationType,
pe: Option<f64>,
pb: Option<f64>,
ev_to_ebitda: Option<f64>,
t_f: f64,
financial_instrument_id: FinancialInstrumentId,
asset_historical_data: AssetHistData,
stochastic_model: Option<Box<dyn StochasticProcess>>,
}
impl Stock {
pub fn new(
price_per_share: f64, n_shares_outstanding: i32, dividend_per_share: f64, earnings_per_share: f64, quote_values: QuoteValues, initial_price: f64,
compounding_type: CompoundingType, dividend_growth_rate: f64, stock_valuation_type: StockValuationType, pe: Option<f64>, pb: Option<f64>,
ev_to_ebitda: Option<f64>, t_f: f64, financial_instrument_id: FinancialInstrumentId, asset_historical_data: AssetHistData,
stochastic_model: Option<Box<dyn StochasticProcess>>
) -> Self {
Self {
price_per_share, n_shares_outstanding: n_shares_outstanding as f64, dividend_per_share, earnings_per_share, quote_values, initial_price,
compounding_type, dividend_growth_rate, stock_valuation_type, pe, pb, ev_to_ebitda, t_f, financial_instrument_id, asset_historical_data,
stochastic_model,
}
}
fn apply_value_quotation_type(&self, value_per_share: f64) -> f64 {
match self.quote_values {
QuoteValues::PerShare => value_per_share,
QuoteValues::Total => value_per_share * self.n_shares_outstanding,
}
}
pub fn share_price(&self) -> f64 {
self.price_per_share
}
pub fn trailing_eps(&mut self, net_income: f64, preferred_dividends: f64, n_common_shares_outstanding: usize, in_place: bool) -> f64 {
let trailing_eps: f64 = corporate_finance::earnings_per_share(net_income, preferred_dividends, n_common_shares_outstanding);
if in_place {
self.earnings_per_share = trailing_eps;
}
trailing_eps
}
pub fn trailing_pe(&mut self, in_place: bool) -> f64 {
let trailing_pe: f64 = corporate_finance::pe_ratio(self.price_per_share, self.earnings_per_share);
if in_place {
self.pe = Some(trailing_pe);
}
trailing_pe
}
pub fn peg(&self, eps_growth: f64) -> f64 {
corporate_finance::peg_ratio(self.price_per_share, self.earnings_per_share, eps_growth)
}
pub fn pb(&mut self, market_cap: f64, assets: f64, liabilities: f64, in_place: bool) -> f64 {
let book_value: f64 = corporate_finance::book_value(assets, liabilities);
let pb: f64 = corporate_finance::pb_ratio(market_cap, book_value);
if in_place {
self.pb = Some(pb);
}
pb
}
pub fn enterprise_value(&self, market_cap: f64, total_debt: f64, cash: f64) -> f64 {
corporate_finance::enterprise_value(market_cap, total_debt, cash)
}
pub fn ev_to_revenue(&self, market_cap: f64, total_debt: f64, cash: f64, revenue: f64) -> f64 {
corporate_finance::ev_to_revenue(market_cap, total_debt, cash, revenue)
}
pub fn ev_to_ebitda(&mut self, market_cap: f64, total_debt: f64, cash: f64, ebitda: f64, in_place: bool) -> f64 {
let ev_to_ebitda: f64 = corporate_finance::ev_to_ebitda(market_cap, total_debt, cash, ebitda);
if in_place {
self.ev_to_ebitda = Some(ev_to_ebitda);
}
ev_to_ebitda
}
pub fn book_value(&self, assets: f64, liabilities: f64) -> f64 {
corporate_finance::book_value(assets, liabilities)
}
pub fn current_ratio(&self, current_assets: f64, current_liabilities: f64) -> f64 {
corporate_finance::current_ratio(current_assets, current_liabilities)
}
pub fn quick_ratio(&self, current_assets: f64, inventories: f64, current_liabilities: f64) -> f64 {
corporate_finance::quick_ratio(current_assets, inventories, current_liabilities)
}
pub fn gross_margin(&self, revenue: f64, cost_of_goods_sold: f64) -> f64 {
corporate_finance::gross_margin(revenue, cost_of_goods_sold)
}
pub fn operating_margin(&self, operating_income: f64, revenue: f64) -> f64 {
corporate_finance::operating_margin(operating_income, revenue)
}
pub fn net_profit_margin(&self, net_income: f64, revenue: f64) -> f64 {
corporate_finance::net_profit_margin(net_income, revenue)
}
pub fn cost_of_equity_capital(&self, expected_dividend: Option<f64>) -> f64 {
let expected_dividend: f64 = match expected_dividend {
Some(v) => v,
None => self.dividend_per_share,
};
expected_dividend / self.price_per_share + self.dividend_growth_rate
}
pub fn return_on_equity(&self, net_income: f64, equity: f64) -> f64 {
corporate_finance::return_on_equity(net_income, equity)
}
pub fn return_on_assets(&self, net_income: f64, total_assets: f64) -> f64 {
corporate_finance::return_on_assets(net_income, total_assets)
}
pub fn return_on_investment(&self, revenue: f64, cost_of_goods_sold: f64) -> f64 {
corporate_finance::return_on_investment(revenue, cost_of_goods_sold)
}
pub fn debt_to_equity(&self, debt: f64, equity: f64) -> f64 {
corporate_finance::debt_to_equity(debt, equity)
}
pub fn interest_coverage_ratio(&self, ebit: f64, interest_expense: f64) -> f64 {
corporate_finance::interest_coverage_ratio(ebit, interest_expense)
}
pub fn debt_service_coverage_ratio(&self, operating_income: f64, current_debt_obligations: f64) -> f64 {
corporate_finance::debt_service_coverage_ratio(operating_income, current_debt_obligations)
}
pub fn asset_coverage_ratio(&self, total_assets: f64, current_liabilities: f64, total_debt: f64) -> f64 {
corporate_finance::asset_coverage_ratio(total_assets, current_liabilities, total_debt)
}
pub fn liquidity_ratio(&self, liquid_assets: f64, current_liabilities: f64) -> f64 {
corporate_finance::liquidity_ratio(liquid_assets, current_liabilities)
}
pub fn cash_flow_to_debt(&self, operating_cash_flow: f64, total_debt: f64) -> f64 {
corporate_finance::cash_flow_to_debt(operating_cash_flow, total_debt)
}
pub fn dividend_discount_model(&self, expected_dividend: Option<f64>) -> Result<f64, DigiFiError> {
let cost_of_equity_capital: f64 = self.cost_of_equity_capital(expected_dividend);
let dividend_perpetuity: Perpetuity = Perpetuity::build(
self.dividend_per_share, cost_of_equity_capital, self.dividend_growth_rate, self.compounding_type.clone()
)?;
let pv: f64 = dividend_perpetuity.present_value();
Ok(self.apply_value_quotation_type(pv))
}
pub fn valuation_by_comparables(&self, pe: Option<f64>, pb: Option<f64>, ev_to_ebitda: Option<f64>, valuation_params: &ValuationByComparablesParams) -> Result<f64, DigiFiError> {
let add_feature = |feature: &Option<Array1<f64>>, multiple: &Option<f64>, fc: &mut FeatureCollection, x: &mut Vec<f64>, feature_name: &str, multiple_name: &str| -> Result<(), DigiFiError> {
match (feature, multiple) {
(Some(f), Some(m)) => {
fc.add_feature(f.iter(), feature_name)?;
x.push(*m);
Ok(())
},
(None, None) => Ok(()),
_ => return Err(DigiFiError::ValidationError {
title: Self::error_title(),
details: format!("The argument `{}` is None, while the argument `{}` is not. They must be the same variant of `Option` enum.", feature_name, multiple_name),
}),
}
};
let mut x: Vec<f64> = Vec::with_capacity(valuation_params.n_parameters());
let mut fc: FeatureCollection = FeatureCollection::new();
fc.add_constant = true;
add_feature(valuation_params.pe_ratios(), &pe, &mut fc, &mut x, "P/E Ratio", "P/E")?;
add_feature(valuation_params.pb_ratios(), &pb, &mut fc, &mut x, "P/B Ratio", "P/B")?;
add_feature(valuation_params.ev_to_ebitda(), &ev_to_ebitda, &mut fc, &mut x, "EV/EBITDA", "EV/EBITDA")?;
x.push(1.0); let lra_result: LinearRegressionResult = LinearRegressionAnalysis::new(LinearRegressionSettings::disable_all()).run(&fc, valuation_params.valuations())?;
let valuation: f64 = lra_result.all_coefficients.dot(&Array1::from_vec(x));
Ok(self.apply_value_quotation_type(valuation / self.n_shares_outstanding))
}
pub fn payout_ratio(&self) -> f64 {
corporate_finance::payout_ratio(self.dividend_per_share, self.earnings_per_share)
}
pub fn plowback_ratio(&self) -> f64 {
corporate_finance::plowback_ratio(self.dividend_per_share, self.earnings_per_share)
}
pub fn dividend_growth_rate(&mut self, plowback_ratio: f64, roe: f64, in_place: bool) -> f64 {
let g: f64 = plowback_ratio * roe;
if in_place {
self.dividend_growth_rate = g
}
g
}
pub fn present_value_of_growth_opportunities(&self, expected_earnings: Option<f64>, expected_dividend: Option<f64>) -> f64 {
let expected_earnings: f64 = expected_earnings.unwrap_or(self.earnings_per_share);
let r: f64 = self.cost_of_equity_capital(expected_dividend);
self.price_per_share - expected_earnings / r
}
}
impl ErrorTitle for Stock {
fn error_title() -> String {
String::from("Stock")
}
}
impl FinancialInstrument for Stock {
fn present_value(&self) -> Result<f64, DigiFiError> {
match &self.stock_valuation_type {
StockValuationType::DividendDiscountModel => {
Ok(self.dividend_discount_model(None)?)
},
StockValuationType::ValuationByComparables { params } => {
self.valuation_by_comparables(self.pe, self.pb, self.ev_to_ebitda, params)
},
}
}
fn net_present_value(&self) -> Result<f64, DigiFiError> {
Ok(self.present_value()? - self.initial_price)
}
fn future_value(&self) -> Result<f64, DigiFiError> {
let r: f64 = self.cost_of_equity_capital(None);
let discount_term: Compounding = Compounding::new(r, &self.compounding_type);
Ok(self.present_value()? / discount_term.compounding_term(self.t_f))
}
fn historical_data(&self) -> &AssetHistData {
&self.asset_historical_data
}
fn update_historical_data(&mut self, new_data: &AssetHistData) -> () {
self.asset_historical_data = new_data.clone();
}
fn stochastic_model(&mut self) -> &mut Option<Box<dyn StochasticProcess>> {
&mut self.stochastic_model
}
}
impl PortfolioInstrument for Stock {
fn asset_name(&self) -> String {
self.financial_instrument_id.identifier.clone()
}
fn historical_data(&self) -> &AssetHistData {
&self.asset_historical_data
}
}
#[cfg(test)]
mod tests {
use ndarray::Array1;
use crate::utilities::{TEST_ACCURACY, Time, time_value_utils::CompoundingType};
use crate::financial_instruments::{FinancialInstrument, FinancialInstrumentId, FinancialInstrumentType, AssetClass};
use crate::financial_instruments::stocks::{QuoteValues, StockValuationType, ValuationByComparablesParams, Stock};
use crate::portfolio_applications::AssetHistData;
#[test]
fn unit_test_stock_dividend_discount_model() -> () {
let compounding_type: CompoundingType = CompoundingType::Continuous;
let financial_instrument_id: FinancialInstrumentId = FinancialInstrumentId {
instrument_type: FinancialInstrumentType::CashInstrument, asset_class: AssetClass::EquityBasedInstrument,
identifier: String::from("32198407128904"),
};
let asset_historical_data: AssetHistData = AssetHistData::build(
Array1::from_vec(vec![0.4, 0.5]), Array1::from_vec(vec![0.0, 0.0]), Time::new(Array1::from_vec(vec![0.0, 1.0]))
).unwrap();
let stock: Stock = Stock::new(
100.0, 1_000_000, 3.0, 2.5, QuoteValues::PerShare, 99.0, compounding_type, 0.0,
StockValuationType::DividendDiscountModel, Some(10.0), Some(3.0), Some(6.5), 5.0, financial_instrument_id,
asset_historical_data, None
);
let r: f64 = 3.0 / 100.0 + 0.0;
let perpetuity_pv: f64 = 3.0 * 0.0_f64.exp() / ((r - 0.0).exp() - 1.0);
assert!((stock.present_value().unwrap() - perpetuity_pv).abs() < TEST_ACCURACY);
}
#[test]
fn unit_test_stock_valuation_by_comparables() -> () {
let compounding_type: CompoundingType = CompoundingType::Continuous;
let financial_instrument_id: FinancialInstrumentId = FinancialInstrumentId {
instrument_type: FinancialInstrumentType::CashInstrument, asset_class: AssetClass::EquityBasedInstrument,
identifier: String::from("32198407128904"),
};
let asset_historical_data: AssetHistData = AssetHistData::build(
Array1::from_vec(vec![0.4, 0.5]), Array1::from_vec(vec![0.0, 0.0]), Time::new(Array1::from_vec(vec![0.0, 1.0]))
).unwrap();
let valuations: Array1<f64> = Array1::from_vec(vec![200_000_000.0, 1_000_000_000.0, 3_000_000_000.0, 500_000_000.0, 1_500_000_000.0]);
let pe_ratios: Array1<f64> = Array1::from_vec(vec![20.0, 8.0, 9.0, 15.0, 11.0]);
let pb_ratios: Array1<f64> = Array1::from_vec(vec![7.0, 2.0, 4.0, 10.0, 5.0]);
let ev_to_ebitda: Array1<f64> = Array1::from_vec(vec![10.0, 5.0, 6.0, 7.0, 6.0]);
let valuation_params: ValuationByComparablesParams = ValuationByComparablesParams::build(
valuations, Some(pe_ratios), Some(pb_ratios), Some(ev_to_ebitda)
).unwrap();
let stock: Stock = Stock::new(
100.0, 1_000_000, 3.0, 2.5, QuoteValues::Total, 99.0, compounding_type, 0.0,
StockValuationType::ValuationByComparables { params: valuation_params }, Some(10.0), Some(3.0),
Some(6.5), 5.0, financial_instrument_id, asset_historical_data, None
);
assert!(1_000_000_000.0 < stock.present_value().unwrap());
}
}