use std::borrow::Borrow;
use ndarray::Array1;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "plotly")]
use plotly::{Plot, Surface, Layout, layout::{Axis, LayoutScene}, common::Title};
use crate::error::{DigiFiError, ErrorTitle};
use crate::utilities::time_value_utils::{Compounding, CompoundingType};
use crate::financial_instruments::{FinancialInstrument, FinancialInstrumentId, Payoff};
use crate::portfolio_applications::{AssetHistData, PortfolioInstrument};
use crate::statistics::{ProbabilityDistribution, continuous_distributions::NormalDistribution};
use crate::lattice_models::LatticeModel;
use crate::stochastic_processes::StochasticProcess;
use crate::lattice_models::{binomial_models::BrownianMotionBinomialModel, trinomial_models::BrownianMotionTrinomialModel};
pub fn minimum_variance_hedge_ratio(asset_price_sigma: f64, contract_price_sigma: f64, asset_to_contract_corr: f64) -> f64 {
asset_to_contract_corr * asset_price_sigma / contract_price_sigma
}
#[derive(Clone, Copy, Debug)]
pub enum ContractType {
Forward,
Future,
BillsOfExchange,
}
#[derive(Debug)]
pub enum OptionType {
European,
American,
Bermudan { exercise_time_steps: Vec<bool> },
}
#[derive(Clone, Copy, Debug)]
pub enum BlackScholesType {
Call,
Put,
}
#[derive(Clone, Copy, Debug)]
pub enum OptionPricingMethod {
BlackScholes { type_: BlackScholesType },
Binomial { n_steps: usize },
Trinomial { n_steps: usize },
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PresentValueSurface {
pub times_to_maturity: Array1<f64>,
pub price_array: Array1<f64>,
pub pv_matrix: Vec<Array1<f64>>,
}
pub fn black_scholes_formula(s: f64, k: f64, sigma: f64, time_to_maturity: f64, r: f64, q: f64, type_: &BlackScholesType) -> Result<f64, DigiFiError> {
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
let d_1_numerator: f64 = (s/k).ln() + time_to_maturity * (r - q + 0.5*sigma.powi(2));
let d_1: f64 = d_1_numerator / (sigma * time_to_maturity.sqrt());
let d_2: f64 = d_1 - sigma * time_to_maturity.sqrt();
match type_ {
BlackScholesType::Call => {
let component_1: f64 = s * (-q * time_to_maturity).exp() * normal_dist.cdf(d_1)?;
let component_2: f64 = k * (-r * time_to_maturity).exp() * normal_dist.cdf(d_2)?;
Ok(component_1 - component_2)
},
BlackScholesType::Put => {
let component_1: f64 = s * (-q * time_to_maturity).exp() * normal_dist.cdf(-d_1)?;
let component_2: f64 = k * (-r * time_to_maturity).exp() * normal_dist.cdf(-d_2)?;
Ok(component_2 - component_1)
},
}
}
pub struct Contract {
_contract_type: ContractType,
current_contract_price: f64,
delivery_price: f64,
discount_rate: f64,
time_to_maturity: f64,
spot_price: f64,
compounding_type: CompoundingType,
financial_instrument_id: FinancialInstrumentId,
asset_historical_data: AssetHistData,
stochastic_model: Option<Box<dyn StochasticProcess>>,
}
impl Contract {
pub fn build(
_contract_type: ContractType, current_contract_price: f64, delivery_price: f64, discount_rate: f64, time_to_maturity: f64, spot_price: f64,
compounding_type: CompoundingType, financial_instrument_id: FinancialInstrumentId, asset_historical_data: AssetHistData,
stochastic_model: Option<Box<dyn StochasticProcess>>
) -> Result<Self, DigiFiError> {
if time_to_maturity < 0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `time_to_maturity` must be non-negative.".to_owned(),
});
}
Ok(Self {
_contract_type, current_contract_price, delivery_price, discount_rate, time_to_maturity, spot_price, compounding_type,
financial_instrument_id, asset_historical_data, stochastic_model,
})
}
pub fn update_spot_price(&mut self, new_spot_price: f64) -> () {
self.spot_price = new_spot_price;
}
pub fn update_time_to_maturity(&mut self, new_time_to_maturity: f64) -> Result<(), DigiFiError> {
if new_time_to_maturity < 0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `new_time_to_maturity` must be non-negative.".to_owned(),
});
}
self.time_to_maturity = new_time_to_maturity;
Ok(())
}
pub fn append_predictable_yield(&mut self, predictable_yield: f64) -> () {
self.discount_rate += predictable_yield;
}
pub fn append_predictable_income(&mut self, predictable_income: f64) -> () {
self.spot_price += predictable_income;
}
pub fn forward_price(&self) -> Result<f64, DigiFiError> {
let discount_term: Compounding = Compounding::new(self.discount_rate, &self.compounding_type);
Ok(self.spot_price / discount_term.compounding_term(self.time_to_maturity))
}
}
impl ErrorTitle for Contract {
fn error_title() -> String {
String::from("Contract")
}
}
impl FinancialInstrument for Contract {
fn present_value(&self) -> Result<f64, DigiFiError> {
let discount_term: Compounding = Compounding::new(self.discount_rate, &self.compounding_type);
let forward_price: f64 = self.forward_price()?;
Ok((forward_price - self.delivery_price) * discount_term.compounding_term(self.time_to_maturity))
}
fn net_present_value(&self) -> Result<f64, DigiFiError> {
Ok(-self.current_contract_price + self.present_value()?)
}
fn future_value(&self) -> Result<f64, DigiFiError> {
let discount_term: Compounding = Compounding::new(self.discount_rate, &self.compounding_type);
Ok(self.present_value()? / discount_term.compounding_term(self.time_to_maturity))
}
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 Contract {
fn asset_name(&self) -> String {
self.financial_instrument_id.identifier.clone()
}
fn historical_data(&self) -> &AssetHistData {
&self.asset_historical_data
}
}
pub struct OptionContract {
asset_price: f64,
strike_price: f64,
discount_rate: f64,
yield_: f64,
time_to_maturity: f64,
sigma: f64,
option_type: OptionType,
payoff: Box<dyn Payoff>,
initial_option_price: f64,
option_pricing_method: OptionPricingMethod,
financial_instrument_id: FinancialInstrumentId,
asset_historical_data: AssetHistData,
stochastic_model: Option<Box<dyn StochasticProcess>>,
}
impl OptionContract {
pub fn build(
asset_price: f64, strike_price: f64, discount_rate: f64, yield_: f64, time_to_maturity: f64, sigma: f64, option_type: OptionType,
payoff: Box<dyn Payoff>, initial_option_price: f64, option_pricing_method: OptionPricingMethod, financial_instrument_id: FinancialInstrumentId,
asset_historical_data: AssetHistData, stochastic_model: Option<Box<dyn StochasticProcess>>
) -> Result<Self, DigiFiError> {
if time_to_maturity < 0.0 {
return Err(DigiFiError::ParameterConstraint { title: Self::error_title(), constraint: "The argument `time_to_maturity` must be non-negative.".to_owned(), });
}
if let OptionPricingMethod::BlackScholes { .. } = option_pricing_method {
match option_type {
OptionType::European => (),
_ => return Err(DigiFiError::ValidationError {
title: Self::error_title(),
details: "`Black-Scholes` option pricing can only be used for `European` options.".to_owned(),
}),
}
}
payoff.validate_payoff(5)?;
Ok(Self {
asset_price, strike_price, discount_rate, yield_, time_to_maturity, sigma, option_type, payoff, initial_option_price, option_pricing_method,
financial_instrument_id, asset_historical_data, stochastic_model,
})
}
fn is_european(&self) -> Result<(), DigiFiError> {
if let OptionType::European = self.option_type { return Ok(()) }
Err(DigiFiError::ValidationError { title: Self::error_title(), details: "The option is not `European`.".to_owned(), })
}
fn is_black_scholes(&self) -> Result<(), DigiFiError> {
if let OptionPricingMethod::BlackScholes { .. } = &self.option_pricing_method {
return Err(DigiFiError::ValidationError {
title: Self::error_title(), details: "The option pricing method must be set to `BlackScholes`".to_owned(),
});
}
Ok(())
}
pub fn delta(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { type_ } = &self.option_pricing_method {
return self.bs_delta(type_)
}
let original_value: f64 = self.asset_price;
let original_pv: f64 = self.present_value()?;
let new_value: f64 = original_value * (1.0 + increment);
self.asset_price = new_value;
let new_pv: f64 = self.present_value()?;
self.asset_price = original_value;
Ok((new_pv - original_pv) / (new_value - original_value))
}
pub fn vega(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { .. } = &self.option_pricing_method {
return self.bs_vega()
}
let original_value: f64 = self.sigma;
let original_pv: f64 = self.present_value()?;
let new_value: f64 = original_value * (1.0 + increment);
self.sigma = new_value;
let new_pv: f64 = self.present_value()?;
self.sigma = original_value;
Ok((new_pv - original_pv) / (new_value - original_value))
}
pub fn theta(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { type_ } = &self.option_pricing_method {
return self.bs_theta(type_)
}
let original_value: f64 = self.time_to_maturity;
let original_pv: f64 = self.present_value()?;
let new_value: f64 = original_value * (1.0 + increment);
self.time_to_maturity = new_value;
let new_pv: f64 = self.present_value()?;
self.time_to_maturity = original_value;
Ok((new_pv - original_pv) / (new_value - original_value))
}
pub fn rho(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { type_ } = &self.option_pricing_method {
return self.bs_rho(type_)
}
let original_value: f64 = self.discount_rate;
let original_pv: f64 = self.present_value()?;
let new_value: f64 = original_value * (1.0 + increment);
self.discount_rate = new_value;
let new_pv: f64 = self.present_value()?;
self.discount_rate = original_value;
Ok((new_pv - original_pv) / (new_value - original_value))
}
pub fn epsilon(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { type_ } = &self.option_pricing_method {
return self.bs_epsilon(type_)
}
let original_value: f64 = self.yield_;
let original_pv: f64 = self.present_value()?;
let new_value: f64 = original_value * (1.0 + increment);
self.yield_ = new_value;
let new_pv: f64 = self.present_value()?;
self.yield_ = original_value;
Ok((new_pv - original_pv) / (new_value - original_value))
}
pub fn gamma(&mut self, increment: f64) -> Result<f64, DigiFiError> {
if let OptionPricingMethod::BlackScholes { .. } = &self.option_pricing_method {
return self.bs_gamma()
}
let original_value: f64 = self.asset_price;
let original_pv: f64 = self.present_value()?;
let up_value: f64 = original_value * (1.0 + increment);
self.asset_price = up_value;
let up_pv: f64 = self.present_value()?;
let down_value: f64 = original_value * (1.0 - increment);
self.asset_price = down_value;
let down_pv: f64 = self.present_value()?;
self.asset_price = original_value;
let delta_1: f64 = (original_pv - down_pv) / (original_value - down_value);
let delta_2: f64 = (up_pv - original_pv) / (up_value - original_value);
Ok((delta_2 - delta_1) / (up_value - down_value))
}
fn european_option_black_scholes_params(&self) -> Result<(f64, f64), DigiFiError> {
self.is_european()?;
self.is_black_scholes()?;
let d_1_numerator: f64 = (self.asset_price/self.strike_price).ln() + self.time_to_maturity * (self.discount_rate - self.yield_ + 0.5*self.sigma.powi(2));
let d_1: f64 = d_1_numerator / (self.sigma * self.time_to_maturity.sqrt());
let d_2: f64 = d_1 - self.sigma * self.time_to_maturity.sqrt();
Ok((d_1, d_2))
}
fn european_option_black_scholes(&self, black_scholes_type: &BlackScholesType) -> Result<f64, DigiFiError> {
self.is_european()?;
black_scholes_formula(self.asset_price, self.strike_price, self.sigma, self.time_to_maturity, self.discount_rate, self.yield_, black_scholes_type)
}
pub fn bs_delta(&self, black_scholes_type: &BlackScholesType) -> Result<f64, DigiFiError> {
let (d_1, _) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
match black_scholes_type {
BlackScholesType::Call => {
Ok((-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(d_1)?)
},
BlackScholesType::Put => {
Ok(-(-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(-d_1)?)
},
}
}
pub fn bs_vega(&self) -> Result<f64, DigiFiError> {
let (d_1, _) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
Ok(self.asset_price * (-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(d_1)? * self.time_to_maturity.sqrt())
}
pub fn bs_theta(&self, black_scholes_type: &BlackScholesType) -> Result<f64, DigiFiError> {
let (d_1, d_2) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
match black_scholes_type {
BlackScholesType::Call => {
let component_1: f64 = -(-self.yield_ * self.time_to_maturity).exp() * self.asset_price * normal_dist.cdf(d_1)? * self.sigma / (2.0 * self.time_to_maturity.sqrt());
let component_2: f64 = self.discount_rate * self.strike_price * (-self.discount_rate * self.time_to_maturity).exp() * normal_dist.cdf(d_2)?;
let component_3: f64 = self.yield_ * self.asset_price * (-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(d_1)?;
Ok(component_1 - component_2 + component_3)
},
BlackScholesType::Put => {
let component_1: f64 = -(-self.yield_ * self.time_to_maturity).exp() * self.asset_price * normal_dist.cdf(d_1)? * self.sigma / (2.0 * self.time_to_maturity.sqrt());
let component_2: f64 = self.discount_rate * self.strike_price * (-self.discount_rate * self.time_to_maturity).exp() * normal_dist.cdf(-d_2)?;
let component_3: f64 = self.yield_ * self.asset_price * (-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(-d_1)?;
Ok(component_1+ component_2 - component_3)
},
}
}
pub fn bs_rho(&self, black_scholes_type: &BlackScholesType) -> Result<f64, DigiFiError> {
let (_, d_2) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
match black_scholes_type {
BlackScholesType::Call => {
Ok(self.strike_price * self.time_to_maturity * (-self.discount_rate * self.time_to_maturity).exp() * normal_dist.cdf(d_2)?)
},
BlackScholesType::Put => {
Ok(-self.strike_price * self.time_to_maturity * (-self.discount_rate * self.time_to_maturity).exp() * normal_dist.cdf(-d_2)?)
},
}
}
pub fn bs_epsilon(&self, black_scholes_type: &BlackScholesType) -> Result<f64, DigiFiError> {
let (d_1, _) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
match black_scholes_type {
BlackScholesType::Call => {
Ok(-self.asset_price * self.time_to_maturity * (-self.yield_*self.time_to_maturity).exp() * normal_dist.cdf(d_1)?)
},
BlackScholesType::Put => {
Ok(self.asset_price * self.time_to_maturity * (-self.yield_*self.time_to_maturity).exp() * normal_dist.cdf(-d_1)?)
},
}
}
pub fn bs_gamma(&self) -> Result<f64, DigiFiError> {
let (d_1, _) = self.european_option_black_scholes_params()?;
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0)?;
Ok((-self.yield_ * self.time_to_maturity).exp() * normal_dist.cdf(d_1)? / (self.asset_price * self.sigma * self.time_to_maturity.sqrt()))
}
pub fn bs_present_value_table<T, I>(&mut self, strike_prices: T) -> Result<Vec<(f64, f64)>, DigiFiError>
where
T: Iterator<Item = I> + ExactSizeIterator,
I: Borrow<f64>,
{
self.is_black_scholes()?;
let original_strike_price: f64 = self.strike_price;
let mut price_table: Vec<(f64, f64)> = Vec::with_capacity(strike_prices.len());
for strike_price in strike_prices {
self.strike_price = *strike_price.borrow();
price_table.push((self.strike_price, self.present_value()?));
}
self.strike_price = original_strike_price;
Ok(price_table)
}
pub fn present_value_surface(&mut self, start_price: f64, stop_price: f64, n_prices: usize, n_time_steps: usize) -> Result<PresentValueSurface, DigiFiError> {
let initial_time_to_maturity: f64 = self.time_to_maturity;
let initial_asset_price: f64 = self.asset_price;
let times_to_maturity: Array1<f64> = Array1::linspace(self.time_to_maturity, 0.0, n_time_steps);
let price_array: Array1<f64> = Array1::linspace(start_price, stop_price, n_prices);
let mut option_pv_matrix: Vec<Array1<f64>> = Vec::with_capacity(n_time_steps);
for i in 0..n_time_steps {
let option_pv_array: Array1<f64>;
if i == (n_time_steps - 1) {
option_pv_array = self.payoff.payoff_iter(&price_array);
} else {
self.time_to_maturity = times_to_maturity[i];
let mut present_values: Vec<f64> = Vec::with_capacity(n_prices);
for j in 0..n_prices {
self.asset_price = price_array[j];
present_values.push(self.present_value()?);
}
option_pv_array = Array1::from_vec(present_values);
}
option_pv_matrix.push(option_pv_array);
}
self.time_to_maturity = initial_time_to_maturity;
self.asset_price = initial_asset_price;
Ok(PresentValueSurface { times_to_maturity, price_array, pv_matrix: option_pv_matrix })
}
}
impl ErrorTitle for OptionContract {
fn error_title() -> String {
String::from("Option Contract")
}
}
impl FinancialInstrument for OptionContract {
fn present_value(&self) -> Result<f64, DigiFiError> {
match &self.option_pricing_method {
OptionPricingMethod::BlackScholes { type_ } => {
self.european_option_black_scholes(type_)
},
OptionPricingMethod::Binomial { n_steps } => {
let lattice_model: BrownianMotionBinomialModel = BrownianMotionBinomialModel::build(self.payoff.clone_box(), self.asset_price, self.time_to_maturity, self.discount_rate, self.sigma, self.yield_, *n_steps)?;
match &self.option_type {
OptionType::European => lattice_model.european(),
OptionType::American => lattice_model.american(),
OptionType::Bermudan { exercise_time_steps } => lattice_model.bermudan(exercise_time_steps),
}
},
OptionPricingMethod::Trinomial { n_steps } => {
let lattice_model: BrownianMotionTrinomialModel = BrownianMotionTrinomialModel::build(self.payoff.clone_box(), self.asset_price, self.time_to_maturity, self.discount_rate, self.sigma, self.yield_, *n_steps)?;
match &self.option_type {
OptionType::European => lattice_model.european(),
OptionType::American => lattice_model.american(),
OptionType::Bermudan { exercise_time_steps } => lattice_model.bermudan(exercise_time_steps),
}
},
}
}
fn net_present_value(&self) -> Result<f64, DigiFiError> {
Ok(-self.initial_option_price + self.present_value()?)
}
fn future_value(&self) -> Result<f64, DigiFiError> {
Ok(self.present_value()? * (self.discount_rate * self.time_to_maturity).exp())
}
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 OptionContract {
fn asset_name(&self) -> String {
self.financial_instrument_id.identifier.clone()
}
fn historical_data(&self) -> &AssetHistData {
&self.asset_historical_data
}
}
#[cfg(feature = "plotly")]
pub fn plot_present_value_surface(surface: &PresentValueSurface) -> Plot {
let x: Vec<f64> = surface.price_array.to_vec();
let y: Vec<f64> = surface.times_to_maturity.to_vec();
let z: Vec<Vec<f64>> = surface.pv_matrix.iter().map(|a| a.to_vec() ).collect();
let mut plot: Plot = Plot::new();
plot.add_trace(Surface::new(z).x(x).y(y).name("Option Premium Surface"));
let x_axis: Axis = Axis::new().title(Title::from("Underlying Asset Price"));
let y_axis: Axis = Axis::new().title(Title::from("Time to Maturity"));
let z_axis: Axis = Axis::new().title(Title::from("Option Premium"));
let layout_scene: LayoutScene = LayoutScene::new().x_axis(x_axis).y_axis(y_axis).z_axis(z_axis);
let layout: Layout = Layout::new().scene(layout_scene).title("<b>Option Premium Surface</b>");
plot.set_layout(layout);
plot
}
#[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, LongCall};
use crate::financial_instruments::derivatives::{ContractType, Contract, OptionType, OptionPricingMethod, BlackScholesType, OptionContract};
use crate::portfolio_applications::AssetHistData;
use crate::statistics::{ProbabilityDistribution, continuous_distributions::NormalDistribution};
#[test]
fn unit_test_black_scholes_formula() -> () {
use crate::financial_instruments::derivatives::{BlackScholesType, black_scholes_formula};
let value: f64 = black_scholes_formula(10.0, 11.0, 0.2, 1.0, 0.02, 0.0, &BlackScholesType::Call).unwrap();
assert!((value - 0.49438669572304805).abs() < TEST_ACCURACY);
}
#[test]
fn unit_test_contract() -> () {
let compounding_type: CompoundingType = CompoundingType::Continuous;
let financial_instrument_id: FinancialInstrumentId = FinancialInstrumentId {
instrument_type: FinancialInstrumentType::DerivativeInstrument, 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 contract: Contract = Contract::build(
ContractType::Future, 1.5, 100.0, 0.03, 2.0, 101.0, compounding_type, financial_instrument_id,
asset_historical_data, None
).unwrap();
let forward_price: f64 = 101.0 * (0.03 * 2.0_f64).exp();
let pv: f64 = (forward_price - 100.0) * (-0.03 * 2.0_f64).exp();
assert!((contract.present_value().unwrap() - pv).abs() < TEST_ACCURACY);
}
#[test]
fn unit_test_option() -> () {
let financial_instrument_id: FinancialInstrumentId = FinancialInstrumentId {
instrument_type: FinancialInstrumentType::DerivativeInstrument, 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 payoff: Box<LongCall> = Box::new(LongCall { k: 100.0, cost: 1.5 });
let option_pricing_method: OptionPricingMethod = OptionPricingMethod::BlackScholes { type_: BlackScholesType::Call };
let option: OptionContract = OptionContract::build(
99.0, 100.0, 0.02, 0.0, 3.0, 0.2, OptionType::European, payoff, 1.5, option_pricing_method,
financial_instrument_id, asset_historical_data, None
).unwrap();
let normal_dist: NormalDistribution = NormalDistribution::build(0.0, 1.0).unwrap();
let d_1_numerator: f64 = (99.0 / 100.0_f64).ln() + 3.0 * (0.02 - 0.0 + 0.5 * 0.2_f64.powi(2));
let d_1: f64 = d_1_numerator / (0.2 * 3.0_f64.sqrt());
let d_2: f64 = d_1 - 0.2 * 3.0_f64.sqrt();
let component_1: f64 = 99.0 * (-0.0 * 3.0_f64).exp() * normal_dist.cdf(d_1).unwrap();
let component_2: f64 = 100.0 * (-0.02 * 3.0_f64).exp() * normal_dist.cdf(d_2).unwrap();
let pv: f64 = component_1 - component_2;
assert!((option.present_value().unwrap() - pv).abs() < TEST_ACCURACY);
}
#[test]
fn unit_test_option_pricing_all() -> () {
use crate::lattice_models::LatticeModel;
use crate::lattice_models::trinomial_models::BrownianMotionTrinomialModel;
use crate::lattice_models::binomial_models::BrownianMotionBinomialModel;
use crate::financial_instruments::derivatives::{BlackScholesType, black_scholes_formula};
use crate::stochastic_processes::standard_stochastic_models::GeometricBrownianMotion;
use crate::monte_carlo::monte_carlo_simulation;
use crate::financial_instruments::LongCall;
let payoff: LongCall = LongCall { k: 105.0, cost: 0.0, };
let bmbm: BrownianMotionBinomialModel = BrownianMotionBinomialModel::build(
Box::new(payoff.clone()), 100.0, 1.0, 0.02, 0.15, 0.0, 1000
).unwrap();
let predicted_value: f64 = bmbm.european().unwrap();
println!("Binomial: {}", predicted_value);
let bmtm: BrownianMotionTrinomialModel = BrownianMotionTrinomialModel::build(
Box::new(payoff.clone()), 100.0, 1.0, 0.02, 0.15, 0.0, 1000
).unwrap();
let predicted_value: f64 = bmtm.european().unwrap();
println!("Trinomial: {}", predicted_value);
let predicted_value: f64 = black_scholes_formula(100.0, 105.0, 0.15, 1.0, 0.02, 0.0, &BlackScholesType::Call).unwrap();
println!("Black-Scholes: {}", predicted_value);
let gbm: GeometricBrownianMotion = GeometricBrownianMotion::new(0.02, 0.15, 1_000, 200, 1.0, 100.0);
let predicted_value: f64 = monte_carlo_simulation(&gbm, &payoff, 0.02, &Some(vec![false; 200])).unwrap();
println!("Monte-Carlo: {}", predicted_value);
}
#[cfg(feature = "plotly")]
#[test]
#[ignore]
fn unit_test_option_plot() -> () {
use plotly::Plot;
use crate::financial_instruments::derivatives::{plot_present_value_surface, PresentValueSurface};
let financial_instrument_id: FinancialInstrumentId = FinancialInstrumentId {
instrument_type: FinancialInstrumentType::DerivativeInstrument, 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 payoff: Box<LongCall> = Box::new(LongCall { k: 100.0, cost: 1.5 });
let option_pricing_method: OptionPricingMethod = OptionPricingMethod::Binomial { n_steps: 50 };
let mut option: OptionContract = OptionContract::build(
99.0, 100.0, 0.02, 0.0, 3.0, 0.2, OptionType::European, payoff, 1.5, option_pricing_method,
financial_instrument_id, asset_historical_data, None
).unwrap();
let surface: PresentValueSurface = option.present_value_surface(70.0, 130.0, 61, 30).unwrap();
let plot: Plot = plot_present_value_surface(&surface);
plot.show();
}
}