use core::f64;
use rand::rngs::ThreadRng;
use rand_distr::{Distribution, Normal};
#[derive(Debug, Default, Clone)]
pub struct Instrument {
pub spot: Vec<f64>,
pub continuous_dividend_yield: f64,
pub discrete_dividend_yield: f64,
pub dividend_times: Vec<f64>,
pub assets: Vec<(Instrument, f64)>,
pub sorted: bool,
}
impl Instrument {
pub fn new() -> Self {
Self {
spot: vec![0.0],
continuous_dividend_yield: 0.0,
discrete_dividend_yield: 0.0,
dividend_times: Vec::new(),
assets: Vec::new(),
sorted: false,
}
}
pub fn with_spot(mut self, spot: f64) -> Self {
self.spot = vec![spot];
self
}
pub fn with_spots(mut self, spot: Vec<f64>) -> Self {
self.spot = spot;
self
}
pub fn max_spot(&self) -> f64 {
*self.spot.iter().max_by(|x, y| x.total_cmp(y)).unwrap()
}
pub fn min_spot(&self) -> f64 {
*self.spot.iter().min_by(|x, y| x.total_cmp(y)).unwrap()
}
pub fn with_continuous_dividend_yield(mut self, yield_: f64) -> Self {
self.continuous_dividend_yield = yield_;
self.assets.iter_mut().for_each(|(a, _)| {
a.continuous_dividend_yield = yield_;
});
self
}
pub fn with_cont_yield(self, yield_: f64) -> Self {
self.with_continuous_dividend_yield(yield_)
}
pub fn with_discrete_dividend_yield(mut self, yield_: f64) -> Self {
self.discrete_dividend_yield = yield_;
self
}
pub fn with_dividend_times(mut self, times: Vec<f64>) -> Self {
self.dividend_times = times;
self
}
pub fn with_assets(mut self, assets: Vec<Instrument>) -> Self {
if assets.is_empty() {
return self;
}
let weight = 1.0 / assets.len() as f64;
self.assets = assets.iter().map(|asset| (asset.clone(), weight)).collect();
let new_spot = self.assets.iter().map(|(a, w)| a.spot() * w).sum::<f64>();
self.spot = vec![new_spot];
self.sort_assets_by_performance();
self
}
pub fn with_weighted_assets(mut self, assets: Vec<(Instrument, f64)>) -> Self {
if assets.is_empty() {
return self;
}
self.assets = assets;
self.sort_assets_by_performance();
self
}
pub fn sort_assets_by_performance(&mut self) {
self.assets
.sort_by(|a, b| b.0.spot.partial_cmp(&a.0.spot).unwrap());
self.spot = vec![(self.assets.iter().map(|(a, w)| a.spot() * w).sum::<f64>())];
self.sorted = true;
}
pub fn best_performer(&self) -> &Instrument {
if self.assets.is_empty() {
return self;
}
if !self.sorted {
panic!("Assets are not sorted");
}
&self.assets.first().unwrap().0
}
pub fn worst_performer(&self) -> &Instrument {
if self.assets.is_empty() {
return self;
}
if !self.sorted {
panic!("Assets are not sorted");
}
&self.assets.last().unwrap().0
}
pub fn best_performer_mut(&mut self) -> &mut Instrument {
if self.assets.is_empty() {
return self;
}
if !self.sorted {
panic!("Assets are not sorted");
}
&mut self.assets.first_mut().unwrap().0
}
pub fn worst_performer_mut(&mut self) -> &mut Instrument {
if self.assets.is_empty() {
return self;
}
if !self.sorted {
panic!("Assets are not sorted");
}
&mut self.assets.last_mut().unwrap().0
}
pub fn calculate_adjusted_spot(&self, ttm: f64) -> f64 {
let n_dividends = self.dividend_times.iter().filter(|&&t| t <= ttm).count() as f64;
self.spot() * (1.0 - self.discrete_dividend_yield).powf(n_dividends)
}
pub fn spot(&self) -> f64 {
*self.spot.first().unwrap()
}
pub fn terminal_spot(&self) -> f64 {
*self.spot.last().unwrap()
}
pub fn euler_simulation(
&self,
rng: &mut ThreadRng,
risk_free_rate: f64,
volatility: f64,
steps: usize,
) -> Vec<f64> {
let normal = Normal::new(0.0, 1.0).unwrap();
let dt: f64 = 1.0 / steps as f64; let mut prices = vec![self.spot(); steps];
for i in 1..steps {
let z = normal.sample(rng);
prices[i] = prices[i - 1]
* (1.0
+ (risk_free_rate - self.continuous_dividend_yield) * dt
+ volatility * z * dt.sqrt());
}
prices
}
pub fn log_simulation(
&self,
rng: &mut ThreadRng,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> Vec<f64> {
let dt = time_to_maturity / steps as f64; let normal: Normal<f64> = Normal::new(0.0, dt.sqrt()).unwrap(); let mut logs = vec![self.spot().ln(); steps];
for i in 1..steps {
let z = normal.sample(rng);
logs[i] = logs[i - 1]
+ (risk_free_rate - self.continuous_dividend_yield - 0.5 * volatility.powi(2)) * dt
+ volatility * z;
}
logs.iter().map(|log| log.exp()).collect()
}
pub fn simulate_arithmetic_average(
&self,
rng: &mut ThreadRng,
method: SimMethod,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> f64 {
let prices = match method {
SimMethod::Milstein => unimplemented!("Milstein method not implemented"),
SimMethod::Euler => self.euler_simulation(rng, risk_free_rate, volatility, steps),
SimMethod::Log => {
self.log_simulation(rng, volatility, time_to_maturity, risk_free_rate, steps)
}
};
prices.iter().sum::<f64>() / (prices.len()) as f64
}
pub fn simulate_geometric_average(
&self,
rng: &mut ThreadRng,
method: SimMethod,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> f64 {
let prices = match method {
SimMethod::Milstein => unimplemented!("Milstein method not implemented"),
SimMethod::Euler => self.euler_simulation(rng, risk_free_rate, volatility, steps),
SimMethod::Log => {
self.log_simulation(rng, volatility, time_to_maturity, risk_free_rate, steps)
}
};
(self.spot.iter().map(|price| price.ln()).sum::<f64>() / self.spot.len() as f64).exp()
}
pub fn simulate_arithmetic_average_mut(
&mut self,
rng: &mut ThreadRng,
method: SimMethod,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> f64 {
self.spot = match method {
SimMethod::Milstein => unimplemented!("Milstein method not implemented"),
SimMethod::Euler => self.euler_simulation(rng, risk_free_rate, volatility, steps),
SimMethod::Log => {
self.log_simulation(rng, volatility, time_to_maturity, risk_free_rate, steps)
}
};
self.spot.iter().sum::<f64>() / (self.spot.len()) as f64
}
pub fn simulate_geometric_average_mut(
&mut self,
rng: &mut ThreadRng,
method: SimMethod,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> f64 {
self.spot = match method {
SimMethod::Milstein => unimplemented!("Milstein method not implemented"),
SimMethod::Euler => self.euler_simulation(rng, risk_free_rate, volatility, steps),
SimMethod::Log => {
self.log_simulation(rng, volatility, time_to_maturity, risk_free_rate, steps)
}
};
(self.spot.iter().map(|price| price.ln()).sum::<f64>() / self.spot.len() as f64).exp()
}
pub fn simulate_geometric_brownian_motion(
&self,
rng: &mut ThreadRng,
volatility: f64,
time_to_maturity: f64,
risk_free_rate: f64,
steps: usize,
) -> f64 {
let normal = Normal::new(0.0, 1.0).unwrap();
let dt = time_to_maturity / steps as f64;
let mut price = self.spot();
for _ in 0..steps {
let z = normal.sample(rng);
price *= ((risk_free_rate - self.continuous_dividend_yield - 0.5 * volatility.powi(2))
* dt
+ volatility * z * dt.sqrt())
.exp();
}
price
}
}
pub enum SimMethod {
Milstein,
Euler,
Log,
}