Skip to content

Commit b4a1c0e

Browse files
committed
feat(options): implement bermudan options (#53)
* Implement bermudan option * Implement bermudan option * Update src/options/types/bermudan_option.rs * Update BermudanOption * Implement bermudan option --------- Signed-off-by: Carlo Bortolan <[email protected]>
1 parent 3fd5ef0 commit b4a1c0e

File tree

6 files changed

+173
-6
lines changed

6 files changed

+173
-6
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Quantrs supports options pricing with various models for both vanilla and exotic
3838
| --------------------------- | --------------- | -------- | ------------ | ------------ | ------------- | ------ |
3939
| European |||||||
4040
| American |||| ❌ (L. Sq.) |||
41-
| Bermudan ||| | ❌ (L. Sq.) | ❌ (complex) ||
41+
| Bermudan ||| | ❌ (L. Sq.) | ❌ (complex) ||
4242
| ¹Basket | ⏳ (∀component) || ⏳ (approx.) ||||
4343
| ¹Rainbow | ✅ (∀component) ||||||
4444
| ²Barrier | ❌ (mod. BSM) ||||||

src/options/models/binomial_tree.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,13 @@ impl OptionPricing for BinomialTreeModel {
138138
let expected_value =
139139
discount_factor * (p * option_values[i + 1] + (1.0 - p) * option_values[i]);
140140

141-
if matches!(option.style(), OptionStyle::American) {
141+
if matches!(option.style(), OptionStyle::American)
142+
|| matches!(option.style(), OptionStyle::Bermudan)
143+
&& option
144+
.expiration_dates()
145+
.unwrap()
146+
.contains(&(step as f64 * dt))
147+
{
142148
let early_exercise = option.payoff(Some(
143149
option.instrument().spot() * u.powi(i as i32) * d.powi((step - i) as i32),
144150
));
@@ -149,7 +155,10 @@ impl OptionPricing for BinomialTreeModel {
149155
}
150156
}
151157

152-
if matches!(option.style(), OptionStyle::American) {
158+
if matches!(option.style(), OptionStyle::American)
159+
|| matches!(option.style(), OptionStyle::Bermudan)
160+
&& option.expiration_dates().unwrap().contains(&0.0)
161+
{
153162
option_values[0].max(option.strike() - option.instrument().spot()) // TODO: Change to max(0.0, self.payoff(Some(self.spot)))
154163
} else {
155164
option_values[0] // Return the root node value

src/options/traits/option.rs

+9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ pub trait Option: Clone + Send + Sync {
4141
/// The time horizon (in years).
4242
fn time_to_maturity(&self) -> f64;
4343

44+
/// Get the expiration dates of the option.
45+
///
46+
/// # Returns
47+
///
48+
/// The expiration dates of the option. (Only for Bermudan options)
49+
fn expiration_dates(&self) -> std::option::Option<&Vec<f64>> {
50+
None
51+
}
52+
4453
/// Set the time horizon (in years).
4554
///
4655
/// # Arguments

src/options/types.rs

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
77
pub use american_option::AmericanOption;
88
pub use asian_option::AsianOption;
9+
pub use bermudan_option::BermudanOption;
910
pub use binary_option::BinaryOption;
1011
pub use european_option::EuropeanOption;
1112
pub use lookback_option::LookbackOption;
1213
pub use rainbow_option::RainbowOption;
1314

1415
mod american_option;
1516
mod asian_option;
17+
mod bermudan_option;
1618
mod binary_option;
1719
mod european_option;
1820
mod lookback_option;

src/options/types/bermudan_option.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! Module for Bermudan option type.
2+
//!
3+
//! A Bermuda option can be exercised early, but only on a set of specific dates before its expiration.
4+
//! These exercise dates are often set in one-month increments.
5+
//! Premiums for Bermuda options are typically lower than those of American options, which can be exercised any time before expiry.
6+
7+
use std::any::Any;
8+
9+
use super::{OptionStyle, OptionType};
10+
use crate::{
11+
log_warn,
12+
options::{Instrument, Option},
13+
};
14+
15+
/// A struct representing an Bermudan option.
16+
#[derive(Clone, Debug)]
17+
pub struct BermudanOption {
18+
/// The underlying instrument.
19+
pub instrument: Instrument,
20+
/// Strike price of the option (aka exercise price).
21+
pub strike: f64,
22+
/// The time horizon (in years).
23+
pub time_to_maturity: f64,
24+
/// The expiration dates of the option (in years).
25+
pub expiration_dates: Vec<f64>,
26+
/// Type of the option (Call or Put).
27+
pub option_type: OptionType,
28+
}
29+
30+
impl BermudanOption {
31+
/// Create a new `BermudanOption`.
32+
pub fn new(
33+
instrument: Instrument,
34+
strike: f64,
35+
expiration_dates: Vec<f64>,
36+
option_type: OptionType,
37+
) -> Self {
38+
Self {
39+
instrument,
40+
strike,
41+
time_to_maturity: if let Some(&last_date) = expiration_dates.last() {
42+
last_date
43+
} else {
44+
log_warn!("Expiration dates are empty, setting time to maturity to 0.0");
45+
0.0
46+
},
47+
expiration_dates,
48+
option_type,
49+
}
50+
}
51+
}
52+
53+
impl Option for BermudanOption {
54+
fn instrument(&self) -> &Instrument {
55+
&self.instrument
56+
}
57+
58+
fn instrument_mut(&mut self) -> &mut Instrument {
59+
&mut self.instrument
60+
}
61+
62+
fn set_instrument(&mut self, instrument: Instrument) {
63+
self.instrument = instrument;
64+
}
65+
66+
fn strike(&self) -> f64 {
67+
self.strike
68+
}
69+
70+
fn time_to_maturity(&self) -> f64 {
71+
self.time_to_maturity
72+
}
73+
74+
fn expiration_dates(&self) -> std::option::Option<&Vec<f64>> {
75+
Some(&self.expiration_dates)
76+
}
77+
78+
fn set_time_to_maturity(&mut self, time_to_maturity: f64) {
79+
self.time_to_maturity = time_to_maturity;
80+
}
81+
82+
fn option_type(&self) -> OptionType {
83+
self.option_type
84+
}
85+
86+
fn style(&self) -> &OptionStyle {
87+
&OptionStyle::Bermudan
88+
}
89+
90+
fn flip(&self) -> Self {
91+
let flipped_option_type = match self.option_type {
92+
OptionType::Call => OptionType::Put,
93+
OptionType::Put => OptionType::Call,
94+
};
95+
BermudanOption::new(
96+
self.instrument.clone(),
97+
self.strike,
98+
self.expiration_dates.clone(),
99+
flipped_option_type,
100+
)
101+
}
102+
103+
fn as_any(&self) -> &dyn Any {
104+
self
105+
}
106+
}

tests/options_pricing.rs

+44-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use approx::assert_abs_diff_eq;
22
use quantrs::options::{
3-
AmericanOption, AsianOption, BinaryOption, BinomialTreeModel, Black76Model, BlackScholesModel,
4-
EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option, OptionGreeks,
5-
OptionPricing, OptionType, RainbowOption,
3+
AmericanOption, AsianOption, BermudanOption, BinaryOption, BinomialTreeModel, Black76Model,
4+
BlackScholesModel, EuropeanOption, Greeks, Instrument, LookbackOption, MonteCarloModel, Option,
5+
OptionGreeks, OptionPricing, OptionType, RainbowOption,
66
};
77

88
struct MockModel {}
@@ -26,6 +26,7 @@ fn assert_implements_option_trait<T: Option>(option: &T) {
2626
option.clone().set_time_to_maturity(1.0);
2727
option.strike();
2828
option.time_to_maturity();
29+
option.expiration_dates();
2930
option.option_type();
3031
option.style();
3132
option.flip();
@@ -786,6 +787,32 @@ mod binomial_tree_tests {
786787
}
787788
}
788789

790+
mod bermudan_option_tests {
791+
use super::*;
792+
793+
#[test]
794+
fn test_itm() {
795+
let instrument = Instrument::new().with_spot(52.0);
796+
let expiration_dates = vec![0.0, 1.0, 2.0];
797+
let option = BermudanOption::new(instrument, 50.0, expiration_dates, OptionType::Call);
798+
let model = BinomialTreeModel::new(0.05, 0.182321557, 2);
799+
800+
assert_abs_diff_eq!(model.price(&option), 8.8258, epsilon = 0.0001);
801+
assert_abs_diff_eq!(model.price(&option.flip()), 2.5722, epsilon = 0.0001);
802+
}
803+
804+
#[test]
805+
fn test_otm() {
806+
let instrument = Instrument::new().with_spot(50.0);
807+
let expiration_dates = vec![0.0, 1.0, 2.0];
808+
let option = BermudanOption::new(instrument, 60.0, expiration_dates, OptionType::Call);
809+
let model = BinomialTreeModel::new(0.05, 0.182321557, 2);
810+
811+
assert_abs_diff_eq!(model.price(&option), 10.0000, epsilon = 0.0001);
812+
assert_abs_diff_eq!(model.price(&option.flip()), 10.0000, epsilon = 0.0001);
813+
}
814+
}
815+
789816
mod rainbow_option_tests {
790817
use super::*;
791818

@@ -1908,6 +1935,20 @@ mod option_trait_tests {
19081935
OptionType::Put,
19091936
);
19101937
assert_implements_option_trait(&opt);
1938+
let opt = BermudanOption::new(
1939+
Instrument::new().with_spot(100.0),
1940+
100.0,
1941+
vec![1.0],
1942+
OptionType::Call,
1943+
);
1944+
assert_implements_option_trait(&opt);
1945+
let opt = BermudanOption::new(
1946+
Instrument::new().with_spot(100.0),
1947+
100.0,
1948+
vec![1.0],
1949+
OptionType::Put,
1950+
);
1951+
assert_implements_option_trait(&opt);
19111952
let opt = AsianOption::fixed(
19121953
Instrument::new().with_spot(100.0),
19131954
100.0,

0 commit comments

Comments
 (0)