Skip to content

feat(options): implement barrier options #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Quantrs supports options pricing with various models for both vanilla and exotic
| Bermudan | ❌ | ❌ | ✅ | ❌ (L. Sq.) | ❌ (complex) | ❌ |
| ¹Basket | ⏳ (∀component) | ❌ | ⏳ (approx.) | ⏳ | ❌ | ❌ |
| ¹Rainbow | ✅ (∀component) | ❌ | ✅ | ✅ | ❌ | ❌ |
| ²Barrier | ❌ (mod. BSM) | ❌ | | ⏳ | ⏳ | ⏳ |
| ²Barrier | ❌ (mod. BSM) | ❌ | ✅ (flaky) | ✅ | ⏳ | ⏳ |
| ²Double Barrier | ❌ (mod. BSM) | ❌ | ⏳ | ⏳ | ❌ (complex) | ⏳ |
| ²Asian (fixed strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ |
| ²Asian (floating strike) | ❌ (mod. BSM) | ❌ | ❌ | ✅ | ⏳ | ⏳ |
Expand Down
78 changes: 77 additions & 1 deletion src/options/models/binomial_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
//! println!("Option price: {}", price);
//! ```

use crate::options::{Option, OptionPricing, OptionStrategy, OptionStyle};
use crate::options::{BarrierOption, Option, OptionPricing, OptionStrategy, OptionStyle};

/// Binomial tree option pricing model.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -104,10 +104,82 @@ impl BinomialTreeModel {
steps,
}
}

fn price_barrier<T: Option>(&self, option: &T) -> f64 {
let dt = option.time_to_maturity() / self.steps as f64;
let u = (self.volatility * dt.sqrt()).exp();
let d = 1.0 / u;
let p = (((self.risk_free_rate - option.instrument().continuous_dividend_yield) * dt)
.exp()
- d)
/ (u - d);
let discount_factor = (-self.risk_free_rate * dt).exp();

let barrier_option = option.as_any().downcast_ref::<BarrierOption>();
let is_barrier = barrier_option.is_some();

// Initialize option values at maturity
let mut option_values: Vec<(f64, bool)> = (0..=self.steps)
.map(|i| {
let mut path = vec![];
let spot =
option.instrument().spot() * u.powi(i as i32) * d.powi((self.steps - i) as i32);
path.push(spot);

let mut value = option.payoff(Some(spot));
let mut activated = false;

if let Some(barrier_opt) = &barrier_option {
if barrier_opt.is_knocked_out(spot) {
value = 0.0; // Knocked-out options are worthless
} else if barrier_opt.is_activated(&path) {
activated = true; // Track knock-in activation
} else if barrier_opt.is_in() && !activated {
value = 0.0; // Knock-in options are worthless unless activated
}
}

(value, activated)
})
.collect();

// Backward induction
for step in (0..self.steps).rev() {
for i in 0..=step {
let mut path = vec![];
let spot =
option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32);
path.push(spot);

let expected_value =
discount_factor * (p * option_values[i + 1].0 + (1.0 - p) * option_values[i].0);

if let Some(barrier_opt) = &barrier_option {
if barrier_opt.is_knocked_out(spot) {
option_values[i] = (0.0, false); // Knocked-out options are worthless
} else if barrier_opt.is_activated(&path) {
option_values[i] = (expected_value, true);
} else if barrier_opt.is_in() && !option_values[i].1 {
option_values[i] = (0.0, false); // Knock-in options are worthless unless activated
} else {
option_values[i] = (expected_value, option_values[i].1);
}
} else {
option_values[i] = (expected_value, false);
}
}
}

option_values[0].0
}
}

impl OptionPricing for BinomialTreeModel {
fn price<T: Option>(&self, option: &T) -> f64 {
if matches!(option.style(), OptionStyle::Barrier(_)) {
return self.price_barrier(option);
}

// Multiplicative up-/downward movements of an asset in a single step of the binomial tree
let dt = option.time_to_maturity() / self.steps as f64;
let u = (self.volatility * dt.sqrt()).exp();
Expand Down Expand Up @@ -135,9 +207,13 @@ impl OptionPricing for BinomialTreeModel {
// Backward induction
for step in (0..self.steps).rev() {
for i in 0..=step {
let spot =
option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32);

let expected_value =
discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]);

// Check if the option can be exercised early
if matches!(option.style(), OptionStyle::American)
|| matches!(option.style(), OptionStyle::Bermudan)
&& option
Expand Down
54 changes: 50 additions & 4 deletions src/options/models/monte_carlo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl OptionPricing for MonteCarloModel {
OptionStyle::European => self.simulate_price_paths(option),
OptionStyle::Basket => self.simulate_price_paths(option),
OptionStyle::Rainbow(_) => self.simulate_price_paths(option),
OptionStyle::Barrier(_) => self.simulate_price_paths(option),
OptionStyle::Barrier(_) => self.price_barrier(option),
OptionStyle::DoubleBarrier(_, _) => self.simulate_price_paths(option),
OptionStyle::Asian(_) => self.price_asian(option),
OptionStyle::Lookback(_) => self.price_asian(option),
Expand Down Expand Up @@ -277,15 +277,61 @@ impl MonteCarloModel {
};

// Now call payoff after the mutable borrow is done
sum += (-(self.risk_free_rate - option_clone.instrument().continuous_dividend_yield)
* option_clone.time_to_maturity())
.exp()
sum += (-self.risk_free_rate * option_clone.time_to_maturity()).exp()
* option_clone.payoff(Some(avg_price));
}

// Return average payoff
sum / self.simulations as f64
}

/// Simulate price paths and compute the expected discounted payoff for Barrier options.
///
/// # Arguments
///
/// * `option` - The Barrier option to price.
///
/// # Returns
///
/// The expected discounted payoff of the option.
fn price_barrier<T: Option>(&self, option: &T) -> f64 {
let mut rng: ThreadRng = rand::rng();
let mut sum = 0.0;
let mut option_clone = option.clone();

for _ in 0..self.simulations {
// Get a mutable reference to the instrument
let avg_price = {
let instrument = option_clone.instrument_mut();
match self.method {
AvgMethod::Geometric => instrument.simulate_geometric_average_mut(
&mut rng,
SimMethod::Log,
self.volatility,
option.time_to_maturity(),
self.risk_free_rate,
self.steps,
),
AvgMethod::Arithmetic => instrument.simulate_arithmetic_average_mut(
&mut rng,
SimMethod::Log,
self.volatility,
option.time_to_maturity(),
self.risk_free_rate,
self.steps,
),
_ => panic!("Invalid averaging method"),
}
};

// Now call payoff after the mutable borrow is done
sum += (-self.risk_free_rate * option_clone.time_to_maturity()).exp()
* option_clone.payoff(None);
}

// Return average payoff
sum / self.simulations as f64
}
}

impl OptionStrategy for MonteCarloModel {}
2 changes: 1 addition & 1 deletion src/options/traits/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub trait Option: Clone + Send + Sync {
/// # Returns
///
/// The style of the option.
fn style(&self) -> &OptionStyle;
fn style(&self) -> OptionStyle;

/// Flip the option type (Call to Put or Put to Call).
///
Expand Down
2 changes: 2 additions & 0 deletions src/options/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

pub use american_option::AmericanOption;
pub use asian_option::AsianOption;
pub use barrier_option::BarrierOption;
pub use bermudan_option::BermudanOption;
pub use binary_option::BinaryOption;
pub use european_option::EuropeanOption;
Expand All @@ -14,6 +15,7 @@ pub use rainbow_option::RainbowOption;

mod american_option;
mod asian_option;
mod barrier_option;
mod bermudan_option;
mod binary_option;
mod european_option;
Expand Down
6 changes: 3 additions & 3 deletions src/options/types/american_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ pub struct AmericanOption {
pub instrument: Instrument,
/// Strike price of the option (aka exercise price).
pub strike: f64,
/// Type of the option (Call or Put).
/// The time horizon (in years).
pub time_to_maturity: f64,
/// Type of the option (Call or Put).
pub option_type: OptionType,
}

Expand Down Expand Up @@ -86,8 +86,8 @@ impl Option for AmericanOption {
self.option_type
}

fn style(&self) -> &OptionStyle {
&OptionStyle::American
fn style(&self) -> OptionStyle {
OptionStyle::American
}

fn flip(&self) -> Self {
Expand Down
19 changes: 8 additions & 11 deletions src/options/types/asian_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ pub struct AsianOption {
pub time_to_maturity: f64,
/// Type of the option (Call or Put).
pub option_type: OptionType,
/// The style of the option (Asian).
pub option_style: OptionStyle,
/// The type of the Asian option (Fixed or Floating).
pub asian_type: Permutation,
/// Style of the option (Asian with specific type).
pub permutation: Permutation,
}

impl AsianOption {
Expand All @@ -48,15 +46,14 @@ impl AsianOption {
strike: f64,
time_to_maturity: f64,
option_type: OptionType,
asian_type: Permutation,
permutation: Permutation,
) -> Self {
Self {
instrument,
strike,
time_to_maturity,
option_type,
option_style: OptionStyle::Asian(asian_type),
asian_type,
permutation,
}
}

Expand Down Expand Up @@ -121,13 +118,13 @@ impl Option for AsianOption {
self.option_type
}

fn style(&self) -> &OptionStyle {
&self.option_style
fn style(&self) -> OptionStyle {
OptionStyle::Asian(self.permutation)
}

fn payoff(&self, avg_price: std::option::Option<f64>) -> f64 {
let avg_price = avg_price.unwrap_or(self.instrument.spot());
match self.asian_type {
match self.permutation {
Permutation::Fixed => match self.option_type {
OptionType::Call => (avg_price - self.strike).max(0.0),
OptionType::Put => (self.strike - avg_price).max(0.0),
Expand All @@ -149,7 +146,7 @@ impl Option for AsianOption {
self.strike,
self.time_to_maturity,
flipped_option_type,
self.asian_type,
self.permutation,
)
}

Expand Down
Loading
Loading