Skip to content
Merged
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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Greeks { delta: 0.013645840354947947, gamma: -0.0008813766475726433, theta: 0.17

Quantrs also supports plotting option prices and strategies using the `plotters` backend.

E.g., Plot the P/L of a slightly skewed Condor spread:
E.g., Plot the P/L of a slightly skewed Condor spread using the Monte-Carlo model:

<details>
<summary><i>Click to see example code</i></summary>
Expand All @@ -148,24 +148,26 @@ let options = vec![
EuropeanOption::new(instrument.clone(), 115.0, 1.0, Call),
];

// Create a new Black-Scholes model with:
// Create a new Monte-Carlo model with:
// - Risk-free interest rate (r) = 5%
// - Volatility (σ) = 20%
let model = BlackScholesModel::new(0.05, 0.2);
// - Number of simulations = 10,000
// - Number of time steps = 365
let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 365);

// Plot a breakdown of the Condor spread with a spot price range of [80,120]
model.plot_strategy_breakdown(
"Condor Example",
model.condor(&options[0], &options[1], &options[2], &options[3]),
80.0..120.0,
"path/to/destination.png",
"examples/images/strategy.png",
&options,
);
```

</details>

![condor_strategy](./examples/images/condor.png)
![condor_strategy](./examples/images/strategy.png)

<!--<div align="center">
<img src="https://github.com/user-attachments/assets/0298807f-43ed-4458-9c7d-43b0f70defea" alt="condor_strategy" width="600"/>
Expand All @@ -176,7 +178,7 @@ See the [documentation][docs-url] for more information and examples.
## Benchmarks

Compared to other popular and well-maintained (i.e., actively developed, well-documented, and feature-rich) options pricing libraries, quantrs competes well in terms of performance:
E.g., for building and pricing a European call with the Merton Black-Scholes model, quantrs is:
E.g., for pricing a European call with the Merton Black-Scholes model, quantrs is:

- **87x faster** than `py_vollib`
- **29x faster** than `QuantLib` (python bindings)
Expand Down
Binary file removed examples/images/condor.png
Binary file not shown.
Binary file added examples/images/covered_call.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/images/diagonal_spread_strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/images/iron_butterfly_strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/images/strategy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 18 additions & 24 deletions examples/options_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ fn example_rainbow() {
}

fn example_strategy() {
let model = BlackScholesModel::new(0.0025, 0.15);
// let model = BlackScholesModel::new(0.0025, 0.15);
// let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 365);
let model = BinomialTreeModel::new(0.05, 0.2, 100);
let instrument = Instrument::new().with_spot(50.0);

////////////////////
Expand Down Expand Up @@ -587,32 +589,28 @@ fn example_strategy() {
); // => Calendar Spread: examples/images/calendar_spread_strategy.png

let options = vec![
EuropeanOption::new(instrument.clone(), 60.0, 1.0 / 12.0, Call),
EuropeanOption::new(instrument.clone(), 75.0, 2.0 / 12.0, Call),
EuropeanOption::new(instrument.clone(), 60.0, 1.0 / 12.0, Call),
EuropeanOption::new(Instrument::new().with_spot(48.0), 49.0, 1.0 / 12.0, Call),
EuropeanOption::new(Instrument::new().with_spot(48.0), 49.0, 1.0 / 12.0, Call),
EuropeanOption::new(Instrument::new().with_spot(48.0), 50.0, 2.0 / 12.0, Call),
];
let _ = model.plot_strategy_breakdown(
"Diagonal Spread",
model.diagonal_spread(&options[0], &options[1], &options[2]),
20.0..80.0,
40.0..60.0,
"examples/images/diagonal_spread_strategy.png",
&options,
); // => Diagonal Spread: examples/images/diagonal_spread_strategy.png
); //model.diagonal_spread(&options[0], &options[1], &options[2])(50.0);
}

fn example_plots() {
// Create a new plotter and plot the option prices
// let plotter = Plotter::new();
// let _ = plotter.plot_option_prices(
// "Binary Call Option",
// instrument,
// 80.0..120.0,
// 0.1..1.0,
// 0.1,
// Call,
// |k, t| BinaryOption::cash_or_nothing(instrument.clone(), k, t, Call),
// "path/to/destination.png",
// );
// Create a new Monte-Carlo model with:
// - Risk-free interest rate (r) = 5%
// - Volatility (σ) = 20%
// - Number of simulations = 10,000
// - Number of time steps = 365
let model = MonteCarloModel::geometric(0.05, 0.2, 10_000, 365);

// Create a new instrument with a spot price of 100 and a dividend yield of 2%
let instrument = Instrument::new().with_spot(100.0).with_cont_yield(0.02);

// Create a vector of European call options with different strike prices
Expand All @@ -623,16 +621,12 @@ fn example_plots() {
EuropeanOption::new(instrument.clone(), 115.0, 1.0, Call),
];

// Create a new Black-Scholes model with:
// - Risk-free interest rate (r) = 5%
// - Volatility (σ) = 20%
let model = BlackScholesModel::new(0.05, 0.2);

// Plot a breakdown of the Condor spread with a spot price range of [80,120]
let _ = model.plot_strategy_breakdown(
"Condor Example",
model.condor(&options[0], &options[1], &options[2], &options[3]),
80.0..120.0,
"examples/images/condor.png",
"examples/images/strategy.png",
&options,
);
}
4 changes: 3 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, OptionStyle};
use crate::options::{Option, OptionPricing, OptionStrategy, OptionStyle};

/// Binomial tree option pricing model.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -150,3 +150,5 @@ impl OptionPricing for BinomialTreeModel {
panic!("BinomialTreeModel does not support implied volatility calculation yet");
}
}

impl OptionStrategy for BinomialTreeModel {}
4 changes: 3 additions & 1 deletion src/options/models/black_76.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Module for Black76 option pricing model.

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

/// Black76 option pricing model.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -33,3 +33,5 @@ impl OptionPricing for Black76Model {
panic!("Black76Model does not support implied volatility calculation yet");
}
}

impl OptionStrategy for Black76Model {}
4 changes: 3 additions & 1 deletion src/options/models/finite_diff.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Module for finite difference option pricing model.

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

/// Finite difference option pricing model.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -33,3 +33,5 @@ impl OptionPricing for FiniteDiffModel {
panic!("FiniteDiffModel does not support implied volatility calculation yet");
}
}

impl OptionStrategy for FiniteDiffModel {}
4 changes: 3 additions & 1 deletion src/options/models/heston.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Module for Heston option pricing model.

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

/// Heston option pricing model.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -33,3 +33,5 @@ impl OptionPricing for HestonModel {
panic!("HestonModel does not support implied volatility calculation yet");
}
}

impl OptionStrategy for HestonModel {}
4 changes: 3 additions & 1 deletion src/options/models/monte_carlo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//! println!("Monte Carlo Call Price: {}", price);
//! ```

use crate::options::{Option, OptionPricing, OptionStyle, SimMethod};
use crate::options::{Option, OptionPricing, OptionStrategy, OptionStyle, SimMethod};
use rand::rngs::ThreadRng;
use rayon::prelude::*;

Expand Down Expand Up @@ -202,3 +202,5 @@ impl MonteCarloModel {
self.simulate_price_paths(option)
}
}

impl OptionStrategy for MonteCarloModel {}
7 changes: 7 additions & 0 deletions src/options/traits/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ pub trait Option: Clone + Send + Sync {
/// The time horizon (in years).
fn time_to_maturity(&self) -> f64;

/// Set the time horizon (in years).
///
/// # Arguments
///
/// * `time_to_maturity` - The time horizon (in years).
fn set_time_to_maturity(&mut self, time_to_maturity: f64);

/// Get the type of the option.
///
/// # Returns
Expand Down
64 changes: 33 additions & 31 deletions src/options/traits/option_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@

/* STOCK & OPTION */

/// Buy (long covered call) or sell (short covered call) a pair of ITM (in the money) stock and sell a OTM (out of the money) call.
/// Own underlying stock and sell a OTM (out of the money) call.
fn covered_call<'a, T: Option>(
&'a self,
stock: &'a Instrument,
Expand All @@ -427,8 +427,8 @@
"Stock must be ITM and call must be OTM!"
);

let price = stock.spot + self.price(call);
let payoff = spot_price + call.payoff(Some(spot_price));
let price = stock.spot - self.price(call);
let payoff = spot_price - call.payoff(Some(spot_price));
(payoff, price)
}
}
Expand Down Expand Up @@ -557,7 +557,7 @@
}
}

/// The iron butterfly strategy involves buying a pair of ATM (at the money) call and put, and shorting a pair of OTM (out of the money) call and put.
/// The iron butterfly strategy involves buying a pair of OTM (out of the money) call and put, and shorting a pair of ATM (at the money) call and put.
/// It is a limited-risk, limited-profit trading strategy structured for a larger probability of earning smaller limited profit when the underlying stock is perceived to have a low volatility.
fn iron_butterfly<'a, T: Option>(
&'a self,
Expand All @@ -578,12 +578,12 @@
assert!(otm_put.strike() < atm_put.strike() && atm_put.strike() == atm_call.strike() && atm_call.strike() < otm_call.strike(),
"Iron Butterfly must have ordered strikes: lower_put < atm_put == atm_call < upper_call");

let price = -self.price(otm_put) + self.price(atm_put) + self.price(atm_call)
- self.price(otm_call);
let payoff = -otm_put.payoff(Some(spot_price))
+ atm_put.payoff(Some(spot_price))
+ atm_call.payoff(Some(spot_price))
- otm_call.payoff(Some(spot_price));
let price = self.price(otm_put) - self.price(atm_put) - self.price(atm_call)
+ self.price(otm_call);
let payoff = otm_put.payoff(Some(spot_price))
- atm_put.payoff(Some(spot_price))
- atm_call.payoff(Some(spot_price))
+ otm_call.payoff(Some(spot_price));
(payoff, price)
}
}
Expand Down Expand Up @@ -778,6 +778,7 @@
front_month: &'a T,
back_month: &'a T,
) -> impl Fn(f64) -> (f64, f64) + 'a {
log_warn!("Flaky implementation of calendar spread. Use with caution!");
if back_month.time_to_maturity() < front_month.time_to_maturity() {
log_warn!("Back month is the front month => continuing with the inverse order!");
return self.calendar_spread(back_month, front_month);
Expand All @@ -791,10 +792,6 @@
log_warn!("Options are not ATM. Consider choosing ATM options!");
}

if front_month.time_to_maturity() > 0.083333334 {
log_warn!("Front month expires in more than 1 month. Consider choosing a shorter expiration date!");
}

if back_month.time_to_maturity() - front_month.time_to_maturity() > 0.083333334 {
log_warn!("Time to maturity delta is more than 1 month. Consider choosing a shorter expiration date!");
}
Expand All @@ -814,11 +811,6 @@
back_month_long: &'a T,
) -> impl Fn(f64) -> (f64, f64) + 'a {
move |spot_price| {
if back_month_long.time_to_maturity() < front_month.time_to_maturity() {
log_warn!("Back month is the front month => continuing with the inverse order!");
return self.calendar_spread(back_month_long, front_month)(spot_price);
}

if front_month.strike() != back_month_short.strike() {
log_warn!("Front month short and back month long strikes are not equal. Consider choosing equal strikes!");
}
Expand All @@ -831,24 +823,25 @@
log_warn!("Front month expires in more than 1 month. Consider choosing a shorter expiration date!");
}

// Check if back-month long is further OTM than back-month short.
if back_month_long.is_call() && back_month_long.strike() < back_month_short.strike()
|| back_month_long.is_put() && back_month_long.strike() > back_month_short.strike()
{
log_warn!("Back-month long is not further OTM than back-month short. Consider choosing further OTM options!");
}

if (front_month.time_to_maturity() - back_month_short.time_to_maturity()).abs() > 0.0027
{
log_warn!("Time to maturity delta between front-month and back-month short is more than 1 day. Consider choosing a shorter expiration date!");
}

if back_month_long.time_to_maturity() - front_month.time_to_maturity() > 0.086073059 {
log_warn!("Time to maturity delta between front-month and back-month long is more than 1 month. Consider choosing a shorter expiration option!");
// Ensure back-month long expires ~1 month after the front-month
let time_delta = back_month_long.time_to_maturity() - front_month.time_to_maturity();
if time_delta > 0.086073059 {
log_warn!("Back-month long expires more than 1 month after front-month. Consider a shorter expiration!");

Check warning on line 834 in src/options/traits/option_strategy.rs

View check run for this annotation

Codecov / codecov/patch

src/options/traits/option_strategy.rs#L834

Added line #L834 was not covered by tests
}
if time_delta < 0.080593607 {
log_warn!("Back-month long expires less than 1 month after front-month. Consider a longer expiration!");

Check warning on line 837 in src/options/traits/option_strategy.rs

View check run for this annotation

Codecov / codecov/patch

src/options/traits/option_strategy.rs#L837

Added line #L837 was not covered by tests
}

if back_month_long.time_to_maturity() - front_month.time_to_maturity() < 0.080593607 {
log_warn!("Time to maturity delta between front-month and back-month long is less than 1 month. Consider choosing a longer expiration option!");
// Check if back-month long is further OTM than back-month short.
if back_month_long.is_call() && back_month_long.strike() < back_month_short.strike()
|| back_month_long.is_put() && back_month_long.strike() > back_month_short.strike()
{
log_warn!("Back-month long is not further OTM than back-month short. Consider choosing further OTM options!");

Check warning on line 844 in src/options/traits/option_strategy.rs

View check run for this annotation

Codecov / codecov/patch

src/options/traits/option_strategy.rs#L843-L844

Added lines #L843 - L844 were not covered by tests
}

if front_month.is_call() {
Expand All @@ -859,12 +852,21 @@
check_is_put!(back_month_short);
}

// Adjust back-month short time to maturity after front-month expires
let mut back_month_short = back_month_short.clone();
back_month_short
.set_instrument(back_month_short.instrument().clone().with_spot(spot_price));

// Calculate the net price (cost/debit of entering the trade)
let price = self.price(back_month_long)
- self.price(front_month)
- self.price(back_month_short);
- self.price(&back_month_short);

// Compute the payoff at given spot price
let payoff = back_month_long.payoff(Some(spot_price))
- front_month.payoff(Some(spot_price))
- back_month_short.payoff(Some(spot_price));

(payoff, price)
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/options/types/american_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ impl Option for AmericanOption {
self.time_to_maturity
}

fn set_time_to_maturity(&mut self, time_to_maturity: f64) {
self.time_to_maturity = time_to_maturity;
}

fn option_type(&self) -> OptionType {
self.option_type
}
Expand Down
4 changes: 4 additions & 0 deletions src/options/types/asian_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ impl Option for AsianOption {
self.time_to_maturity
}

fn set_time_to_maturity(&mut self, time_to_maturity: f64) {
self.time_to_maturity = time_to_maturity;
}

fn option_type(&self) -> OptionType {
self.option_type
}
Expand Down
4 changes: 4 additions & 0 deletions src/options/types/binary_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ impl Option for BinaryOption {
self.time_to_maturity
}

fn set_time_to_maturity(&mut self, time_to_maturity: f64) {
self.time_to_maturity = time_to_maturity;
}

fn option_type(&self) -> OptionType {
self.option_type
}
Expand Down
4 changes: 4 additions & 0 deletions src/options/types/european_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ impl Option for EuropeanOption {
self.time_to_maturity
}

fn set_time_to_maturity(&mut self, time_to_maturity: f64) {
self.time_to_maturity = time_to_maturity;
}

fn option_type(&self) -> OptionType {
self.option_type
}
Expand Down
Loading