diff --git a/Cargo.toml b/Cargo.toml index 0cff3150..8e22e3e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ thiserror = "1.0.57" # https://docs.rs/thiserror/latest/thiserror/ yahoo_finance_api = "2.1.0" # https://docs.rs/yahoo-finance-api/latest/yahoo_finance_api/ tokio-test = "0.4.3" # https://docs.rs/tokio-test/latest/tokio_test/ + # https://docs.rs/num/latest/num/ num = { version = "0.4.1", features = ["rand"] } @@ -84,6 +85,9 @@ time = { version = "0.3.34", features = ["macros"] } # https://docs.rs/polars/latest/polars/ polars = { version = "0.41.1", features = ["docs-selection"] } +# https://docs.rs/uuid/latest/uuid/ +uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } + [dev-dependencies] finitediff = "0.1.4" # https://docs.rs/finitediff/latest/finitediff/ diff --git a/book/book.toml b/book/book.toml index 1e5df9e8..a86b30b8 100644 --- a/book/book.toml +++ b/book/book.toml @@ -15,3 +15,4 @@ title = "The RustQuant Book" ## and you will be able to use the usual $...$ and $$...$$ delimiters. [output.html] mathjax-support = true +fold = { enable = true } diff --git a/book/src/Introduction.md b/book/src/Introduction.md index 445388bb..06039d23 100644 --- a/book/src/Introduction.md +++ b/book/src/Introduction.md @@ -1,8 +1,27 @@ -# Introduction to RustQuant +![](./assets/logo.png) RustQuant is a Rust library (crate) for quantitative finance. -You can download the library from [Crates.io](https://crates.io/crates/RustQuant), and API documentation can be found at [Docs.rs](https://docs.rs/crate/RustQuant/latest). +# Code -> Note: this book is a very early work-in-progress and has almost no content. +The crate is available for download from [Crates.io](https://crates.io/crates/RustQuant), and the source is available on [GitHub](https://github.com/avhz/RustQuant). + +# Installation + +RustQuant of course requires [Rust](https://www.rust-lang.org/) to be installed first. + +You can easily add RustQuant to your Rust project by running: + +```bash +cargo add RustQuant +``` +# Documentation + +API documentation can be found at [Docs.rs](https://docs.rs/crate/RustQuant/latest). + +The documentation contained in this book is more *"cookbook"* style. + +Contributions to documentation in any form are highly welcome. + +> Note: this book is a very early work-in-progress and has almost no content. diff --git a/book/src/Modules.md b/book/src/Modules.md deleted file mode 100644 index a55ecc05..00000000 --- a/book/src/Modules.md +++ /dev/null @@ -1 +0,0 @@ -# Modules diff --git a/book/src/Modules/autodiff.md b/book/src/Modules/autodiff/autodiff.md similarity index 100% rename from book/src/Modules/autodiff.md rename to book/src/Modules/autodiff/autodiff.md diff --git a/book/src/Modules/cashflows.md b/book/src/Modules/cashflows/cashflows.md similarity index 100% rename from book/src/Modules/cashflows.md rename to book/src/Modules/cashflows/cashflows.md diff --git a/book/src/Modules/data.md b/book/src/Modules/data.md deleted file mode 100644 index aa94f50f..00000000 --- a/book/src/Modules/data.md +++ /dev/null @@ -1 +0,0 @@ -# `data` \ No newline at end of file diff --git a/book/src/Modules/data/curves.md b/book/src/Modules/data/curves.md new file mode 100644 index 00000000..22c0856a --- /dev/null +++ b/book/src/Modules/data/curves.md @@ -0,0 +1,39 @@ +# Curves + +Curves can be fit to market data. Here we include an example of a spot curve being fitted. + +![`Spot curve`](../../assets/spotcurve.png) + +```rust +{{#include ../../../../examples/curves_spot.rs}} +``` + + + + + + + + + + + +
Good Bad
+ +```rust +int foo() { + int result = 4; + return result; +} +``` + + + +```rust +int foo() { + int x = 4; + return x; +} +``` + +
\ No newline at end of file diff --git a/book/src/Modules/data/data.md b/book/src/Modules/data/data.md new file mode 100644 index 00000000..740ffd94 --- /dev/null +++ b/book/src/Modules/data/data.md @@ -0,0 +1,8 @@ +# `data` + +The `data` module encompasses everything data related. + +That is, anything that can be observed, either in markets or derived from market observable data, and also facilities to manage that data. + +Another form of data is contextual (or reference) data. These are things such as calendars and date conventions. While there are facilities to handle these data inside the `data` module, the underlying implementations are in other modules, such as the `time` module. + diff --git a/book/src/Modules/error.md b/book/src/Modules/error.md index 68d3b6bd..df79877d 100644 --- a/book/src/Modules/error.md +++ b/book/src/Modules/error.md @@ -1 +1 @@ -# `error` \ No newline at end of file +# error diff --git a/book/src/Modules/instruments.md b/book/src/Modules/instruments/instruments.md similarity index 100% rename from book/src/Modules/instruments.md rename to book/src/Modules/instruments/instruments.md diff --git a/book/src/Modules/iso.md b/book/src/Modules/iso/iso.md similarity index 100% rename from book/src/Modules/iso.md rename to book/src/Modules/iso/iso.md diff --git a/book/src/Modules/macros.md b/book/src/Modules/macros.md index 416061d3..24dabf6d 100644 --- a/book/src/Modules/macros.md +++ b/book/src/Modules/macros.md @@ -1 +1 @@ -# `macros` \ No newline at end of file +# macros diff --git a/book/src/Modules/math.md b/book/src/Modules/math/math.md similarity index 100% rename from book/src/Modules/math.md rename to book/src/Modules/math/math.md diff --git a/book/src/Modules/ml.md b/book/src/Modules/ml/ml.md similarity index 100% rename from book/src/Modules/ml.md rename to book/src/Modules/ml/ml.md diff --git a/book/src/Modules/models.md b/book/src/Modules/models/models.md similarity index 100% rename from book/src/Modules/models.md rename to book/src/Modules/models/models.md diff --git a/book/src/Modules/portfolio.md b/book/src/Modules/portfolio/portfolio.md similarity index 100% rename from book/src/Modules/portfolio.md rename to book/src/Modules/portfolio/portfolio.md diff --git a/book/src/Modules/stochastics.md b/book/src/Modules/stochastics/stochastics.md similarity index 100% rename from book/src/Modules/stochastics.md rename to book/src/Modules/stochastics/stochastics.md diff --git a/book/src/Modules/time.md b/book/src/Modules/time/time.md similarity index 100% rename from book/src/Modules/time.md rename to book/src/Modules/time/time.md diff --git a/book/src/Modules/trading.md b/book/src/Modules/trading/trading.md similarity index 100% rename from book/src/Modules/trading.md rename to book/src/Modules/trading/trading.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 889b6875..dc0765da 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,21 +1,35 @@ # Summary -- [Introduction to RustQuant](./Introduction.md) -- [Modules](./Modules.md) - - [`autodiff`](./Modules/autodiff.md) - - [`cashflows`](./Modules/cashflows.md) - - [`data`](./Modules/data.md) - - [`error`](./Modules/error.md) - - [`instruments`](./Modules/instruments.md) - - [`iso`](./Modules/iso.md) - - [`macros`](./Modules/macros.md) - - [`math`](./Modules/math.md) - - [`ml`](./Modules/ml.md) - - [`models`](./Modules/models.md) - - [`portfolio`](./Modules/portfolio.md) - - [`stochastics`](./Modules/stochastics.md) - - [`time`](./Modules/time.md) - - [`trading`](./Modules/trading.md) +# Introduction + +- [RustQuant](./Introduction.md) + +--- + +# Modules + + + +- [`data`](./Modules/data/data.md) + - [Curves](./Modules/data/curves.md) + + + +--- + +# Development + - [Contributing](./Contributing.md) - [File Template](./Template.md) - [References](./References.md) diff --git a/book/src/assets/gbm.png b/book/src/assets/gbm.png new file mode 100644 index 00000000..40f43c79 Binary files /dev/null and b/book/src/assets/gbm.png differ diff --git a/book/src/assets/logo.png b/book/src/assets/logo.png new file mode 100644 index 00000000..8a5d7099 Binary files /dev/null and b/book/src/assets/logo.png differ diff --git a/book/src/assets/logo_banner.png b/book/src/assets/logo_banner.png new file mode 100644 index 00000000..a469a55c Binary files /dev/null and b/book/src/assets/logo_banner.png differ diff --git a/book/src/assets/logo_square.png b/book/src/assets/logo_square.png new file mode 100644 index 00000000..7dcec701 Binary files /dev/null and b/book/src/assets/logo_square.png differ diff --git a/book/src/assets/spotcurve.png b/book/src/assets/spotcurve.png new file mode 100644 index 00000000..b4da4613 Binary files /dev/null and b/book/src/assets/spotcurve.png differ diff --git a/examples/curve.rs b/examples/curve.rs index d6749306..0bfc82d8 100644 --- a/examples/curve.rs +++ b/examples/curve.rs @@ -1,4 +1,3 @@ -use plotly::{Plot, Scatter}; use time::macros::date; use time::{Date, Duration}; use RustQuant::data::Curve; diff --git a/examples/curves_discount.rs b/examples/curves_discount.rs index e3d98638..c0a1d10c 100644 --- a/examples/curves_discount.rs +++ b/examples/curves_discount.rs @@ -1,10 +1,9 @@ -use plotly::{Plot, Scatter}; use polars::prelude::*; use time::macros::date; -use time::{Date, Duration}; -use RustQuant::data::{discount_curve, Curve, DiscountCurve}; +use time::Date; +use RustQuant::data::Curves; +use RustQuant::data::{Curve, DiscountCurve}; use RustQuant::time::oceania::australia::AustraliaCalendar; -use RustQuant::time::Calendar; fn main() { let cal = AustraliaCalendar; diff --git a/examples/curves_spot.rs b/examples/curves_spot.rs index f2486e5a..1f6a6e2e 100644 --- a/examples/curves_spot.rs +++ b/examples/curves_spot.rs @@ -1,46 +1,13 @@ -use plotly::{Plot, Scatter}; -// use polars::prelude::*; -use time::macros::date; -use time::{Date, Duration}; -use RustQuant::data::CurveModel; -use RustQuant::data::Curves; -use RustQuant::data::{Curve, DiscountCurve, SpotCurve}; -use RustQuant::models::NelsonSiegelSvensson; +use time::{macros::date, Date}; +use RustQuant::data::{Curves, SpotCurve}; use RustQuant::time::oceania::australia::AustraliaCalendar; -// use RustQuant::time::Calendar; fn main() { - // let cal = AustraliaCalendar; - // let curve = Curve::::new_from_slice(&DATES, &RATES); + let mut spot_curve = SpotCurve::::new(&DATES, &RATES); - let mut discount_curve = SpotCurve::::new(&DATES, &RATES); + spot_curve.get_rates(&NEW_DATES); - let new_dates = [ - date!(2025 - 01 - 01), - date!(2026 - 01 - 01), - date!(2027 - 01 - 01), - date!(2028 - 01 - 01), - date!(2029 - 01 - 01), - date!(2030 - 01 - 01), - date!(2033 - 01 - 01), - date!(2036 - 01 - 01), - date!(2040 - 01 - 01), - date!(2044 - 01 - 01), - date!(2046 - 01 - 01), - date!(2048 - 01 - 01), - date!(2050 - 01 - 01), - date!(2053 - 01 - 01), - ]; - - discount_curve.get_rates(&new_dates); - - discount_curve.plot(); - - // let nss = NelsonSiegelSvensson::new(0.0806, -0.0031, -0.0625, -0.0198, 1.58, 0.15); - - // let date = date!(2027 - 01 - 01); - // println!("Forward rate: {:?}", nss.forward_rate(date)); - // println!("Spot rate: {:?}", nss.spot_rate(date)); + spot_curve.plot(); } const DATES: [Date; 33] = [ @@ -79,10 +46,56 @@ const DATES: [Date; 33] = [ date!(2054 - 07 - 28), ]; +#[rustfmt::skip] const RATES: [f64; 33] = [ - 0.03400521, 0.03259227, 0.0313705, 0.03031886, 0.02746567, 0.02614014, 0.02574612, 0.02590431, - 0.02637474, 0.02700684, 0.02770726, 0.02841916, 0.02910886, 0.02975736, 0.03035484, 0.03089715, - 0.0313836, 0.03181554, 0.03219547, 0.03252652, 0.03281203, 0.03305541, 0.03326001, 0.033429, - 0.03356541, 0.03367205, 0.03375153, 0.03380629, 0.03383858, 0.03385046, 0.03384384, 0.03382048, + 0.03400521, + 0.03259227, + 0.0313705, + 0.03031886, + 0.02746567, + 0.02614014, + 0.02574612, + 0.02590431, + 0.02637474, + 0.02700684, + 0.02770726, + 0.02841916, + 0.02910886, + 0.02975736, + 0.03035484, + 0.03089715, + 0.0313836, + 0.03181554, + 0.03219547, + 0.03252652, + 0.03281203, + 0.03305541, + 0.03326001, + 0.033429, + 0.03356541, + 0.03367205, + 0.03375153, + 0.03380629, + 0.03383858, + 0.03385046, + 0.03384384, + 0.03382048, 0.033782, ]; + +const NEW_DATES: [Date; 14] = [ + date!(2025 - 01 - 01), + date!(2026 - 01 - 01), + date!(2027 - 01 - 01), + date!(2028 - 01 - 01), + date!(2029 - 01 - 01), + date!(2030 - 01 - 01), + date!(2033 - 01 - 01), + date!(2036 - 01 - 01), + date!(2040 - 01 - 01), + date!(2044 - 01 - 01), + date!(2046 - 01 - 01), + date!(2048 - 01 - 01), + date!(2050 - 01 - 01), + date!(2053 - 01 - 01), +]; diff --git a/examples/custom_payoffs.rs b/examples/custom_payoffs.rs index 18e033d7..f12d6c0a 100644 --- a/examples/custom_payoffs.rs +++ b/examples/custom_payoffs.rs @@ -63,7 +63,8 @@ fn main() { // Generate path using Euler-Maruyama scheme. // Parameters: x_0, t_0, t_n, n, sims, parallel. - let gbm_out = gbm.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); + let config = StochasticProcessConfig::new(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); + let gbm_out = gbm.euler_maruyama(&config); // Price the options. println!("Up-and-out call: {}", barrier_option_payoff(&gbm_out.paths[0], 10.0, 12.0, OptionType::Call, BarrierType::UpAndOut)); diff --git a/examples/custom_process.rs b/examples/custom_process.rs index b1b2f4b7..3ae55a8f 100644 --- a/examples/custom_process.rs +++ b/examples/custom_process.rs @@ -21,7 +21,11 @@ // """ use std::f64::consts::PI; -use RustQuant::{math::Sequence, plot_vector, stochastics::process::StochasticProcess}; +use RustQuant::{ + math::Sequence, + plot_vector, + stochastics::{process::StochasticProcess, StochasticProcessConfig}, +}; fn main() { // Create an x-axis. @@ -40,7 +44,8 @@ fn main() { }; // Generate a path and plot it. - let output = custom_process.euler_maruyama(0.01, 0.0, 10.0, 500, 1, false); + let config = StochasticProcessConfig::new(0.01, 0.0, 10.0, 500, 1, false); + let output = custom_process.euler_maruyama(&config); plot_vector!(output.paths[0], "./images/ricker_wavelet_process.png"); } diff --git a/examples/market_data.rs b/examples/market_data.rs new file mode 100644 index 00000000..3c6d4243 --- /dev/null +++ b/examples/market_data.rs @@ -0,0 +1,14 @@ +use RustQuant::pricer::MarketData; +use RustQuant::pricer::MarketDataBuilder; +use RustQuant::time::oceania::australia::AustraliaCalendar; + +fn main() { + let market_data: MarketData = MarketDataBuilder::default() + .underlying_price(Some(100.0)) + .volatility(Some(0.2)) + .dividend_yield(Some(0.0)) + .build() + .unwrap(); + + println!("{:?}", market_data); +} diff --git a/examples/stochastic_processes.rs b/examples/stochastic_processes.rs index c6849111..d9942828 100644 --- a/examples/stochastic_processes.rs +++ b/examples/stochastic_processes.rs @@ -37,19 +37,21 @@ fn main() { // Generate path using Euler-Maruyama scheme. // Parameters: x_0, t_0, t_n, n, sims, parallel. - let abm_out = abm.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let bdt_out = bdt.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let bm_out = bm.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let cir_out = cir.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let ev_out = ev.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let gbm_out = gbm.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let hl_out = hl.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let hw_out = hw.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let ou_out = ou.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let fbm_out = fbm.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let mjd_out = mjd.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let gbb_out = gbb.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); - let cev_out = cev.euler_maruyama(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); + let config = StochasticProcessConfig::new(INITIAL_VALUE, START_TIME, END_TIME, NUM_STEPS, NUM_SIMS, PARALLEL); + + let abm_out = abm.euler_maruyama(&config); + let bdt_out = bdt.euler_maruyama(&config); + let bm_out = bm.euler_maruyama(&config); + let cir_out = cir.euler_maruyama(&config); + let ev_out = ev.euler_maruyama(&config); + let gbm_out = gbm.euler_maruyama(&config); + let hl_out = hl.euler_maruyama(&config); + let hw_out = hw.euler_maruyama(&config); + let ou_out = ou.euler_maruyama(&config); + let fbm_out = fbm.euler_maruyama(&config); + let mjd_out = mjd.euler_maruyama(&config); + let gbb_out = gbb.euler_maruyama(&config); + let cev_out = cev.euler_maruyama(&config); // Plot the paths. plot_vector!(abm_out.paths[0].clone(), "./images/arithmetic_brownian_motion.png"); @@ -65,4 +67,34 @@ fn main() { plot_vector!(mjd_out.paths[0].clone(), "./images/merton_jump_diffusion.png"); plot_vector!(gbb_out.paths[0].clone(), "./images/geometric_brownian_bridge.png"); plot_vector!(cev_out.paths[0].clone(), "./images/constant_elasticity_of_variance.png"); + + plot_trajectories(&gbm_out, true); +} + +use plotly::{common::Mode, Plot, Scatter}; + +fn plot_trajectories(paths: &Trajectories, show: bool) -> Plot { + let mut plot = Plot::new(); + + let xs = paths + .times + .iter() + .map(|x| x.to_string()) + .collect::>(); + + for (i, path) in paths.paths.iter().enumerate() { + let ys = path.iter().cloned().collect::>(); + + let trace = Scatter::new(xs.clone(), ys) + .mode(Mode::Lines) + .name(format!("Path {}", i + 1)); + + plot.add_trace(trace); + } + + if show { + plot.show(); + } + + plot } diff --git a/examples/yield_curve_interpolation.rs b/examples/yield_curve_interpolation.rs deleted file mode 100644 index 599be4d2..00000000 --- a/examples/yield_curve_interpolation.rs +++ /dev/null @@ -1,38 +0,0 @@ -use time::{Date, Duration}; -use RustQuant::{ - data::{Curve, YieldCurve}, - plot_vector, - time::today, -}; - -fn main() { - // Initial date of the curve (today). - let t0 = today(); - - // Create a treasury yield curve with 8 points (3m, 6m, 1y, 2y, 5y, 10y, 30y). - // Values from Bloomberg: - let rate_vec = vec![0.0544, 0.0556, 0.0546, 0.0514, 0.0481, 0.0481, 0.0494]; - let date_vec = vec![ - t0 + Duration::days(90), - t0 + Duration::days(180), - t0 + Duration::days(365), - t0 + Duration::days(2 * 365), - t0 + Duration::days(5 * 365), - t0 + Duration::days(10 * 365), - t0 + Duration::days(30 * 365), - ]; - - let yield_curve = YieldCurve::from_dates_and_rates(&date_vec, &rate_vec); - - // Create a vector of dates to interpolate the yield curve at. - let dates_to_plot = (91..(30 * 365)) - .step_by(10) - .map(|i| t0 + Duration::days(i)) - .collect::>(); - - // Compute the discount factors. - let discount_factors = yield_curve.discount_factors(&dates_to_plot); - - // Plot the interpolated yield curve. - plot_vector!(discount_factors, "./images/interpolated_yield_curve.png"); -} diff --git a/images/arithmetic_brownian_motion.png b/images/arithmetic_brownian_motion.png index 5d1c9d6d..2172f206 100644 Binary files a/images/arithmetic_brownian_motion.png and b/images/arithmetic_brownian_motion.png differ diff --git a/images/black_derman_toy.png b/images/black_derman_toy.png index db0a383f..5dcb0f64 100644 Binary files a/images/black_derman_toy.png and b/images/black_derman_toy.png differ diff --git a/images/brownian_motion.png b/images/brownian_motion.png index cca81907..fd5abd2e 100644 Binary files a/images/brownian_motion.png and b/images/brownian_motion.png differ diff --git a/images/constant_elasticity_of_variance.png b/images/constant_elasticity_of_variance.png index 8187d3d8..ac3ae178 100644 Binary files a/images/constant_elasticity_of_variance.png and b/images/constant_elasticity_of_variance.png differ diff --git a/images/cox_ingersoll_ross.png b/images/cox_ingersoll_ross.png index f9f738f8..cf94efd0 100644 Binary files a/images/cox_ingersoll_ross.png and b/images/cox_ingersoll_ross.png differ diff --git a/images/extended_vasicek.png b/images/extended_vasicek.png index 08340190..92befd57 100644 Binary files a/images/extended_vasicek.png and b/images/extended_vasicek.png differ diff --git a/images/fractional_brownian_motion.png b/images/fractional_brownian_motion.png index f32db9f0..e94ad1fa 100644 Binary files a/images/fractional_brownian_motion.png and b/images/fractional_brownian_motion.png differ diff --git a/images/geometric_brownian_bridge.png b/images/geometric_brownian_bridge.png index c5ba2724..17835c9b 100644 Binary files a/images/geometric_brownian_bridge.png and b/images/geometric_brownian_bridge.png differ diff --git a/images/geometric_brownian_motion.png b/images/geometric_brownian_motion.png index 13fd4074..853a5a7f 100644 Binary files a/images/geometric_brownian_motion.png and b/images/geometric_brownian_motion.png differ diff --git a/images/ho_lee.png b/images/ho_lee.png index 4f1b0674..02002d08 100644 Binary files a/images/ho_lee.png and b/images/ho_lee.png differ diff --git a/images/hull_white.png b/images/hull_white.png index 15fe08d4..1641e2c6 100644 Binary files a/images/hull_white.png and b/images/hull_white.png differ diff --git a/images/merton_jump_diffusion.png b/images/merton_jump_diffusion.png index 5a207b9a..aa4f942e 100644 Binary files a/images/merton_jump_diffusion.png and b/images/merton_jump_diffusion.png differ diff --git a/images/ornstein_uhlenbeck.png b/images/ornstein_uhlenbeck.png index bc3e4936..aa618578 100644 Binary files a/images/ornstein_uhlenbeck.png and b/images/ornstein_uhlenbeck.png differ diff --git a/src/cashflows/cashflow.rs b/src/cashflows/cashflow.rs index 3f0cfde3..88a05ad2 100644 --- a/src/cashflows/cashflow.rs +++ b/src/cashflows/cashflow.rs @@ -174,20 +174,20 @@ mod test_cashflows { use super::*; use time::Duration; - use crate::assert_approx_equal; + use crate::{assert_approx_equal, time::today}; use std::f64::EPSILON as EPS; // Test to verify the `amount` method. #[test] fn test_amount() { - let cf = Cashflow::new(100.0, Date::now_utc()); + let cf = Cashflow::new(100.0, today()); assert_approx_equal!(cf.amount(), 100.0, EPS); } // Test to verify the `date` method. #[test] fn test_date() { - let now = Date::now_utc(); + let now = today(); let cf = Cashflow::new(100.0, now); assert_eq!(cf.date(), now); } @@ -195,41 +195,26 @@ mod test_cashflows { // Test to verify the `npv` method. #[test] fn test_npv() { - let now = Date::now_utc(); - let cf = Cashflow::new(100.0, now); + let cf = Cashflow::new(100.0, today()); - // Discount function that reduces value by 10%. - let df = |date: Date| if date == now { 0.9 } else { 1.0 }; - assert_approx_equal!(cf.npv(df), 90.0, EPS); + assert_approx_equal!(cf.npv(0.9), 90.0, EPS); } // Test to verify the `npv` method with a zero discount rate. #[test] fn test_npv_zero_discount() { - let now = Date::now_utc(); + let now = today(); let cf = Cashflow::new(100.0, now); // Discount function that keeps value the same. - let df = |_: Date| 1.0; + let df = 1.0; assert_approx_equal!(cf.npv(df), 100.0, EPS); } - // Test to verify the `npv` method with future date - #[test] - fn test_npv_future_date() { - let now = Date::now_utc(); - let future_date = now + Duration::days(30); - let cf = Cashflow::new(100.0, future_date); - - // Discount function that reduces value by 10% for future_date. - let df = |date: Date| if date == future_date { 0.9 } else { 1.0 }; - assert_approx_equal!(cf.npv(df), 90.0, EPS); - } - // Test to verify addition of cashflows with the same date. #[test] fn test_add_cashflows() { - let date = Date::now_utc(); + let date = today(); let cf1 = Cashflow::new(100.0, date); let cf2 = Cashflow::new(50.0, date); let result = cf1 + cf2; @@ -240,7 +225,7 @@ mod test_cashflows { // Test to verify subtraction of cashflows with the same date. #[test] fn test_sub_cashflows() { - let date = Date::now_utc(); + let date = today(); let cf1 = Cashflow::new(100.0, date); let cf2 = Cashflow::new(50.0, date); let result = cf1 - cf2; @@ -251,7 +236,7 @@ mod test_cashflows { // Test for negative cashflows. #[test] fn test_negative_cashflow() { - let date = Date::now_utc(); + let date = today(); let cf = Cashflow::new(-100.0, date); assert_approx_equal!(cf.amount(), -100.0, EPS); } @@ -259,7 +244,7 @@ mod test_cashflows { // Test for zero cashflows. #[test] fn test_zero_cashflow() { - let date = Date::now_utc(); + let date = today(); let cf = Cashflow::new(0.0, date); assert_approx_equal!(cf.amount(), 0.0, EPS); } @@ -268,7 +253,7 @@ mod test_cashflows { #[test] #[should_panic(expected = "Dates must match.")] fn test_non_matching_dates_add() { - let date1 = Date::now_utc(); + let date1 = today(); let date2 = date1 + Duration::days(1); let cf1 = Cashflow::new(100.0, date1); let cf2 = Cashflow::new(50.0, date2); diff --git a/src/cashflows/legs.rs b/src/cashflows/legs.rs index a1b8e55d..6cd2bd53 100644 --- a/src/cashflows/legs.rs +++ b/src/cashflows/legs.rs @@ -78,18 +78,20 @@ impl Leg { #[cfg(test)] mod tests_legs { - use super::super::SimpleCashflow; + // use super::super::SimpleCashflow; use super::*; use crate::assert_approx_equal; + use crate::time::today; use std::f64::EPSILON as EPS; use time::Duration; + use time::OffsetDateTime; // Utility function to generate a simple leg for testing. - fn generate_simple_leg(now: OffsetDateTime) -> Leg { + fn generate_simple_leg(now: Date) -> Leg { let cashflows = vec![ - SimpleCashflow::new(100.0, now), - SimpleCashflow::new(200.0, now + Duration::days(30)), - SimpleCashflow::new(300.0, now + Duration::days(60)), + Cashflow::new(100.0, now), + Cashflow::new(200.0, now + Duration::days(30)), + Cashflow::new(300.0, now + Duration::days(60)), ]; Leg::new(cashflows) } @@ -97,7 +99,7 @@ mod tests_legs { // Test to verify the `size` method. #[test] fn test_size() { - let now = OffsetDateTime::now_utc(); + let now = today(); let leg = generate_simple_leg(now); assert_eq!(leg.size(), 3); } @@ -105,20 +107,20 @@ mod tests_legs { // Test to verify the `npv` method. #[test] fn test_npv() { - let now = OffsetDateTime::now_utc(); + let now = today(); let leg = generate_simple_leg(now); // Discount function that reduces value by 10%. - let df = |_| 0.9; + let df = 0.9; assert_approx_equal!(leg.npv(df), 540.0, EPS); } // Test to verify the `add_cashflow` method. #[test] fn test_add_cashflow() { - let now = OffsetDateTime::now_utc(); + let now = today(); let mut leg = generate_simple_leg(now); - let new_cashflow = SimpleCashflow::new(400.0, now + Duration::days(90)); + let new_cashflow = Cashflow::new(400.0, now + Duration::days(90)); leg.add_cashflow(new_cashflow.clone()); assert_eq!(leg.size(), 4); assert_approx_equal!( @@ -131,7 +133,7 @@ mod tests_legs { // Test to verify the `start_date` and `end_date` methods. #[test] fn test_start_end_date() { - let now = OffsetDateTime::now_utc(); + let now = today(); let leg = generate_simple_leg(now); let start = leg.start_date().unwrap(); let end = leg.end_date().unwrap(); @@ -142,7 +144,7 @@ mod tests_legs { // Test to verify the `is_active` method. #[test] fn test_is_active() { - let now = OffsetDateTime::now_utc(); + let now = today(); let leg = generate_simple_leg(now); assert!(leg.is_active(now)); assert!(leg.is_active(now + Duration::days(30))); diff --git a/src/data/curves.rs b/src/data/curves.rs index f9732e2c..febfe41e 100644 --- a/src/data/curves.rs +++ b/src/data/curves.rs @@ -316,15 +316,6 @@ pub trait Curves { /// Create a new curve from a set of `Date`s and rates (`f64`s). fn new(dates: &[Date], rates: &[f64]) -> Self; - /// Set the calendar for the curve. - fn with_calendar(&mut self, calendar: C); - - /// Set the day count convention for the curve. - fn with_day_count_convention(&mut self, day_count_convention: DayCountConvention); - - /// Set the date rolling convention for the curve. - fn with_date_rolling_convention(&mut self, date_rolling_convention: DateRollingConvention); - /// Get the initial date of the curve. fn initial_date(&self) -> Date; @@ -347,7 +338,7 @@ pub trait Curves { fn plot(&self); } -macro_rules! impl_specific_curve { +macro_rules! impl_specific_curve_cost_function { ($curve:ident, $curve_function:ident) => { impl CostFunction for &$curve where @@ -377,11 +368,11 @@ macro_rules! impl_specific_curve { Ok(log_cosh_loss) } } + }; +} - // impl $curve - // where - // C: Calendar + Clone, - // { +macro_rules! impl_specific_curve { + ($curve:ident, $curve_function:ident) => { impl Curves for $curve where C: Calendar + Clone, @@ -439,24 +430,6 @@ macro_rules! impl_specific_curve { } } - #[doc = concat!("Set the calendar for the ", stringify!($curve))] - fn with_calendar(&mut self, calendar: C) { - self.calendar = Some(calendar); - } - - #[doc = concat!("Set the day count convention for the ", stringify!($curve))] - fn with_day_count_convention(&mut self, day_count_convention: DayCountConvention) { - self.day_count_convention = Some(day_count_convention); - } - - #[doc = concat!("Set the date rolling convention for the ", stringify!($curve))] - fn with_date_rolling_convention( - &mut self, - date_rolling_convention: DateRollingConvention, - ) { - self.date_rolling_convention = Some(date_rolling_convention); - } - #[doc = concat!("Get the initial date of the ", stringify!($curve))] fn initial_date(&self) -> Date { *self.curve.first_key().unwrap() @@ -569,7 +542,7 @@ macro_rules! impl_specific_curve { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// Discount curve data structure. -#[derive(Builder, Clone)] +#[derive(Builder, Clone, Debug)] pub struct DiscountCurve where I: CurveIndex, @@ -601,6 +574,7 @@ where pub fitted_curve: Option>, } +impl_specific_curve_cost_function!(DiscountCurve, discount_factor); impl_specific_curve!(DiscountCurve, discount_factor); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -608,7 +582,7 @@ impl_specific_curve!(DiscountCurve, discount_factor); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// Spot curve data structure. -#[derive(Builder, Clone)] +#[derive(Builder, Clone, Debug)] pub struct SpotCurve where I: CurveIndex, @@ -640,6 +614,7 @@ where pub fitted_curve: Option>, } +impl_specific_curve_cost_function!(SpotCurve, spot_rate); impl_specific_curve!(SpotCurve, spot_rate); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -647,7 +622,7 @@ impl_specific_curve!(SpotCurve, spot_rate); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// Forward curve data structure. -#[derive(Builder, Clone)] +#[derive(Builder, Clone, Debug)] pub struct ForwardCurve where I: CurveIndex, @@ -679,8 +654,100 @@ where pub fitted_curve: Option>, } +impl_specific_curve_cost_function!(ForwardCurve, forward_rate); impl_specific_curve!(ForwardCurve, forward_rate); +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// FLAT CURVE +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Flat curve data structure. +#[derive(Builder, Clone, Debug)] +pub struct FlatCurve +where + C: Calendar, +{ + /// Rate of the curve. + pub rate: f64, + + /// Calendar. + pub calendar: Option, + + /// Day count convention. + pub day_count_convention: Option, + + /// Date rolling convention. + pub date_rolling_convention: Option, +} + +impl FlatCurve +where + C: Calendar, +{ + /// Create a new flat curve. + pub fn new_flat_curve(rate: f64) -> Self { + Self { + rate, + calendar: None, + day_count_convention: None, + date_rolling_convention: None, + } + } + + /// Get the rate of the curve. + pub fn get_rate(&self) -> f64 { + self.rate + } + + /// Get rate for a specific date. + pub fn get_rate_for_date(&self, _date: Date) -> f64 { + self.rate + } + + /// Get rates for multiple dates. + pub fn get_rates_for_dates(&self, dates: &[Date]) -> Vec { + vec![self.rate; dates.len()] + } +} + +// impl Curves for FlatCurve +// where +// C: Calendar, +// { +// /// NOT TO BE USED. Prefer the `new_flat_curve()` method. +// fn new(dates: &[Date], rates: &[f64]) -> Self { +// unimplemented!("FlatCurve does not support this method. Use `new_flat_curve()` instead.") +// } + +// fn initial_date(&self) -> Date { +// Date::MIN +// } + +// fn terminal_date(&self) -> Date { +// Date::MAX +// } + +// fn get_rate(&mut self, date: Date) -> f64 { +// self.rate +// } + +// fn get_rates(&mut self, dates: &[Date]) -> Vec { +// vec![self.rate; dates.len()] +// } + +// fn insert_rate(&mut self, date: Date, rate: f64) { +// todo!() +// } + +// fn fit(&mut self) -> Result<(), argmin::core::Error> { +// unimplemented!() +// } + +// fn plot(&self) { +// unimplemented!() +// } +// } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Base Curve Trait // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -774,342 +841,341 @@ impl_specific_curve!(ForwardCurve, forward_rate); #[cfg(test)] mod tests_curves { - use super::*; - use crate::time::today; - use std::collections::BTreeMap; - use time::Duration; - use time::OffsetDateTime; - - #[test] - fn test_discount_curve_creation() { - let dates = [today() + Duration::days(30), today() + Duration::days(60)]; - let rates = [0.025, 0.03]; - - let discount_curve = DiscountCurve::new(&dates, &rates); - - assert_eq!(discount_curve.rates, rates); - } - - #[test] - fn test_discount_curve_initial_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = DiscountCurve::new(&dates, &rates); - let initial_date = discount_curve.initial_date(); - - assert_eq!( - initial_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) - ); - } - - #[test] - fn test_discount_curve_final_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = DiscountCurve::new(&dates, &rates); - let final_date = discount_curve.terminal_date(); - - assert_eq!( - final_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) - ); - } - - #[test] - fn test_find_date_interval() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = DiscountCurve::new(&dates, &rates); - - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); - - let interval1 = discount_curve.find_date_interval(date1); - let interval2 = discount_curve.find_date_interval(date2); - let interval3 = discount_curve.find_date_interval(date3); - - assert_eq!(interval1, (date1, date1)); - assert_eq!(interval2, (date1, date3)); - assert_eq!(interval3, (date3, date3)); - } - - #[allow(clippy::similar_names)] - #[test] - fn test_discount_curve_discount_factor() { - // Initial date of the curve. - let t0 = OffsetDateTime::UNIX_EPOCH.date(); - - // Create a discount curve with 8 points. - let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; - let date_vec = vec![ - t0 + Duration::days(30), - t0 + Duration::days(60), - t0 + Duration::days(90), - t0 + Duration::days(120), - t0 + Duration::days(150), - t0 + Duration::days(180), - t0 + Duration::days(210), - t0 + Duration::days(360), - ]; - - let discount_curve = DiscountCurve::from_dates_and_rates(&date_vec, &rate_vec); - - println!("Curve: {:?}", discount_curve.rates); - - // Test the discount factor for a dates inside the curve's range. - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); - - let df1 = discount_curve.discount_factor(date1); - let df2 = discount_curve.discount_factor(date2); - let df3 = discount_curve.discount_factor(date3); - - println!("df1: {:?}", df1); - println!("df2: {:?}", df2); - println!("df3: {:?}", df3); - - assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); - - assert!(df1 > df2 && df2 > df3); - } - - #[test] - fn test_discount_curve_creation() { - let dates = [today() + Duration::days(30), today() + Duration::days(60)]; - let rates = [0.025, 0.03]; - - let discount_curve = ForwardCurve::new(&dates, &rates); - - assert_eq!(discount_curve.rates, rates); - } - - #[test] - fn test_discount_curve_initial_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = ForwardCurve::new(&dates, &rates); - let initial_date = discount_curve.initial_date(); - - assert_eq!( - initial_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) - ); - } - - #[test] - fn test_discount_curve_final_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = ForwardCurve::new(&dates, &rates); - let final_date = discount_curve.terminal_date(); + // use super::*; + // use crate::time::today; + // use time::Duration; + // use time::OffsetDateTime; + + // #[test] + // fn test_discount_curve_creation() { + // let dates = [today() + Duration::days(30), today() + Duration::days(60)]; + // let rates = [0.025, 0.03]; + + // let discount_curve = DiscountCurve::new(&dates, &rates); + + // assert_eq!(discount_curve.rates, rates); + // } + + // #[test] + // fn test_discount_curve_initial_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = DiscountCurve::new(&dates, &rates); + // let initial_date = discount_curve.initial_date(); + + // assert_eq!( + // initial_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) + // ); + // } + + // #[test] + // fn test_discount_curve_final_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = DiscountCurve::new(&dates, &rates); + // let final_date = discount_curve.terminal_date(); + + // assert_eq!( + // final_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) + // ); + // } + + // #[test] + // fn test_find_date_interval() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = DiscountCurve::new(&dates, &rates); + + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); + + // let interval1 = discount_curve.find_date_interval(date1); + // let interval2 = discount_curve.find_date_interval(date2); + // let interval3 = discount_curve.find_date_interval(date3); + + // assert_eq!(interval1, (date1, date1)); + // assert_eq!(interval2, (date1, date3)); + // assert_eq!(interval3, (date3, date3)); + // } + + // #[allow(clippy::similar_names)] + // #[test] + // fn test_discount_curve_discount_factor() { + // // Initial date of the curve. + // let t0 = OffsetDateTime::UNIX_EPOCH.date(); + + // // Create a discount curve with 8 points. + // let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; + // let date_vec = vec![ + // t0 + Duration::days(30), + // t0 + Duration::days(60), + // t0 + Duration::days(90), + // t0 + Duration::days(120), + // t0 + Duration::days(150), + // t0 + Duration::days(180), + // t0 + Duration::days(210), + // t0 + Duration::days(360), + // ]; + + // let discount_curve = DiscountCurve::from_dates_and_rates(&date_vec, &rate_vec); + + // println!("Curve: {:?}", discount_curve.rates); + + // // Test the discount factor for a dates inside the curve's range. + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); + + // let df1 = discount_curve.discount_factor(date1); + // let df2 = discount_curve.discount_factor(date2); + // let df3 = discount_curve.discount_factor(date3); + + // println!("df1: {:?}", df1); + // println!("df2: {:?}", df2); + // println!("df3: {:?}", df3); + + // assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); + + // assert!(df1 > df2 && df2 > df3); + // } + + // #[test] + // fn test_discount_curve_creation() { + // let dates = [today() + Duration::days(30), today() + Duration::days(60)]; + // let rates = [0.025, 0.03]; + + // let discount_curve = ForwardCurve::new(&dates, &rates); + + // assert_eq!(discount_curve.rates, rates); + // } + + // #[test] + // fn test_discount_curve_initial_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = ForwardCurve::new(&dates, &rates); + // let initial_date = discount_curve.initial_date(); + + // assert_eq!( + // initial_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) + // ); + // } + + // #[test] + // fn test_discount_curve_final_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = ForwardCurve::new(&dates, &rates); + // let final_date = discount_curve.terminal_date(); + + // assert_eq!( + // final_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) + // ); + // } + + // #[test] + // fn test_find_date_interval() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = ForwardCurve::new(&dates, &rates); + + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); + + // let interval1 = discount_curve.find_date_interval(date1); + // let interval2 = discount_curve.find_date_interval(date2); + // let interval3 = discount_curve.find_date_interval(date3); + + // assert_eq!(interval1, (date1, date1)); + // assert_eq!(interval2, (date1, date3)); + // assert_eq!(interval3, (date3, date3)); + // } + + // #[allow(clippy::similar_names)] + // #[test] + // fn test_discount_curve_discount_factor() { + // // Initial date of the curve. + // let t0 = OffsetDateTime::UNIX_EPOCH.date(); - assert_eq!( - final_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) - ); - } - - #[test] - fn test_find_date_interval() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = ForwardCurve::new(&dates, &rates); - - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); - - let interval1 = discount_curve.find_date_interval(date1); - let interval2 = discount_curve.find_date_interval(date2); - let interval3 = discount_curve.find_date_interval(date3); - - assert_eq!(interval1, (date1, date1)); - assert_eq!(interval2, (date1, date3)); - assert_eq!(interval3, (date3, date3)); - } - - #[allow(clippy::similar_names)] - #[test] - fn test_discount_curve_discount_factor() { - // Initial date of the curve. - let t0 = OffsetDateTime::UNIX_EPOCH.date(); - - // Create a discount curve with 8 points. - let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; - let date_vec = vec![ - t0 + Duration::days(30), - t0 + Duration::days(60), - t0 + Duration::days(90), - t0 + Duration::days(120), - t0 + Duration::days(150), - t0 + Duration::days(180), - t0 + Duration::days(210), - t0 + Duration::days(360), - ]; - - let discount_curve = ForwardCurve::from_dates_and_rates(&date_vec, &rate_vec); - - println!("Curve: {:?}", discount_curve.rates); - - // Test the discount factor for a dates inside the curve's range. - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); - - let df1 = discount_curve.discount_factor(date1); - let df2 = discount_curve.discount_factor(date2); - let df3 = discount_curve.discount_factor(date3); - - println!("df1: {:?}", df1); - println!("df2: {:?}", df2); - println!("df3: {:?}", df3); - - assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); - - assert!(df1 > df2 && df2 > df3); - } - - #[test] - fn test_discount_curve_creation() { - let dates = [today() + Duration::days(30), today() + Duration::days(60)]; - let rates = [0.025, 0.03]; - - let discount_curve = SpotCurve::new(&dates, &rates); - - assert_eq!(discount_curve.rates, rates); - } - - #[test] - fn test_discount_curve_initial_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; + // // Create a discount curve with 8 points. + // let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; + // let date_vec = vec![ + // t0 + Duration::days(30), + // t0 + Duration::days(60), + // t0 + Duration::days(90), + // t0 + Duration::days(120), + // t0 + Duration::days(150), + // t0 + Duration::days(180), + // t0 + Duration::days(210), + // t0 + Duration::days(360), + // ]; + + // let discount_curve = ForwardCurve::from_dates_and_rates(&date_vec, &rate_vec); + + // println!("Curve: {:?}", discount_curve.rates); + + // // Test the discount factor for a dates inside the curve's range. + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); + + // let df1 = discount_curve.discount_factor(date1); + // let df2 = discount_curve.discount_factor(date2); + // let df3 = discount_curve.discount_factor(date3); + + // println!("df1: {:?}", df1); + // println!("df2: {:?}", df2); + // println!("df3: {:?}", df3); + + // assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); + + // assert!(df1 > df2 && df2 > df3); + // } + + // #[test] + // fn test_discount_curve_creation() { + // let dates = [today() + Duration::days(30), today() + Duration::days(60)]; + // let rates = [0.025, 0.03]; + + // let discount_curve = SpotCurve::new(&dates, &rates); + + // assert_eq!(discount_curve.rates, rates); + // } + + // #[test] + // fn test_discount_curve_initial_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = SpotCurve::new(&dates, &rates); + // let initial_date = discount_curve.initial_date(); + + // assert_eq!( + // initial_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) + // ); + // } + + // #[test] + // fn test_discount_curve_final_date() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = SpotCurve::new(&dates, &rates); + // let final_date = discount_curve.terminal_date(); + + // assert_eq!( + // final_date, + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) + // ); + // } + + // #[test] + // fn test_find_date_interval() { + // let dates = [ + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), + // OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), + // ]; + + // let rates = [0.025, 0.03]; + + // let discount_curve = SpotCurve::new(&dates, &rates); + + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); + + // let interval1 = discount_curve.find_date_interval(date1); + // let interval2 = discount_curve.find_date_interval(date2); + // let interval3 = discount_curve.find_date_interval(date3); + + // assert_eq!(interval1, (date1, date1)); + // assert_eq!(interval2, (date1, date3)); + // assert_eq!(interval3, (date3, date3)); + // } + + // #[allow(clippy::similar_names)] + // #[test] + // fn test_discount_curve_discount_factor() { + // // Initial date of the curve. + // let t0 = OffsetDateTime::UNIX_EPOCH.date(); - let discount_curve = SpotCurve::new(&dates, &rates); - let initial_date = discount_curve.initial_date(); - - assert_eq!( - initial_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30) - ); - } - - #[test] - fn test_discount_curve_final_date() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = SpotCurve::new(&dates, &rates); - let final_date = discount_curve.terminal_date(); - - assert_eq!( - final_date, - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60) - ); - } - - #[test] - fn test_find_date_interval() { - let dates = [ - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30), - OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60), - ]; - - let rates = [0.025, 0.03]; - - let discount_curve = SpotCurve::new(&dates, &rates); - - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(30); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(60); - - let interval1 = discount_curve.find_date_interval(date1); - let interval2 = discount_curve.find_date_interval(date2); - let interval3 = discount_curve.find_date_interval(date3); - - assert_eq!(interval1, (date1, date1)); - assert_eq!(interval2, (date1, date3)); - assert_eq!(interval3, (date3, date3)); - } - - #[allow(clippy::similar_names)] - #[test] - fn test_discount_curve_discount_factor() { - // Initial date of the curve. - let t0 = OffsetDateTime::UNIX_EPOCH.date(); - - // Create a discount curve with 8 points. - let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; - let date_vec = vec![ - t0 + Duration::days(30), - t0 + Duration::days(60), - t0 + Duration::days(90), - t0 + Duration::days(120), - t0 + Duration::days(150), - t0 + Duration::days(180), - t0 + Duration::days(210), - t0 + Duration::days(360), - ]; - - let discount_curve = SpotCurve::from_dates_and_rates(&date_vec, &rate_vec); - - println!("Curve: {:?}", discount_curve.rates); - - // Test the discount factor for a dates inside the curve's range. - let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); - let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); - let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); - - let df1 = discount_curve.discount_factor(date1); - let df2 = discount_curve.discount_factor(date2); - let df3 = discount_curve.discount_factor(date3); - - println!("df1: {:?}", df1); - println!("df2: {:?}", df2); - println!("df3: {:?}", df3); - - assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); - - assert!(df1 > df2 && df2 > df3); - } + // // Create a discount curve with 8 points. + // let rate_vec = vec![0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06]; + // let date_vec = vec![ + // t0 + Duration::days(30), + // t0 + Duration::days(60), + // t0 + Duration::days(90), + // t0 + Duration::days(120), + // t0 + Duration::days(150), + // t0 + Duration::days(180), + // t0 + Duration::days(210), + // t0 + Duration::days(360), + // ]; + + // let discount_curve = SpotCurve::new(&date_vec, &rate_vec); + + // println!("Curve: {:?}", discount_curve.rates); + + // // Test the discount factor for a dates inside the curve's range. + // let date1 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(45); + // let date2 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(80); + // let date3 = OffsetDateTime::UNIX_EPOCH.date() + Duration::days(250); + + // let df1 = discount_curve.discount_factor(date1); + // let df2 = discount_curve.discount_factor(date2); + // let df3 = discount_curve.discount_factor(date3); + + // println!("df1: {:?}", df1); + // println!("df2: {:?}", df2); + // println!("df3: {:?}", df3); + + // assert!(df1 > 0.0 && df1 < 1.0 && df2 > 0.0 && df2 < 1.0 && df3 > 0.0 && df3 < 1.0); + + // assert!(df1 > df2 && df2 > df3); + // } } diff --git a/src/instruments/fx/currency.rs b/src/instruments/fx/currency.rs index ca09b08b..3f71ccf3 100644 --- a/src/instruments/fx/currency.rs +++ b/src/instruments/fx/currency.rs @@ -22,7 +22,7 @@ use std::fmt::{self, Formatter}; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// Currency data struct. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Currency { /// Currency name. e.g. United States Dollar pub name: &'static str, @@ -36,6 +36,16 @@ pub struct Currency { pub fractions: usize, } +/// Currency pair. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CurrencyPair { + /// Base currency. + pub base: Currency, + + /// Quote currency. + pub quote: Currency, +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // IMPLEMENTATIONS // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -108,14 +118,21 @@ impl Instrument for Currency { } } -impl Eq for Currency {} - -impl PartialEq for Currency { - fn eq(&self, other: &Self) -> bool { - self.code == other.code +impl CurrencyPair { + /// Create a new currency pair. + pub fn new(base: Currency, quote: Currency) -> Self { + Self { base, quote } } } +// impl Eq for Currency {} + +// impl PartialEq for Currency { +// fn eq(&self, other: &Self) -> bool { +// self.code == other.code +// } +// } + impl fmt::Display for Currency { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Currency:\t{}\nISO Code:\t{:?}", self.name, self.code) diff --git a/src/instruments/fx/exchange.rs b/src/instruments/fx/exchange.rs index 95fe7fa3..05a8691b 100644 --- a/src/instruments/fx/exchange.rs +++ b/src/instruments/fx/exchange.rs @@ -9,6 +9,7 @@ //! FX exchange module. +use super::CurrencyPair; use crate::instruments::fx::currency::Currency; use crate::instruments::fx::money::Money; use std::collections::HashMap; @@ -24,7 +25,7 @@ pub struct Exchange { /// The key is a string of the form e.g. "USD_EUR", /// and the value is an ExchangeRate struct. /// The key is generated from the from_currency and to_currency of the ExchangeRate. - pub rates: HashMap, + pub rates: HashMap, } /// `ExchangeRate` struct to hold exchange rate information. @@ -79,10 +80,11 @@ impl Exchange { /// ``` /// pub fn add_rate(&mut self, rate: ExchangeRate) { - let key = format!( - "{}/{}", - rate.from_currency.code.alphabetic, rate.to_currency.code.alphabetic - ); + // let key = format!( + // "{}/{}", + // rate.from_currency.code.alphabetic, rate.to_currency.code.alphabetic + // ); + let key = CurrencyPair::new(rate.from_currency, rate.to_currency); self.rates.insert(key, rate); } @@ -115,10 +117,11 @@ impl Exchange { from_currency: &Currency, to_currency: &Currency, ) -> Option<&ExchangeRate> { - let key = format!( - "{}/{}", - from_currency.code.alphabetic, to_currency.code.alphabetic - ); + // let key = format!( + // "{}/{}", + // from_currency.code.alphabetic, to_currency.code.alphabetic + // ); + let key = CurrencyPair::new(*from_currency, *to_currency); self.rates.get(&key) } diff --git a/src/instruments/instrument.rs b/src/instruments/instrument.rs index 380e359a..748e6d19 100644 --- a/src/instruments/instrument.rs +++ b/src/instruments/instrument.rs @@ -7,8 +7,6 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -use time::Date; - /// Instrument trait /// The trait provides a common interface for all instruments. /// All instruments can be queried for their net present value (NPV) and @@ -25,65 +23,8 @@ pub trait Instrument { fn error(&self) -> Option; /// Returns the date at which the NPV is calculated. - fn valuation_date(&self) -> Date; + fn valuation_date(&self) -> time::Date; /// Instrument type. fn instrument_type(&self) -> &'static str; } - -/// Price structure. -pub struct Price { - /// Price of the instrument. - pub price: f64, - - /// Error on the price of the instrument. - pub error: Option, -} - -/// Pricing engine for instruments. -pub enum PricingEngine { - /// Analytic pricing method (e.g. closed-form solution). - Analytic, - - /// Simulation pricing method (e.g. Monte Carlo). - Simulation, - - /// Numerical method (e.g. PDE, lattice, finite differences). - Numerical, -} - -/// Path independent payoff trait. -pub trait PathIndependentPayoff { - /// Base method for path independent option payoffs. - fn payoff(&self, underlying: f64) -> f64; -} - -/// Path dependent payoff trait. -pub trait PathDependentPayoff { - /// Base method for path dependent option payoffs. - fn payoff(&self, path: &[f64]) -> f64; -} - -// trait Payoff { -// fn path_dependent(&self, path: &[f64]) -> f64; -// fn path_independent(&self, path: &[f64]) -> f64; -// } - -// struct MonteCarloPricer -// where -// PAYOFF: crate::instruments::PathDependentPayoff, -// MODEL: crate::stochastics::StochasticProcess, -// { -// payoff: PAYOFF, -// model: MODEL, -// } - -// impl PathDependentPayoff for EuropeanOption { -// fn payoff(&self, path: &[f64]) -> f64 { -// let spot = path.last().unwrap(); -// match self.option_type { -// OptionType::Call => (spot - self.strike_price).max(0.0), -// OptionType::Put => (self.strike_price - spot).max(0.0), -// } -// } -// } diff --git a/src/instruments/mod.rs b/src/instruments/mod.rs index 844622cb..48cd03c8 100644 --- a/src/instruments/mod.rs +++ b/src/instruments/mod.rs @@ -75,7 +75,7 @@ pub use instrument::*; /// Bond pricing models. pub mod bonds; -pub use bonds::*; +// pub use bonds::*; /// Option pricers and sensitivity functions. pub mod options; @@ -92,3 +92,7 @@ pub use equities::*; /// Ticker symbol. pub mod ticker; pub use ticker::*; + +/// Generic derivative payoff trait. +pub mod payoff; +pub use payoff::*; diff --git a/src/instruments/options/american/mod.rs b/src/instruments/options/american/mod.rs deleted file mode 100644 index fe604d51..00000000 --- a/src/instruments/options/american/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// RustQuant: A Rust library for quantitative finance tools. -// Copyright (C) 2023-24 https://github.com/avhz -// Dual licensed under Apache 2.0 and MIT. -// See: -// - LICENSE-APACHE.md -// - LICENSE-MIT.md -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -struct AmericanOption { - /// The underlying asset price. - underlying: f64, - - /// The strike price. - strike: f64, - - /// The risk-free interest rate. - rate: f64, - - /// The volatility of the underlying asset. - volatility: f64, - - /// The time to expiry. - time_to_expiry: f64, -} diff --git a/src/instruments/options/asian.rs b/src/instruments/options/asian.rs index 4d5e8daf..65eb5909 100644 --- a/src/instruments/options/asian.rs +++ b/src/instruments/options/asian.rs @@ -7,155 +7,45 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -use crate::{ - math::distributions::{gaussian::Gaussian, Distribution}, - time::{today, DayCountConvention}, -}; -use time::Date; - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STRUCTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/// Type of Asian option (fixed or floating strike). -#[allow(clippy::module_name_repetitions)] -#[derive(Debug, Clone, Copy)] -pub enum AsianStrike { - /// Floating strike Asian option. - /// Payoffs: - /// - Call: `max(S_T - A, 0)` - /// - Put: `max(A - S_T, 0)` - Floating, - /// Fixed strike Asian option. - /// Payoffs: - /// - Call: `max(A - K, 0)` - /// - Put: `max(K - A, 0)` - Fixed, -} - -/// Method of averaging (arithmetic or geometric, and continuous or discrete). -#[derive(Debug, Clone, Copy)] -pub enum AveragingMethod { - /// Arithmetic Asian option with discrete averaging. - ArithmeticDiscrete, - /// Arithmetic Asian option with continuous averaging. - ArithmeticContinuous, - /// Geometric Asian option with discrete averaging. - GeometricDiscrete, - /// Geometric Asian option with continuous averaging. - GeometricContinuous, -} - -/// Asian Option struct. -#[allow(clippy::module_name_repetitions)] -#[derive(derive_builder::Builder, Debug, Clone, Copy)] +/// Asian option. +#[derive(Debug, Clone)] pub struct AsianOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `K` - Strike price. - pub strike_price: f64, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, - /// `v` - Volatility parameter. - pub volatility: f64, - /// `q` - Dividend rate. - pub dividend_rate: f64, - - /// `evaluation_date` - Valuation date. - #[builder(default = "None")] - pub evaluation_date: Option, - - /// `expiry_date` - Expiry date. - pub expiration_date: Date, -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// IMPLEMENTATIONS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /// The option contract. + pub contract: OptionContract, -impl AsianOption { - /// New Asian Option - #[must_use] - pub const fn new( - initial_price: f64, - strike_price: f64, - risk_free_rate: f64, - volatility: f64, - dividend_rate: f64, - evaluation_date: Option, - expiration_date: Date, - ) -> Self { - Self { - initial_price, - strike_price, - risk_free_rate, - volatility, - dividend_rate, - evaluation_date, - expiration_date, - } - } - - /// Geometric Continuous Average-Rate Price - #[must_use] - pub fn price_geometric_average(&self) -> (f64, f64) { - let S = self.initial_price; - let K = self.strike_price; - // let T = self.time_to_maturity; - let r = self.risk_free_rate; - let v = self.volatility; - let q = self.dividend_rate; - - // Compute time to maturity. - let T = DayCountConvention::default().day_count_factor( - self.evaluation_date.unwrap_or(today()), - self.expiration_date, - ); - - let v_a = v / 3_f64.sqrt(); - let b = r - q; - let b_a = 0.5 * (b - v * v / 6.0); - - let d1 = ((S / K).ln() + (b_a + 0.5 * v_a * v_a) * T) / (v_a * (T).sqrt()); - let d2 = d1 - v_a * (T).sqrt(); - - let N = Gaussian::default(); + /// Averging method (arithmetic or geometric). + pub averaging_method: AveragingMethod, - let c = S * ((b_a - r) * T).exp() * N.cdf(d1) - K * (-r * T).exp() * N.cdf(d2); - let p = -S * ((b_a - r) * T).exp() * N.cdf(-d1) + K * (-r * T).exp() * N.cdf(-d2); - - (c, p) - } + /// Strike price of the option. + /// Required for fixed strike Asian options. + pub strike: Option, } -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// TESTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +impl Payoff for AsianOption { + type Underlying = Vec; -#[cfg(test)] -mod tests { - use time::Duration; + fn payoff(&self, underlying: Self::Underlying) -> f64 { + let n = underlying.len(); + let path = underlying.iter(); + let terminal = underlying[n - 1]; - use super::*; - use crate::assert_approx_equal; + let average = match self.averaging_method { + AveragingMethod::ArithmeticDiscrete => path.sum::() / n as f64, + AveragingMethod::GeometricDiscrete => path.product::().powf(1.0 / n as f64), - #[test] - fn test_asian_geometric() { - let expiry_date = today() + Duration::days(92); - - let AsianOption = AsianOption { - initial_price: 80.0, - strike_price: 85.0, - risk_free_rate: 0.05, - volatility: 0.2, - evaluation_date: None, - expiration_date: expiry_date, - dividend_rate: -0.03, + // Continuous averaging (i.e. integral of the path). + _ => panic!("Continuous averaging not implemented."), }; - let prices = AsianOption.price_geometric_average(); - - // Value from Haug's book. - assert_approx_equal!(prices.1, 4.6922, 0.0001); + match self.contract.strike_flag { + StrikeFlag::Fixed => match self.contract.type_flag { + TypeFlag::Call => (average - self.strike.unwrap_or_default()).max(0.0), + TypeFlag::Put => (self.strike.unwrap_or_default() - average).max(0.0), + }, + StrikeFlag::Floating => match self.contract.type_flag { + TypeFlag::Call => (terminal - average).max(0.0), + TypeFlag::Put => (average - terminal).max(0.0), + }, + } } } diff --git a/src/instruments/options/barrier.rs b/src/instruments/options/barrier.rs index 08855545..89e90f6b 100644 --- a/src/instruments/options/barrier.rs +++ b/src/instruments/options/barrier.rs @@ -7,314 +7,21 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -use crate::math::distributions::{gaussian::Gaussian, Distribution}; - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// BARRIER OPTION STRUCT -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/// Barrier Option struct for parameters and pricing methods. -#[derive(Debug, Clone, Copy)] -#[allow(clippy::module_name_repetitions)] +/// Barrier option. +#[derive(Debug, Clone)] pub struct BarrierOption { - /// * `S` - Initial underlying price. - pub initial_price: f64, - /// * `X` - Strike price. - pub strike_price: f64, - /// * `H` - Barrier. - pub barrier: f64, - /// * `t` - Time to expiry. - pub time_to_expiry: f64, - /// * `r` - Risk-free rate. - pub risk_free_rate: f64, - /// * `v` - Volatility. - pub volatility: f64, - /// * `K` - Rebate (paid if the option is not able to be exercised). - pub rebate: f64, - /// * `q` - Dividend yield. - pub dividend_yield: f64, -} - -/// Barrier option type enum. -#[derive(Debug, Clone, Copy)] -#[allow(clippy::module_name_repetitions)] -pub enum BarrierType { - /// Call (up-and-in) - /// Payoff: `max(S_T - X, 0) * I(max(S_t) > H)` - CUI, - /// Call (down-and-in) - /// Payoff: `max(S_T - X, 0) * I(min(S_t) < H)` - CDI, - /// Call (up-and-out) - /// Payoff: `max(S_T - X, 0) * I(max(S_t) < H)` - CUO, - /// Call (down-and-out) - /// Payoff: `max(S_T - X, 0) * I(min(S_t) > H)` - CDO, - /// Put (up-and-in) - /// Payoff: `max(X - S_T, 0) * I(max(S_t) > H)` - PUI, - /// Put (down-and-in) - /// Payoff: `max(X - S_T, 0) * I(min(S_t) < H)` - PDI, - /// Put (up-and-out) - /// Payoff: `max(X - S_T, 0) * I(max(S_t) < H)` - PUO, - /// Put (down-and-out) - /// Payoff: `max(X - S_T, 0) * I(min(S_t) > H)` - PDO, -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// BARRIER OPTION IMPLEMENTATION -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -impl BarrierOption { - /// Closed-form solution for path-dependent barrier options. - /// - /// Adapted from Haug's *Complete Guide to Option Pricing Formulas*. - /// - /// # Arguments: - /// - /// * `type_flag` - One of: `cui`, `cuo`, `pui`, `puo`, `cdi`, `cdo`, `pdi`, `pdo`. - /// - /// # Note: - /// * `b = r - q` - The cost of carry. - #[must_use] - pub fn price(&self, type_flag: BarrierType) -> f64 { - let S = self.initial_price; - let X = self.strike_price; - let H = self.barrier; - let t = self.time_to_expiry; - let r = self.risk_free_rate; - let v = self.volatility; - let K = self.rebate; - let q = self.dividend_yield; - - let b: f64 = r - q; - - // Common terms: - let mu: f64 = (b - v * v / 2.) / (v * v); - let lambda: f64 = (mu * mu + 2. * r / (v * v)).sqrt(); - let z: f64 = (H / S).ln() / (v * t.sqrt()) + lambda * v * t.sqrt(); - - let x1: f64 = (S / X).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); - let x2: f64 = (S / H).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); - - let y1: f64 = (H * H / (S * X)).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); - let y2: f64 = (H / S).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); - - let norm = Gaussian::default(); - - // Common functions: - let A = |phi: f64| -> f64 { - let term1: f64 = phi * S * ((b - r) * t).exp() * norm.cdf(phi * x1); - let term2: f64 = phi * X * (-r * t).exp() * norm.cdf(phi * x1 - phi * v * (t).sqrt()); - term1 - term2 - }; - - let B = |phi: f64| -> f64 { - let term1: f64 = phi * S * ((b - r) * t).exp() * norm.cdf(phi * x2); - let term2: f64 = phi * X * (-r * t).exp() * norm.cdf(phi * x2 - phi * v * (t).sqrt()); - term1 - term2 - }; - - let C = |phi: f64, eta: f64| -> f64 { - let term1: f64 = - phi * S * ((b - r) * t).exp() * (H / S).powf(2. * (mu + 1.)) * norm.cdf(eta * y1); - let term2: f64 = phi - * X - * (-r * t).exp() - * (H / S).powf(2. * mu) - * norm.cdf(eta * y1 - eta * v * t.sqrt()); - term1 - term2 - }; - - let D = |phi: f64, eta: f64| -> f64 { - let term1: f64 = - phi * S * ((b - r) * t).exp() * (H / S).powf(2. * (mu + 1.)) * norm.cdf(eta * y2); - let term2: f64 = phi - * X - * (-r * t).exp() - * (H / S).powf(2. * mu) - * norm.cdf(eta * y2 - eta * v * (t).sqrt()); + /// The option contract. + pub contract: OptionContract, - term1 - term2 - }; + /// Barrier type (up-and-out, down-and-out, up-and-in, down-and-in). + pub barrier_type: BarrierType, - let E = |eta: f64| -> f64 { - let term1: f64 = norm.cdf(eta * x2 - eta * v * (t).sqrt()); - let term2: f64 = (H / S).powf(2. * mu) * norm.cdf(eta * y2 - eta * v * t.sqrt()); - - K * (-r * t).exp() * (term1 - term2) - }; - - let F = |eta: f64| -> f64 { - let term1: f64 = (H / S).powf(mu + lambda) * norm.cdf(eta * z); - let term2: f64 = - (H / S).powf(mu - lambda) * norm.cdf(eta * z - 2. * eta * lambda * v * t.sqrt()); - - K * (term1 + term2) - }; - - // Strike above barrier (X >= H): - if X >= H { - match type_flag { - // Knock-In calls: - BarrierType::CDI if S >= H => C(1., 1.) + E(1.), - BarrierType::CUI if S <= H => A(1.) + E(-1.), - // Knock-In puts: - BarrierType::PDI if S >= H => B(-1.) - C(-1., 1.) + D(-1., 1.) + E(1.), - BarrierType::PUI if S <= H => A(-1.) - B(-1.) + D(-1., -1.) + E(-1.), - // Knock-Out calls: - BarrierType::CDO if S >= H => A(1.) - C(1., 1.) + F(1.), - BarrierType::CUO if S <= H => F(-1.), - // Knock-Out puts: - BarrierType::PDO if S >= H => A(-1.) - B(-1.) + C(-1., 1.) - D(-1., 1.) + F(1.), - BarrierType::PUO if S <= H => B(-1.) - D(-1., -1.) + F(-1.), - - _ => panic!("Barrier touched - check barrier and type flag."), - } - } - // Strike below barrier (X < H): - else { - match type_flag { - // Knock-In calls: - BarrierType::CDI if S >= H => A(1.) - B(1.) + D(1., 1.) + E(1.), - BarrierType::CUI if S <= H => B(1.) - C(1., -1.) + D(1., -1.) + E(-1.), - // Knock-In puts: - BarrierType::PDI if S >= H => A(-1.) + E(1.), - BarrierType::PUI if S <= H => C(-1., -1.) + E(-1.), - // Knock-Out calls: - BarrierType::CDO if S >= H => B(1.) - D(1., 1.) + F(1.), - BarrierType::CUO if S <= H => A(1.) - B(1.) + C(1., -1.) - D(1., -1.) + F(-1.), - // Knock-Out puts: - BarrierType::PDO if S >= H => F(1.), - BarrierType::PUO if S <= H => A(-1.) - C(-1., -1.) + F(-1.), - - _ => panic!("Barrier touched - check barrier and type flag."), - } - } - } -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// TESTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#[cfg(test)] -mod tests { - use super::*; - use crate::assert_approx_equal; - use crate::RUSTQUANT_EPSILON; - - // use std::f64::EPSILON as EPS; - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Initial underlying price ABOVE the barrier. - // - // If S > H, then: - // - "down-in" and "down-out" options have a defined price. - // - "up-in" and "up-out" options make no sense. - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - static S_ABOVE_H: BarrierOption = BarrierOption { - initial_price: 110.0, - strike_price: 100.0, - barrier: 105.0, - time_to_expiry: 1.0, - risk_free_rate: 0.05, - volatility: 0.2, - rebate: 0.0, - dividend_yield: 0.01, - }; - - #[allow(clippy::similar_names)] - #[test] - fn test_S_above_H() { - let cdi = S_ABOVE_H.price(BarrierType::CDI); - let cdo = S_ABOVE_H.price(BarrierType::CDO); - let pdi = S_ABOVE_H.price(BarrierType::PDI); - let pdo = S_ABOVE_H.price(BarrierType::PDO); - - assert_approx_equal!(cdi, 9.504_815_211_050_698, RUSTQUANT_EPSILON); - assert_approx_equal!(cdo, 7.295_021_649_666_765, RUSTQUANT_EPSILON); - assert_approx_equal!(pdi, 3.017_297_598_380_377_4, RUSTQUANT_EPSILON); - assert_approx_equal!(pdo, 0.000_000, RUSTQUANT_EPSILON); - } - - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn cui_panic() { - let _ = S_ABOVE_H.price(BarrierType::CUI); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn cuo_panic() { - let _ = S_ABOVE_H.price(BarrierType::CUO); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn pui_panic() { - let _ = S_ABOVE_H.price(BarrierType::PUI); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn puo_panic() { - let _ = S_ABOVE_H.price(BarrierType::PUO); - } - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Initial underlying price BELOW the barrier. - // - // If S < H, then: - // - "down-in" and "down-out" options make no sense. - // - "up-in" and "up-out" options have a defined price. - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - static S_BELOW_H: BarrierOption = BarrierOption { - initial_price: 90.0, - strike_price: 100.0, - barrier: 105.0, - time_to_expiry: 1.0, - risk_free_rate: 0.05, - volatility: 0.2, - rebate: 0.0, - dividend_yield: 0.01, - }; - - #[allow(clippy::similar_names)] - #[test] - fn test_S_below_H() { - let cui = S_BELOW_H.price(BarrierType::CUI); - let cuo = S_BELOW_H.price(BarrierType::CUO); - let pui = S_BELOW_H.price(BarrierType::PUI); - let puo = S_BELOW_H.price(BarrierType::PUO); + /// Barrier level. + pub barrier: f64, - assert_approx_equal!(cui, 4.692_603_355_387_815, RUSTQUANT_EPSILON); - assert_approx_equal!(cuo, 0.022_448_676_101_445_74, RUSTQUANT_EPSILON); - assert_approx_equal!(pui, 1.359_553_168_024_573_8, RUSTQUANT_EPSILON); - assert_approx_equal!(puo, 9.373_956_276_110_954, RUSTQUANT_EPSILON); - } + /// Strike price of the option. + pub strike: f64, - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn cdi_panic() { - let _ = S_BELOW_H.price(BarrierType::CDI); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn cdo_panic() { - let _ = S_BELOW_H.price(BarrierType::CDO); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn pdi_panic() { - let _ = S_BELOW_H.price(BarrierType::PDI); - } - #[test] - #[should_panic(expected = "Barrier touched - check barrier and type flag.")] - fn pdo_panic() { - let _ = S_BELOW_H.price(BarrierType::PDO); - } + /// Rebate amount. + pub rebate: Option, } diff --git a/src/instruments/options/binary.rs b/src/instruments/options/binary.rs index cee2e2d4..9bceed18 100644 --- a/src/instruments/options/binary.rs +++ b/src/instruments/options/binary.rs @@ -7,154 +7,44 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -//! This module contains various 'binary', or 'digital', option types. +/// Binary option. +#[derive(Debug, Clone)] +pub struct BinaryOption { + /// The option contract. + pub contract: OptionContract, -use crate::math::distributions::{gaussian::Gaussian, Distribution}; + /// Strike price of the option. + pub strike: f64, -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STRUCTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/// Gap option parameters. -#[derive(Debug, Clone, Copy)] -pub struct GapOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `K_1` - First strike price (barrier strike). - pub strike_1: f64, - /// `K_2` - Second strike price (payoff strike). - pub strike_2: f64, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, - /// `v` - Volatility parameter. - pub volatility: f64, - /// `b` - Cost-of-carry. - pub cost_of_carry: f64, - /// `T` - Time to expiry/maturity. - pub time_to_maturity: f64, -} - -/// Cash-or-Nothing option parameters. -#[derive(Debug, Clone, Copy)] -pub struct CashOrNothingOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `X` - Strike price. - pub strike_price: f64, - /// `K` - Cash payout amount. - pub payout_value: f64, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, - /// `v` - Volatility parameter. - pub volatility: f64, - /// `b` - Cost-of-carry. - pub cost_of_carry: f64, - /// `T` - Time to expiry/maturity. - pub time_to_maturity: f64, + /// Type of binary option. + pub binary_type: BinaryType, } -// pub struct AssetOrNothingOption {} -// pub struct SupershareOption {} -// pub struct BinaryBarrierOption {} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// IMPLEMENTATIONS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -impl GapOption { - /// Gap option pricer. - /// The payoff from a call is $0$ if $S < K_1$ and $S — K_2$ if $S > K_1$. - /// Similarly, the payoff from a put is $0$ if $S > K_1$ and $K_2 — S$ if $S < K_1$. - #[must_use] - pub fn price(&self) -> (f64, f64) { - let S = self.initial_price; - let K_1 = self.strike_1; - let K_2 = self.strike_2; - let T = self.time_to_maturity; - let r = self.risk_free_rate; - let v = self.volatility; - let b = self.cost_of_carry; - - let d1 = ((S / K_1).ln() + (b + 0.5 * v * v) * T) / (v * (T).sqrt()); - let d2 = d1 - v * (T).sqrt(); - - let N = Gaussian::default(); - - let c = S * ((b - r) * T).exp() * N.cdf(d1) - K_2 * (-r * T).exp() * N.cdf(d2); - let p = -S * ((b - r) * T).exp() * N.cdf(-d1) + K_2 * (-r * T).exp() * N.cdf(-d2); - - (c, p) - } -} - -impl CashOrNothingOption { - /// Cah-or-Nothing option pricer. - /// The payoff from a call is 0 if S < X and K if S > X. - /// The payoff from a put is 0 if S > X and K if S < X. - #[must_use] - pub fn price(&self) -> (f64, f64) { - let S = self.initial_price; - let X = self.strike_price; - let K = self.payout_value; - let T = self.time_to_maturity; - let r = self.risk_free_rate; - let v = self.volatility; - let b = self.cost_of_carry; - - let d = ((S / X).ln() + (b - 0.5 * v * v) * T) / (v * (T).sqrt()); - - let N = Gaussian::default(); - - let c = K * (-r * T).exp() * N.cdf(d); - let p = K * (-r * T).exp() * N.cdf(-d); - - (c, p) - } -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// TESTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#[cfg(test)] -mod tests { - use super::*; - use crate::assert_approx_equal; - use crate::RUSTQUANT_EPSILON; - - #[test] - fn test_gap_option() { - let gap = GapOption { - initial_price: 50.0, - strike_1: 50.0, - strike_2: 57.0, - risk_free_rate: 0.09, - volatility: 0.2, - time_to_maturity: 0.5, - cost_of_carry: 0.09, - }; - - let prices = gap.price(); - - // Value from Haug's book (note: gap option payoffs can be negative). - assert_approx_equal!(prices.0, -0.005_252_489_258_779_747, RUSTQUANT_EPSILON); - } - - #[test] - fn test_cash_or_nothing_option() { - let CON = CashOrNothingOption { - initial_price: 100.0, - payout_value: 10.0, - strike_price: 80.0, - risk_free_rate: 0.06, - volatility: 0.35, - time_to_maturity: 0.75, - cost_of_carry: 0.0, - }; - - let prices = CON.price(); - - // Value from Haug's book. - assert_approx_equal!(prices.1, 2.671_045_684_461_347, RUSTQUANT_EPSILON); +impl Payoff for BinaryOption { + type Underlying = f64; + + fn payoff(&self, underlying: Self::Underlying) -> f64 { + match self.binary_type { + BinaryType::CashOrNothing => match self.contract.type_flag { + TypeFlag::Call => match underlying > self.strike { + true => self.strike, + false => 0.0, + }, + TypeFlag::Put => match underlying < self.strike { + true => self.strike, + false => 0.0, + }, + }, + BinaryType::AssetOrNothing => match self.contract.type_flag { + TypeFlag::Call => match underlying > self.strike { + true => underlying, + false => 0.0, + }, + TypeFlag::Put => match underlying < self.strike { + true => underlying, + false => 0.0, + }, + }, + } } } diff --git a/src/instruments/options/finite_difference_pricer.rs b/src/instruments/options/finite_difference_pricer.rs index fa83fee0..c2a8e5c0 100644 --- a/src/instruments/options/finite_difference_pricer.rs +++ b/src/instruments/options/finite_difference_pricer.rs @@ -7,7 +7,7 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -use crate::instruments::options::option::{ExerciseFlag, TypeFlag}; +use super::option_flags::*; use crate::time::{today, DayCountConvention}; use std::cmp::Ordering; use time::Date; @@ -84,11 +84,11 @@ impl FiniteDifferencePricer { } fn tridiagonal_matrix_multiply_vector( - &self, - sub_diagonal: f64, - diagonal: f64, - super_diagonal: f64, - v: Vec + &self, + sub_diagonal: f64, + diagonal: f64, + super_diagonal: f64, + v: Vec, ) -> Vec { let mut Av: Vec = Vec::new(); @@ -97,7 +97,7 @@ impl FiniteDifferencePricer { for i in 1..(v.len() - 1) { Av.push(sub_diagonal * v[i - 1] + diagonal * v[i] + super_diagonal * v[i + 1]) } - + Av.push(sub_diagonal * v[v.len() - 2] + diagonal * v[v.len() - 1]); Av @@ -118,22 +118,21 @@ impl FiniteDifferencePricer { Av } - fn invert_tridiagonal_matrix(&self, sub_diagonal: f64, diagonal: f64, super_diagonal: f64) -> Vec> { + fn invert_tridiagonal_matrix( + &self, + sub_diagonal: f64, + diagonal: f64, + super_diagonal: f64, + ) -> Vec> { let mut theta: Vec = Vec::new(); let system_size: usize = (self.price_steps - 1) as usize; theta.push(1.0); theta.push(diagonal); - theta.push( - diagonal * diagonal - - super_diagonal * sub_diagonal, - ); + theta.push(diagonal * diagonal - super_diagonal * sub_diagonal); for i in 2..system_size { - theta.push( - diagonal * theta[i] - - super_diagonal * sub_diagonal * theta[i - 1] - ) + theta.push(diagonal * theta[i] - super_diagonal * sub_diagonal * theta[i - 1]) } let mut phi: Vec = Vec::new(); @@ -141,10 +140,7 @@ impl FiniteDifferencePricer { phi.push(diagonal); for i in 1..(system_size) { - phi.push( - diagonal * phi[i] - - super_diagonal * sub_diagonal * phi[i - 1] - ) + phi.push(diagonal * phi[i] - super_diagonal * sub_diagonal * phi[i - 1]) } let theta_n = theta.pop().unwrap(); @@ -157,14 +153,14 @@ impl FiniteDifferencePricer { for i in 0..system_size { for j in 0..system_size { - value = (- 1.0_f64).powi((i + j) as i32); + value = (-1.0_f64).powi((i + j) as i32); match i.cmp(&j) { Ordering::Less => { for k in i..j { value *= match k { - k if k == system_size - 1 => {diagonal}, - _ => super_diagonal + k if k == system_size - 1 => diagonal, + _ => super_diagonal, } } value *= theta[i] * phi[j] / theta_n; @@ -197,30 +193,25 @@ impl FiniteDifferencePricer { (1..self.price_steps) .map(|i: u32| { v[(i - 1) as usize].max( - f64::exp(self.risk_free_rate * tau) * self.payoff( - f64::exp( - x_min + (i as f64) * delta_x - ) - ) + f64::exp(self.risk_free_rate * tau) + * self.payoff(f64::exp(x_min + (i as f64) * delta_x)), ) }) - .collect() + .collect() } fn initial_condition(&self, x_min: f64, delta_x: f64) -> Vec { (1..self.price_steps) - .map(|i: u32| self.payoff( - f64::exp( - x_min + (i as f64) * delta_x))) + .map(|i: u32| self.payoff(f64::exp(x_min + (i as f64) * delta_x))) .collect() } fn call_boundary(&self, tau: f64, x_max: f64) -> f64 { - f64::exp(x_max) - self.strike_price * f64::exp(- self.risk_free_rate * tau) + f64::exp(x_max) - self.strike_price * f64::exp(-self.risk_free_rate * tau) } fn put_boundary(&self, tau: f64, x_min: f64) -> f64 { - self.strike_price * f64::exp(- self.risk_free_rate * tau) - f64::exp(x_min) + self.strike_price * f64::exp(-self.risk_free_rate * tau) - f64::exp(x_min) } fn year_fraction(&self) -> f64 { @@ -245,13 +236,17 @@ impl FiniteDifferencePricer { let T: f64 = self.year_fraction(); let delta_t: f64 = T / (self.time_steps as f64); let x_min: f64 = self.initial_price.ln() - 5.0 * self.volatility * T.sqrt(); - let delta_x: f64 = (self.initial_price.ln() + 5.0 * self.volatility * T.sqrt() - x_min) / self.price_steps as f64; - + let delta_x: f64 = (self.initial_price.ln() + 5.0 * self.volatility * T.sqrt() - x_min) + / self.price_steps as f64; + (T, delta_t, delta_x, x_min) } fn coefficients(&self, delta_t: f64, delta_x: f64) -> (f64, f64) { - (0.5 * delta_t * self.volatility.powi(2) / delta_x.powi(2), delta_t * (self.risk_free_rate - 0.5 * self.volatility.powi(2)) / (2.0 * delta_x)) + ( + 0.5 * delta_t * self.volatility.powi(2) / delta_x.powi(2), + delta_t * (self.risk_free_rate - 0.5 * self.volatility.powi(2)) / (2.0 * delta_x), + ) } /// Explicit method @@ -269,22 +264,27 @@ impl FiniteDifferencePricer { match self.type_flag { TypeFlag::Call => { - v[(self.price_steps - 2) as usize] += super_diagonal * self.call_boundary( - (t as f64) * delta_t, - self.initial_price.ln() + 5.0 * self.volatility * T.sqrt() - ); + v[(self.price_steps - 2) as usize] += super_diagonal + * self.call_boundary( + (t as f64) * delta_t, + self.initial_price.ln() + 5.0 * self.volatility * T.sqrt(), + ); } TypeFlag::Put => { v[0] += sub_diagonal * self.put_boundary((t as f64) * delta_t, x_min); } } - if let ExerciseFlag::American = self.exercise_flag { + if let ExerciseFlag::American { + start: Date::MIN, + end: Date::MAX, + } = self.exercise_flag + { v = self.american_time_stop_step(v, (t as f64) * delta_t, x_min, delta_x); } } - f64::exp(- self.risk_free_rate * T) * self.return_price(v) + f64::exp(-self.risk_free_rate * T) * self.return_price(v) } ///Implicit method @@ -292,35 +292,37 @@ impl FiniteDifferencePricer { let (T, delta_t, delta_x, x_min) = self.grid(); let (x, y) = self.coefficients(delta_t, delta_x); - let inverse_matrix: Vec> = self.invert_tridiagonal_matrix( - - x + y, - 1.0 + 2.0 * x, - - x - y - ); + let inverse_matrix: Vec> = + self.invert_tridiagonal_matrix(-x + y, 1.0 + 2.0 * x, -x - y); let mut v: Vec = self.initial_condition(x_min, delta_x); for t in 1..(self.time_steps + 1) { match self.type_flag { TypeFlag::Call => { - v[(self.price_steps - 2) as usize] -= (- x - y) * self.call_boundary( - (t as f64) * delta_t, - self.initial_price.ln() + 5.0 * self.volatility * T.sqrt() - ); + v[(self.price_steps - 2) as usize] -= (-x - y) + * self.call_boundary( + (t as f64) * delta_t, + self.initial_price.ln() + 5.0 * self.volatility * T.sqrt(), + ); } TypeFlag::Put => { - v[0] -= (- x + y) * self.put_boundary((t as f64) * delta_t, x_min); + v[0] -= (-x + y) * self.put_boundary((t as f64) * delta_t, x_min); } } v = self.general_matrix_multiply_vector(&inverse_matrix, v); - if let ExerciseFlag::American = self.exercise_flag { + if let ExerciseFlag::American { + start: Date::MIN, + end: Date::MAX, + } = self.exercise_flag + { v = self.american_time_stop_step(v, (t as f64) * delta_t, x_min, delta_x); } } - f64::exp(- self.risk_free_rate * T) * self.return_price(v) + f64::exp(-self.risk_free_rate * T) * self.return_price(v) } /// Crank-Nicolson method @@ -331,29 +333,22 @@ impl FiniteDifferencePricer { let diagonal: f64 = 1.0 - x; let super_diagonal: f64 = 0.5 * (x + y); - let inverse_future_matrix = self.invert_tridiagonal_matrix( - - sub_diagonal, - 1.0 + x, - - super_diagonal - ); + let inverse_future_matrix = + self.invert_tridiagonal_matrix(-sub_diagonal, 1.0 + x, -super_diagonal); let mut v: Vec = self.initial_condition(x_min, delta_x); for t in 1..(self.time_steps + 1) { - v = self.tridiagonal_matrix_multiply_vector( - sub_diagonal, - diagonal, - super_diagonal, - v - ); + v = self.tridiagonal_matrix_multiply_vector(sub_diagonal, diagonal, super_diagonal, v); match self.type_flag { TypeFlag::Call => { - v[(self.price_steps - 2) as usize] += - 2.0 * super_diagonal * self.call_boundary( - (t as f64) * delta_t, - self.initial_price.ln() + 5.0 * self.volatility * T.sqrt() - ); + v[(self.price_steps - 2) as usize] += 2.0 + * super_diagonal + * self.call_boundary( + (t as f64) * delta_t, + self.initial_price.ln() + 5.0 * self.volatility * T.sqrt(), + ); } TypeFlag::Put => { v[0] += 2.0 * sub_diagonal * self.put_boundary((t as f64) * delta_t, x_min); @@ -362,12 +357,16 @@ impl FiniteDifferencePricer { v = self.general_matrix_multiply_vector(&inverse_future_matrix, v); - if let ExerciseFlag::American = self.exercise_flag { + if let ExerciseFlag::American { + start: Date::MIN, + end: Date::MAX, + } = self.exercise_flag + { v = self.american_time_stop_step(v, (t as f64) * delta_t, x_min, delta_x); } } - f64::exp(- self.risk_free_rate * T) * self.return_price(v) + f64::exp(-self.risk_free_rate * T) * self.return_price(v) } } @@ -392,7 +391,9 @@ mod tests_finite_difference_pricer_at_the_money { time_steps: 10000, price_steps: 250, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const EUROPEAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -405,7 +406,9 @@ mod tests_finite_difference_pricer_at_the_money { time_steps: 10000, price_steps: 250, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const AMERICAN_CALL: FiniteDifferencePricer = FiniteDifferencePricer { @@ -418,7 +421,10 @@ mod tests_finite_difference_pricer_at_the_money { time_steps: 10000, price_steps: 250, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const AMERICAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -431,7 +437,10 @@ mod tests_finite_difference_pricer_at_the_money { time_steps: 10000, price_steps: 250, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const EXPECT_A_CALL: f64 = 0.680_478_009_892_241; @@ -521,7 +530,9 @@ mod tests_finite_difference_pricer_in_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const EUROPEAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -534,7 +545,9 @@ mod tests_finite_difference_pricer_in_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const AMERICAN_CALL: FiniteDifferencePricer = FiniteDifferencePricer { @@ -547,7 +560,10 @@ mod tests_finite_difference_pricer_in_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const AMERICAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -560,7 +576,10 @@ mod tests_finite_difference_pricer_in_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const EXPECT_A_CALL: f64 = 5.487_706_388_002_172; @@ -650,7 +669,9 @@ mod tests_finite_difference_pricer_out_of_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const EUROPEAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -663,7 +684,9 @@ mod tests_finite_difference_pricer_out_of_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::European, + exercise_flag: ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }, }; const AMERICAN_CALL: FiniteDifferencePricer = FiniteDifferencePricer { @@ -676,7 +699,10 @@ mod tests_finite_difference_pricer_out_of_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Call, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const AMERICAN_PUT: FiniteDifferencePricer = FiniteDifferencePricer { @@ -689,7 +715,10 @@ mod tests_finite_difference_pricer_out_of_the_money { time_steps: 10000, price_steps: 200, type_flag: TypeFlag::Put, - exercise_flag: ExerciseFlag::American, + exercise_flag: ExerciseFlag::American { + start: date!(2024 - 01 - 01), + end: date!(2025 - 01 - 01), + }, }; const EXPECT_A_CALL: f64 = 0.000_059_393_327_777_911; diff --git a/src/instruments/options/forward_start.rs b/src/instruments/options/forward_start.rs index 34654568..8de2d1f1 100644 --- a/src/instruments/options/forward_start.rs +++ b/src/instruments/options/forward_start.rs @@ -7,122 +7,15 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// FORWARD START OPTION STRUCT -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -use time::Date; - -use crate::{ - math::distributions::{Distribution, Gaussian}, - time::{today, DayCountConvention}, -}; - -/// Forward Start Option parameters struct -#[allow(clippy::module_name_repetitions)] -#[derive(derive_builder::Builder, Debug)] +/// Forward start option. +#[derive(Debug, Clone)] pub struct ForwardStartOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `alpha` - The proportion of S to set the strike price. - /// Three possibilities: - /// - alpha < 1: call (put) will start (1 - alpha)% in-the-money (out-of-the-money). - /// - alpha = 1: the option starts at-the-money. - /// - alpha > 1: call (put) will start (alpha - 1)% out-of-the-money (in-the-money). - pub alpha: f64, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, - /// `v` - Volatility parameter. - pub volatility: f64, - /// `q` - Dividend rate. - pub dividend_rate: f64, - - /// `valuation_date` - Valuation date. - #[builder(default = "None")] - pub valuation_date: Option, - - /// `start` - Time until the start of the option (`T` in most literature). - pub start: Date, - /// `end` - Time until the end of the option (`t` in most literature). - pub end: Date, -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// FORWARD START OPTION IMPLEMENTATION -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -impl ForwardStartOption { - /// Rubinstein (1990) Forward Start Option Price formula. - /// Returns a tuple: `(call_price, put_price)` - /// # Note: - /// * `b = r - q` - The cost of carry. - #[must_use] - pub fn price(&self) -> (f64, f64) { - let S = self.initial_price; - let a = self.alpha; - - let r = self.risk_free_rate; - let v = self.volatility; - let q = self.dividend_rate; - - let T = DayCountConvention::default() - .day_count_factor(self.valuation_date.unwrap_or(today()), self.end); - - let t = DayCountConvention::default() - .day_count_factor(self.valuation_date.unwrap_or(today()), self.start); - - let b = r - q; - - let d1 = ((1. / a).ln() + (b + v * v / 2.) * (T - t)) / (v * (T - t).sqrt()); - let d2 = d1 - v * (T - t).sqrt(); - - let norm = Gaussian::default(); - - let Nd1: f64 = norm.cdf(d1); - let Nd2: f64 = norm.cdf(d2); - - let Nd1_: f64 = norm.cdf(-d1); - let Nd2_: f64 = norm.cdf(-d2); - - let c: f64 = S - * ((b - r) * t).exp() - * (((b - r) * (T - t)).exp() * Nd1 - a * (-r * (T - t)).exp() * Nd2); - let p: f64 = S - * ((b - r) * t).exp() - * (-((b - r) * (T - t)).exp() * Nd1_ + a * (-r * (T - t)).exp() * Nd2_); - - (c, p) - } -} - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// TESTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#[cfg(test)] -mod tests_forward_start { - use super::*; - use crate::assert_approx_equal; - - #[test] - fn TEST_forward_start_option() { - let start = today() + time::Duration::days(91); - let end = today() + time::Duration::days(365); - - let ForwardStart = ForwardStartOption { - initial_price: 60.0, - alpha: 1.1, - risk_free_rate: 0.08, - volatility: 0.3, - dividend_rate: 0.04, - valuation_date: None, - start, - end, - }; + /// The option contract. + pub contract: OptionContract, - let prices = ForwardStart.price(); + /// Strike price of the option. + pub strike: f64, - // Call price example from Haug's book. - assert_approx_equal!(prices.0, 4.402888269001168, 1e-2); - } + /// Forward start date. + pub start_date: Date, } diff --git a/src/instruments/options/gap.rs b/src/instruments/options/gap.rs new file mode 100644 index 00000000..3b917757 --- /dev/null +++ b/src/instruments/options/gap.rs @@ -0,0 +1,21 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Gap option. +#[derive(Debug, Clone)] +pub struct GapOption { + /// The option contract. + pub contract: OptionContract, + + /// First strike price (barrier strike). + pub strike_1: f64, + + /// Second strike price (payoff strike). + pub strike_2: f64, +} diff --git a/src/instruments/options/lookback.rs b/src/instruments/options/lookback.rs index 14731c6a..d6de49b7 100644 --- a/src/instruments/options/lookback.rs +++ b/src/instruments/options/lookback.rs @@ -40,29 +40,25 @@ pub enum LookbackStrike { #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone, Copy)] pub struct LookbackOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, + /// The option contract. + pub contract: OptionContract, + /// `K` - Strike price (only needed for fixed strike lookbacks). /// If the strike is floating, then this is `None`. pub strike_price: Option, - /// `v` - Volatility parameter. - pub volatility: f64, - /// `T` - Time to expiry/maturity. - pub time_to_maturity: f64, - /// `q` - dividend yield. - pub dividend_yield: f64, + /// Minimum value of the underlying price observed **so far**. /// If the contract starts at t=0, then `S_min = S_0`. /// Used for the closed-form put price. pub s_min: f64, + /// Maximum value of the underlying price observed **so far**. /// If the contract starts at t=0, then `S_max = S_0`. /// Used for the closed-form call price. pub s_max: f64, - /// Strike type. - pub strike_type: LookbackStrike, + + /// Start date of the contract. + pub start_date: Date, } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/instruments/options/mod.rs b/src/instruments/options/mod.rs index 9d753aae..58d70b98 100644 --- a/src/instruments/options/mod.rs +++ b/src/instruments/options/mod.rs @@ -8,49 +8,56 @@ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ pub use crate::instruments::options::{ - asian::*, bachelier::*, barrier::*, binary::*, binomial::*, black_scholes_merton::*, - forward_start::*, heston::*, implied_volatility::*, lookback::*, merton_jump_diffusion::*, - option::*, power::*, + black_scholes_merton::*, implied_volatility::*, merton_jump_diffusion::*, option_contract::*, }; -/// Asian option pricers. -pub mod asian; +// /// Asian option pricers. +// pub mod asian; -/// Bachelier option pricer. -pub mod bachelier; +// /// Bachelier option pricer. +// pub mod bachelier; -/// Barrier option pricers. -pub mod barrier; +// /// Barrier option pricers. +// pub mod barrier; -/// Binary option pricers. -pub mod binary; +// /// Binary option pricers. +// pub mod binary; -/// Binomial option pricers. -pub mod binomial; +// /// Binomial option pricers. +// pub mod binomial; /// Generalised Black-Scholes-Merton option pricer. pub mod black_scholes_merton; -/// Forward start options pricers. -pub mod forward_start; +// /// Forward start options pricers. +// pub mod forward_start; -/// Heston model option pricer. -pub mod heston; +// /// Heston model option pricer. +// pub mod heston; /// Implied volatility functions. pub mod implied_volatility; -/// Lookback option pricers. -pub mod lookback; +// /// Lookback option pricers. +// pub mod lookback; /// Merton (1976) jump diffusion model. pub mod merton_jump_diffusion; /// Base option traits. -pub mod option; +pub mod option_contract; +// pub use option_contract::*; -/// Power option pricers. -pub mod power; +// /// Power option pricers. +// pub mod power; /// Finite Difference Pricer pub mod finite_difference_pricer; + +/// Option flags. +pub mod option_flags; +pub use option_flags::*; + +/// Vanilla option pricers. +pub mod vanilla; +pub use vanilla::*; diff --git a/src/instruments/options/option.rs b/src/instruments/options/option.rs deleted file mode 100644 index 658582d8..00000000 --- a/src/instruments/options/option.rs +++ /dev/null @@ -1,279 +0,0 @@ -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// RustQuant: A Rust library for quantitative finance tools. -// Copyright (C) 2023 https://github.com/avhz -// Dual licensed under Apache 2.0 and MIT. -// See: -// - LICENSE-APACHE.md -// - LICENSE-MIT.md -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/// Option contract data. -pub struct OptionContract { - /// The option's type flag (call or put). - pub type_flag: TypeFlag, - - /// The option's strike type (fixed or floating). - pub strike_flag: StrikeFlag, - - /// The option's exercise type (European, American, Bermudan). - pub exercise_flag: ExerciseFlag, - - /// The option's settlement type (cash or physical). - pub settlement_flag: SettlementFlag, -} - -/// Option type enum. -#[derive(Debug, Clone, Copy)] -pub enum TypeFlag { - /// Call option (right to BUY the underlying asset). - Call = 1, - - /// Put option (right to SELL the underlying asset). - Put = -1, -} - -/// American/European option type enum. -#[derive(Debug, Clone, Copy)] -pub enum ExerciseFlag { - /// European option (can only be exercised at expiry). - European, - - /// American option (can be exercised at any time before expiry). - American, - - /// Bermudan option (can be exercised at specific dates before expiry). - Bermudan, -} - -/// Option strike type enum. -#[derive(Debug, Clone, Copy)] -pub enum StrikeFlag { - /// Strike is fixed. - Fixed, - - /// Strike is floating (e.g. strike = S_max). - Floating, -} - -/// Instrument settlement flag. -#[derive(Debug, Clone, Copy)] -pub enum SettlementFlag { - /// Cash settlement. - Cash, - - /// Physical settlement. - Physical, -} - -/// Generic option parameters struct. -/// Contains the common parameters (as in Black-Scholes). -/// Other option types may have additional parameters, -/// such as lookback options (S_min, S_max). -#[allow(clippy::module_name_repetitions)] -#[derive(Debug, Clone)] -pub struct OptionParameters { - /// `S` - Initial price of the underlying. - pub S: Vec, - /// `K` - Strike price. - pub K: Vec, - /// `T` - Time to expiry/maturity. - pub T: Vec, - /// `r` - Risk-free rate parameter. - pub r: Vec, - /// `v` - Volatility parameter. - pub v: Vec, - /// `q` - Dividend rate. - pub q: Vec, -} - -impl OptionParameters { - /// New option parameters struct initialiser. - #[must_use] - pub const fn new( - initial_price: Vec, - strike_price: Vec, - risk_free_rate: Vec, - volatility: Vec, - dividend_rate: Vec, - time_to_maturity: Vec, - ) -> Self { - Self { - S: initial_price, - K: strike_price, - T: time_to_maturity, - r: risk_free_rate, - v: volatility, - q: dividend_rate, - } - } -} - -#[allow(dead_code)] -trait Payoff { - fn payoff(&self, underlying: U, strike: S) -> f64; -} - -impl Payoff for OptionContract { - fn payoff(&self, underlying: f64, strike: f64) -> f64 { - match self.type_flag { - TypeFlag::Call => (underlying - strike).max(0.0), - TypeFlag::Put => (strike - underlying).max(0.0), - } - } -} - -impl Payoff, f64> for OptionContract { - fn payoff(&self, underlying: Vec, strike: f64) -> f64 { - let mut payoff = 0.0; - - for &spot in underlying.iter() { - payoff += match self.type_flag { - TypeFlag::Call => (spot - strike).max(0.0), - TypeFlag::Put => (strike - spot).max(0.0), - }; - } - - payoff / underlying.len() as f64 - } -} - -// trait Payoff { -// type Underlying; -// type Strike; - -// fn call_payoff(&self, underlying: Self::Underlying, strike: Self::Strike) -> f64; -// fn put_payoff(&self, underlying: Self::Underlying, strike: Self::Strike) -> f64; -// } - -// impl Payoff for OptionContract { -// type Underlying = f64; -// type Strike = f64; - -// #[inline] -// fn call_payoff(&self, underlying: f64, strike: f64) -> f64 { -// f64::max(underlying - strike, 0.0) -// } - -// #[inline] -// fn put_payoff(&self, underlying: f64, strike: f64) -> f64 { -// f64::max(strike - underlying, 0.0) -// } -// } - -// impl Payoff for OptionContract { -// type Underlying = Vec; -// type Strike = f64; - -// #[inline] -// fn call_payoff(&self, underlying: Vec, strike: f64) -> f64 { -// let mut payoff = 0.0; -// for &spot in underlying.iter() { -// payoff += f64::max(spot - strike, 0.0); -// } -// payoff / underlying.len() as f64 -// } - -// #[inline] -// fn put_payoff(&self, underlying: Vec, strike: f64) -> f64 { -// let mut payoff = 0.0; -// for &spot in underlying.iter() { -// payoff += f64::max(strike - spot, 0.0); -// } -// payoff / underlying.len() as f64 -// } -// } - -// trait Payoff { -// fn call_payoff(&self, underlying: UNDERLYING, strike: STRIKE) -> f64; -// fn put_payoff(&self, underlying: UNDERLYING, strike: STRIKE) -> f64; -// } - -// impl Payoff for f64 { -// fn call_payoff(&self, underlying: f64, strike: f64) -> f64 { -// (underlying - strike).max(0.0) -// } - -// fn put_payoff(&self, underlying: f64, strike: f64) -> f64 { -// (strike - underlying).max(0.0) -// } -// } - -// impl Payoff, f64> for Vec { -// fn call_payoff(&self, underlying: Vec, strike: f64) -> f64 { -// let mut payoff = 0.0; -// for (i, &spot) in underlying.iter().enumerate() { -// payoff += (spot - strike).max(0.0); -// } -// payoff / underlying.len() as f64 -// } - -// fn put_payoff(&self, underlying: Vec, strike: f64) -> f64 { -// let mut payoff = 0.0; -// for (i, &spot) in underlying.iter().enumerate() { -// payoff += (strike - spot).max(0.0); -// } -// payoff / underlying.len() as f64 -// } -// } - -// impl Payoff for f64 { -// fn payoff(&self, spot: f64) -> f64 { -// self.max(spot) -// } -// } - -// impl Payoff> for Vec< { -// fn payoff(&self, spot: Decimal) -> Decimal { -// self.max(spot) -// } -// } - -// pub trait PathIndependentOption { -// fn price(&self) -> f64; -// } - -// /// Path-dependent option trait. -// pub trait PathDependentOption { -// /// Base method for path-dependent call option payoff. -// fn call_payoff(&self, path: &[f64]) -> f64; - -// /// Base method for path-dependent put option payoff. -// fn put_payoff(&self, path: &[f64]) -> f64; - -// /// Base method for path-dependent option prices using closed-form solution (call and put). -// fn closed_form_prices(&self) -> (f64, f64); - -// /// Base method for path-dependent option prices using Monte Carlo (call and put). -// fn monte_carlo_prices(&self, n_steps: usize, n_sims: usize, parallel: bool) -> (f64, f64); -// } - -// /// General option trait. -// /// All option types must implement this trait. -// /// All option contracts have: -// /// - `Prices` struct to store the option prices (call, put). -// /// - `Parameters` struct to store the option parameters (S, K, T, etc...). -// /// - `TypeFlag` enum to store the option type (call, put). -// /// - `Greeks` struct to store the option Greeks (sensitivities). -// /// - `prices` method to compute the option prices (call, put). -// /// - `set_parameters` method to set the option parameters. -// /// - `option_type` method to set the option type. -// /// - `greeks` method to compute the option Greeks (sensitivities). -// pub trait OptionContract { -// /// Option prices struct. -// type Prices; -// /// Option parameters struct. -// type Parameters; -// /// Option type enum (call or put). -// type Type; -// /// Option Greeks struct. -// type Greeks; - -// /// Base method for computing the options prices (call and put). -// fn prices(&self) -> Self::Prices; -// /// Base method for setting the option parameters. -// fn set_parameters(&self) -> Self::Parameters; -// /// Base method for setting the option type. -// fn option_type(&self) -> Self::Type; -// /// Base method for computing the Greeks (sensitivities). -// fn greeks(&self) -> Self::Greeks; -// } diff --git a/src/instruments/options/option_contract.rs b/src/instruments/options/option_contract.rs new file mode 100644 index 00000000..1f19a006 --- /dev/null +++ b/src/instruments/options/option_contract.rs @@ -0,0 +1,29 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use super::option_flags::*; +use derive_builder::Builder; + +/// Option contract data. +#[derive(Debug, Clone, Builder)] +pub struct OptionContract { + /// Mandatory: Option type (call or put). + pub type_flag: TypeFlag, + + /// Mandatory: Exercise type (European, American, Bermudan). + pub exercise_flag: ExerciseFlag, + + /// Optional: Strike type (fixed or floating). + #[builder(default)] + pub strike_flag: Option, + + /// Optional: Settlement type (cash or physical). + #[builder(default)] + pub settlement_flag: Option, +} diff --git a/src/instruments/options/option_exercise.rs b/src/instruments/options/option_exercise.rs new file mode 100644 index 00000000..a2bca2fc --- /dev/null +++ b/src/instruments/options/option_exercise.rs @@ -0,0 +1,29 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// European exercise type. +pub struct EuropeanExercise { + /// The expiry date of the option. + pub expiry: Date, +} + +/// American exercise type. +pub struct AmericanExercise { + /// Initial date of the option. + pub start: Date, + + /// The terminal date of the option. + pub end: Date, +} + +/// Bermudan exercise type. +pub struct BermudanExercise { + /// The exercise dates of the option. + pub exercise_dates: Vec, +} diff --git a/src/instruments/options/option_flags.rs b/src/instruments/options/option_flags.rs new file mode 100644 index 00000000..7b0fb6c7 --- /dev/null +++ b/src/instruments/options/option_flags.rs @@ -0,0 +1,114 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use time::Date; + +/// Option type enum. +#[derive(Debug, Clone, Copy)] +pub enum TypeFlag { + /// Call option (right to BUY the underlying asset). + Call = 1, + + /// Put option (right to SELL the underlying asset). + Put = -1, +} + +/// American/European option type enum. +#[derive(Debug, Clone)] +pub enum ExerciseFlag { + /// European option (can only be exercised at expiry). + /// Most index options are European. + European { + /// The expiry date of the option. + expiry: Date, + }, + + /// American option (can be exercised at any time before expiry). + /// Most stock options are American. + American { + /// Initial date of the option. + start: Date, + /// The terminal date of the option. + end: Date, + }, + + /// Bermudan option (can be exercised at specific dates before expiry). + /// Bermudan options are a hybrid of American and European options, + /// hence the name. These are relatively rare and typically used + /// in OTC markets. + Bermudan { + /// The exercise dates of the option. + exercise_dates: Vec, + }, +} + +/// Option strike type enum. +/// +/// These are used for options such as +/// Asian options (average) or Lookback options (extreme). +#[derive(Debug, Clone, Copy)] +pub enum StrikeFlag { + /// Strike is fixed. + Fixed, + + /// Strike is floating (e.g. strike = S_max). + Floating, +} + +/// Instrument settlement flag. +#[derive(Debug, Clone, Copy)] +pub enum SettlementFlag { + /// Cash settlement. + Cash, + + /// Physical settlement. + Physical, +} + +/// Method of averaging (arithmetic or geometric, and continuous or discrete). +#[derive(Debug, Clone, Copy)] +pub enum AveragingMethod { + /// Arithmetic Asian option with discrete averaging. + ArithmeticDiscrete, + + /// Arithmetic Asian option with continuous averaging. + ArithmeticContinuous, + + /// Geometric Asian option with discrete averaging. + GeometricDiscrete, + + /// Geometric Asian option with continuous averaging. + GeometricContinuous, +} + +/// Barrier type flag. +#[derive(Clone, Copy, Debug)] +pub enum BarrierType { + /// Up-and-out barrier option. + UpAndOut, + + /// Down-and-out barrier option. + DownAndOut, + + /// Up-and-in barrier option. + UpAndIn, + + /// Down-and-in barrier option. + DownAndIn, +} + +/// Binary type enum. +#[derive(Debug, Clone, Copy)] +pub enum BinaryType { + /// Asset-or-nothing binary option. + AssetOrNothing, + + /// Cash-or-nothing binary option. + CashOrNothing, +} diff --git a/src/instruments/options/power.rs b/src/instruments/options/power.rs index 9d2fd372..11a62c17 100644 --- a/src/instruments/options/power.rs +++ b/src/instruments/options/power.rs @@ -7,115 +7,44 @@ // - LICENSE-MIT.md // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -//! # Power Contracts -//! -//! Power contracts are options with the payoff: (S/K)^i -//! where i is the (fixed) power of the contract. - -use crate::time::{today, DayCountConvention}; -use time::Date; - -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STRUCTS, ENUMS, AND TRAITS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -/// Power Option contract. -#[allow(clippy::module_name_repetitions)] +/// Power Option. #[derive(Debug, Clone, Copy)] pub struct PowerOption { - /// `S` - Initial price of the underlying. - pub initial_price: f64, - /// `K` - Strike price. - pub strike_price: f64, - /// `i` - Power of the contract. - pub power: f64, + /// The option contract. + pub contract: OptionContract, - /// `r` - Risk-free rate parameter. - pub risk_free_rate: f64, - /// `b` - Cost of carry. - pub cost_of_carry: f64, - /// `v` - Volatility parameter. - pub volatility: f64, + /// Strike price of the option. + pub strike_price: f64, - /// `valuation_date` - Valuation date. - pub evaluation_date: Option, - /// `expiry_date` - Expiry date. - pub expiration_date: Date, + /// Power parameter. + pub power: f64, } -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// IMPLEMENTATIONS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -impl PowerOption { - /// New Power Option contract. - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn new( - initial_price: f64, - strike_price: f64, - power: f64, - risk_free_rate: f64, - cost_of_carry: f64, - volatility: f64, - evaluation_date: Option, - expiration_date: Date, - ) -> Self { - Self { - initial_price, - strike_price, - power, - risk_free_rate, - cost_of_carry, - volatility, - evaluation_date, - expiration_date, - } - } +/// Power Option. +#[derive(Debug, Clone, Copy)] +pub struct PowerContract { + /// Strike price of the option. + pub strike_price: f64, - /// Power Option price. - #[must_use] - pub fn price(&self) -> f64 { - let S = self.initial_price; - let K = self.strike_price; - let r = self.risk_free_rate; - let v = self.volatility; - let b = self.cost_of_carry; - let i = self.power; + /// Power parameter. + pub power: f64, +} - // Compute time to maturity. - let T = DayCountConvention::default().day_count_factor( - self.evaluation_date.unwrap_or(today()), - self.expiration_date, - ); +impl Payoff for PowerContract { + type Underlying = f64; - (S / K).powf(i) * (((b - 0.5 * v.powi(2)) * i - r + 0.5 * (i * v).powi(2)) * T).exp() + fn payoff(&self, underlying: Self::Underlying) -> f64 { + (underlying / self.strike_price).powf(self.power) } } -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// TESTS -// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -#[cfg(test)] -mod tests_power_contract { - use super::*; - use crate::{assert_approx_equal, RUSTQUANT_EPSILON}; - use time::Duration; +impl Payoff for PowerOption { + type Underlying = f64; - #[test] - fn test_power() { - let power_option = PowerOption { - initial_price: 400., - strike_price: 450., - power: 2., - risk_free_rate: 0.08, - cost_of_carry: 0.06, - volatility: 0.25, - evaluation_date: None, - expiration_date: today() + Duration::days(182), - }; - - assert_approx_equal!(power_option.price(), 0.83144001309052, RUSTQUANT_EPSILON); + fn payoff(&self, underlying: Self::Underlying) -> f64 { + match self.contract.type_flag { + TypeFlag::Call => (underlying.powf(self.power) - self.strike).max(0.0), + TypeFlag::Put => (self.strike - underlying.powf(self.power)).max(0.0), + } } } diff --git a/src/instruments/options/supershare.rs b/src/instruments/options/supershare.rs new file mode 100644 index 00000000..72a8efae --- /dev/null +++ b/src/instruments/options/supershare.rs @@ -0,0 +1,32 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Supershare option. +#[derive(Debug, Clone)] +pub struct SupershareOption { + /// The option contract. + pub contract: OptionContract, + + /// Lower strike price. + pub strike_1: f64, + + /// Upper strike price. + pub strike_2: f64, +} + +impl Payoff for SupershareOption { + type Underlying = f64; + + fn payoff(&self, underlying: Self::Underlying) -> f64 { + match (strike_1..=strike_2).contains(&underlying) { + true => underlying / strike_1, + false => 0.0, + } + } +} diff --git a/src/instruments/options/todo/asian.rs b/src/instruments/options/todo/asian.rs new file mode 100644 index 00000000..adb2b080 --- /dev/null +++ b/src/instruments/options/todo/asian.rs @@ -0,0 +1,150 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use crate::{ + math::distributions::{gaussian::Gaussian, Distribution}, + time::{today, DayCountConvention}, +}; +use time::Date; + +use super::OptionContract; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STRUCTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Type of Asian option (fixed or floating strike). +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Copy)] +pub enum AsianStrike { + /// Floating strike Asian option. + /// Payoffs: + /// - Call: `max(S_T - A, 0)` + /// - Put: `max(A - S_T, 0)` + Floating, + /// Fixed strike Asian option. + /// Payoffs: + /// - Call: `max(A - K, 0)` + /// - Put: `max(K - A, 0)` + Fixed, +} + +/// Asian Option struct. +#[allow(clippy::module_name_repetitions)] +#[derive(derive_builder::Builder, Debug, Clone, Copy)] +pub struct AsianOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `K` - Strike price. + pub strike_price: f64, + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `v` - Volatility parameter. + pub volatility: f64, + /// `q` - Dividend rate. + pub dividend_rate: f64, + + /// `evaluation_date` - Valuation date. + #[builder(default = "None")] + pub evaluation_date: Option, + + /// `expiry_date` - Expiry date. + pub expiration_date: Date, +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// IMPLEMENTATIONS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl AsianOption { + /// New Asian Option + #[must_use] + pub const fn new( + initial_price: f64, + strike_price: f64, + risk_free_rate: f64, + volatility: f64, + dividend_rate: f64, + evaluation_date: Option, + expiration_date: Date, + ) -> Self { + Self { + initial_price, + strike_price, + risk_free_rate, + volatility, + dividend_rate, + evaluation_date, + expiration_date, + } + } + + /// Geometric Continuous Average-Rate Price + #[must_use] + pub fn price_geometric_average(&self) -> (f64, f64) { + let S = self.initial_price; + let K = self.strike_price; + // let T = self.time_to_maturity; + let r = self.risk_free_rate; + let v = self.volatility; + let q = self.dividend_rate; + + // Compute time to maturity. + let T = DayCountConvention::default().day_count_factor( + self.evaluation_date.unwrap_or(today()), + self.expiration_date, + ); + + let v_a = v / 3_f64.sqrt(); + let b = r - q; + let b_a = 0.5 * (b - v * v / 6.0); + + let d1 = ((S / K).ln() + (b_a + 0.5 * v_a * v_a) * T) / (v_a * (T).sqrt()); + let d2 = d1 - v_a * (T).sqrt(); + + let N = Gaussian::default(); + + let c = S * ((b_a - r) * T).exp() * N.cdf(d1) - K * (-r * T).exp() * N.cdf(d2); + let p = -S * ((b_a - r) * T).exp() * N.cdf(-d1) + K * (-r * T).exp() * N.cdf(-d2); + + (c, p) + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests { + use time::Duration; + + use super::*; + use crate::assert_approx_equal; + + #[test] + fn test_asian_geometric() { + let expiry_date = today() + Duration::days(92); + + let AsianOption = AsianOption { + initial_price: 80.0, + strike_price: 85.0, + risk_free_rate: 0.05, + volatility: 0.2, + evaluation_date: None, + expiration_date: expiry_date, + dividend_rate: -0.03, + }; + + let prices = AsianOption.price_geometric_average(); + + // Value from Haug's book. + assert_approx_equal!(prices.1, 4.6922, 0.0001); + } +} diff --git a/src/instruments/options/bachelier.rs b/src/instruments/options/todo/bachelier.rs similarity index 100% rename from src/instruments/options/bachelier.rs rename to src/instruments/options/todo/bachelier.rs diff --git a/src/instruments/options/todo/barrier.rs b/src/instruments/options/todo/barrier.rs new file mode 100644 index 00000000..08855545 --- /dev/null +++ b/src/instruments/options/todo/barrier.rs @@ -0,0 +1,320 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use crate::math::distributions::{gaussian::Gaussian, Distribution}; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// BARRIER OPTION STRUCT +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Barrier Option struct for parameters and pricing methods. +#[derive(Debug, Clone, Copy)] +#[allow(clippy::module_name_repetitions)] +pub struct BarrierOption { + /// * `S` - Initial underlying price. + pub initial_price: f64, + /// * `X` - Strike price. + pub strike_price: f64, + /// * `H` - Barrier. + pub barrier: f64, + /// * `t` - Time to expiry. + pub time_to_expiry: f64, + /// * `r` - Risk-free rate. + pub risk_free_rate: f64, + /// * `v` - Volatility. + pub volatility: f64, + /// * `K` - Rebate (paid if the option is not able to be exercised). + pub rebate: f64, + /// * `q` - Dividend yield. + pub dividend_yield: f64, +} + +/// Barrier option type enum. +#[derive(Debug, Clone, Copy)] +#[allow(clippy::module_name_repetitions)] +pub enum BarrierType { + /// Call (up-and-in) + /// Payoff: `max(S_T - X, 0) * I(max(S_t) > H)` + CUI, + /// Call (down-and-in) + /// Payoff: `max(S_T - X, 0) * I(min(S_t) < H)` + CDI, + /// Call (up-and-out) + /// Payoff: `max(S_T - X, 0) * I(max(S_t) < H)` + CUO, + /// Call (down-and-out) + /// Payoff: `max(S_T - X, 0) * I(min(S_t) > H)` + CDO, + /// Put (up-and-in) + /// Payoff: `max(X - S_T, 0) * I(max(S_t) > H)` + PUI, + /// Put (down-and-in) + /// Payoff: `max(X - S_T, 0) * I(min(S_t) < H)` + PDI, + /// Put (up-and-out) + /// Payoff: `max(X - S_T, 0) * I(max(S_t) < H)` + PUO, + /// Put (down-and-out) + /// Payoff: `max(X - S_T, 0) * I(min(S_t) > H)` + PDO, +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// BARRIER OPTION IMPLEMENTATION +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl BarrierOption { + /// Closed-form solution for path-dependent barrier options. + /// + /// Adapted from Haug's *Complete Guide to Option Pricing Formulas*. + /// + /// # Arguments: + /// + /// * `type_flag` - One of: `cui`, `cuo`, `pui`, `puo`, `cdi`, `cdo`, `pdi`, `pdo`. + /// + /// # Note: + /// * `b = r - q` - The cost of carry. + #[must_use] + pub fn price(&self, type_flag: BarrierType) -> f64 { + let S = self.initial_price; + let X = self.strike_price; + let H = self.barrier; + let t = self.time_to_expiry; + let r = self.risk_free_rate; + let v = self.volatility; + let K = self.rebate; + let q = self.dividend_yield; + + let b: f64 = r - q; + + // Common terms: + let mu: f64 = (b - v * v / 2.) / (v * v); + let lambda: f64 = (mu * mu + 2. * r / (v * v)).sqrt(); + let z: f64 = (H / S).ln() / (v * t.sqrt()) + lambda * v * t.sqrt(); + + let x1: f64 = (S / X).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); + let x2: f64 = (S / H).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); + + let y1: f64 = (H * H / (S * X)).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); + let y2: f64 = (H / S).ln() / (v * t.sqrt()) + (1. + mu) * v * t.sqrt(); + + let norm = Gaussian::default(); + + // Common functions: + let A = |phi: f64| -> f64 { + let term1: f64 = phi * S * ((b - r) * t).exp() * norm.cdf(phi * x1); + let term2: f64 = phi * X * (-r * t).exp() * norm.cdf(phi * x1 - phi * v * (t).sqrt()); + term1 - term2 + }; + + let B = |phi: f64| -> f64 { + let term1: f64 = phi * S * ((b - r) * t).exp() * norm.cdf(phi * x2); + let term2: f64 = phi * X * (-r * t).exp() * norm.cdf(phi * x2 - phi * v * (t).sqrt()); + term1 - term2 + }; + + let C = |phi: f64, eta: f64| -> f64 { + let term1: f64 = + phi * S * ((b - r) * t).exp() * (H / S).powf(2. * (mu + 1.)) * norm.cdf(eta * y1); + let term2: f64 = phi + * X + * (-r * t).exp() + * (H / S).powf(2. * mu) + * norm.cdf(eta * y1 - eta * v * t.sqrt()); + term1 - term2 + }; + + let D = |phi: f64, eta: f64| -> f64 { + let term1: f64 = + phi * S * ((b - r) * t).exp() * (H / S).powf(2. * (mu + 1.)) * norm.cdf(eta * y2); + let term2: f64 = phi + * X + * (-r * t).exp() + * (H / S).powf(2. * mu) + * norm.cdf(eta * y2 - eta * v * (t).sqrt()); + + term1 - term2 + }; + + let E = |eta: f64| -> f64 { + let term1: f64 = norm.cdf(eta * x2 - eta * v * (t).sqrt()); + let term2: f64 = (H / S).powf(2. * mu) * norm.cdf(eta * y2 - eta * v * t.sqrt()); + + K * (-r * t).exp() * (term1 - term2) + }; + + let F = |eta: f64| -> f64 { + let term1: f64 = (H / S).powf(mu + lambda) * norm.cdf(eta * z); + let term2: f64 = + (H / S).powf(mu - lambda) * norm.cdf(eta * z - 2. * eta * lambda * v * t.sqrt()); + + K * (term1 + term2) + }; + + // Strike above barrier (X >= H): + if X >= H { + match type_flag { + // Knock-In calls: + BarrierType::CDI if S >= H => C(1., 1.) + E(1.), + BarrierType::CUI if S <= H => A(1.) + E(-1.), + // Knock-In puts: + BarrierType::PDI if S >= H => B(-1.) - C(-1., 1.) + D(-1., 1.) + E(1.), + BarrierType::PUI if S <= H => A(-1.) - B(-1.) + D(-1., -1.) + E(-1.), + // Knock-Out calls: + BarrierType::CDO if S >= H => A(1.) - C(1., 1.) + F(1.), + BarrierType::CUO if S <= H => F(-1.), + // Knock-Out puts: + BarrierType::PDO if S >= H => A(-1.) - B(-1.) + C(-1., 1.) - D(-1., 1.) + F(1.), + BarrierType::PUO if S <= H => B(-1.) - D(-1., -1.) + F(-1.), + + _ => panic!("Barrier touched - check barrier and type flag."), + } + } + // Strike below barrier (X < H): + else { + match type_flag { + // Knock-In calls: + BarrierType::CDI if S >= H => A(1.) - B(1.) + D(1., 1.) + E(1.), + BarrierType::CUI if S <= H => B(1.) - C(1., -1.) + D(1., -1.) + E(-1.), + // Knock-In puts: + BarrierType::PDI if S >= H => A(-1.) + E(1.), + BarrierType::PUI if S <= H => C(-1., -1.) + E(-1.), + // Knock-Out calls: + BarrierType::CDO if S >= H => B(1.) - D(1., 1.) + F(1.), + BarrierType::CUO if S <= H => A(1.) - B(1.) + C(1., -1.) - D(1., -1.) + F(-1.), + // Knock-Out puts: + BarrierType::PDO if S >= H => F(1.), + BarrierType::PUO if S <= H => A(-1.) - C(-1., -1.) + F(-1.), + + _ => panic!("Barrier touched - check barrier and type flag."), + } + } + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_approx_equal; + use crate::RUSTQUANT_EPSILON; + + // use std::f64::EPSILON as EPS; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Initial underlying price ABOVE the barrier. + // + // If S > H, then: + // - "down-in" and "down-out" options have a defined price. + // - "up-in" and "up-out" options make no sense. + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + static S_ABOVE_H: BarrierOption = BarrierOption { + initial_price: 110.0, + strike_price: 100.0, + barrier: 105.0, + time_to_expiry: 1.0, + risk_free_rate: 0.05, + volatility: 0.2, + rebate: 0.0, + dividend_yield: 0.01, + }; + + #[allow(clippy::similar_names)] + #[test] + fn test_S_above_H() { + let cdi = S_ABOVE_H.price(BarrierType::CDI); + let cdo = S_ABOVE_H.price(BarrierType::CDO); + let pdi = S_ABOVE_H.price(BarrierType::PDI); + let pdo = S_ABOVE_H.price(BarrierType::PDO); + + assert_approx_equal!(cdi, 9.504_815_211_050_698, RUSTQUANT_EPSILON); + assert_approx_equal!(cdo, 7.295_021_649_666_765, RUSTQUANT_EPSILON); + assert_approx_equal!(pdi, 3.017_297_598_380_377_4, RUSTQUANT_EPSILON); + assert_approx_equal!(pdo, 0.000_000, RUSTQUANT_EPSILON); + } + + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn cui_panic() { + let _ = S_ABOVE_H.price(BarrierType::CUI); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn cuo_panic() { + let _ = S_ABOVE_H.price(BarrierType::CUO); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn pui_panic() { + let _ = S_ABOVE_H.price(BarrierType::PUI); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn puo_panic() { + let _ = S_ABOVE_H.price(BarrierType::PUO); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Initial underlying price BELOW the barrier. + // + // If S < H, then: + // - "down-in" and "down-out" options make no sense. + // - "up-in" and "up-out" options have a defined price. + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + static S_BELOW_H: BarrierOption = BarrierOption { + initial_price: 90.0, + strike_price: 100.0, + barrier: 105.0, + time_to_expiry: 1.0, + risk_free_rate: 0.05, + volatility: 0.2, + rebate: 0.0, + dividend_yield: 0.01, + }; + + #[allow(clippy::similar_names)] + #[test] + fn test_S_below_H() { + let cui = S_BELOW_H.price(BarrierType::CUI); + let cuo = S_BELOW_H.price(BarrierType::CUO); + let pui = S_BELOW_H.price(BarrierType::PUI); + let puo = S_BELOW_H.price(BarrierType::PUO); + + assert_approx_equal!(cui, 4.692_603_355_387_815, RUSTQUANT_EPSILON); + assert_approx_equal!(cuo, 0.022_448_676_101_445_74, RUSTQUANT_EPSILON); + assert_approx_equal!(pui, 1.359_553_168_024_573_8, RUSTQUANT_EPSILON); + assert_approx_equal!(puo, 9.373_956_276_110_954, RUSTQUANT_EPSILON); + } + + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn cdi_panic() { + let _ = S_BELOW_H.price(BarrierType::CDI); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn cdo_panic() { + let _ = S_BELOW_H.price(BarrierType::CDO); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn pdi_panic() { + let _ = S_BELOW_H.price(BarrierType::PDI); + } + #[test] + #[should_panic(expected = "Barrier touched - check barrier and type flag.")] + fn pdo_panic() { + let _ = S_BELOW_H.price(BarrierType::PDO); + } +} diff --git a/src/instruments/options/todo/binary.rs b/src/instruments/options/todo/binary.rs new file mode 100644 index 00000000..cee2e2d4 --- /dev/null +++ b/src/instruments/options/todo/binary.rs @@ -0,0 +1,160 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! This module contains various 'binary', or 'digital', option types. + +use crate::math::distributions::{gaussian::Gaussian, Distribution}; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STRUCTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Gap option parameters. +#[derive(Debug, Clone, Copy)] +pub struct GapOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `K_1` - First strike price (barrier strike). + pub strike_1: f64, + /// `K_2` - Second strike price (payoff strike). + pub strike_2: f64, + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `v` - Volatility parameter. + pub volatility: f64, + /// `b` - Cost-of-carry. + pub cost_of_carry: f64, + /// `T` - Time to expiry/maturity. + pub time_to_maturity: f64, +} + +/// Cash-or-Nothing option parameters. +#[derive(Debug, Clone, Copy)] +pub struct CashOrNothingOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `X` - Strike price. + pub strike_price: f64, + /// `K` - Cash payout amount. + pub payout_value: f64, + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `v` - Volatility parameter. + pub volatility: f64, + /// `b` - Cost-of-carry. + pub cost_of_carry: f64, + /// `T` - Time to expiry/maturity. + pub time_to_maturity: f64, +} + +// pub struct AssetOrNothingOption {} +// pub struct SupershareOption {} +// pub struct BinaryBarrierOption {} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// IMPLEMENTATIONS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl GapOption { + /// Gap option pricer. + /// The payoff from a call is $0$ if $S < K_1$ and $S — K_2$ if $S > K_1$. + /// Similarly, the payoff from a put is $0$ if $S > K_1$ and $K_2 — S$ if $S < K_1$. + #[must_use] + pub fn price(&self) -> (f64, f64) { + let S = self.initial_price; + let K_1 = self.strike_1; + let K_2 = self.strike_2; + let T = self.time_to_maturity; + let r = self.risk_free_rate; + let v = self.volatility; + let b = self.cost_of_carry; + + let d1 = ((S / K_1).ln() + (b + 0.5 * v * v) * T) / (v * (T).sqrt()); + let d2 = d1 - v * (T).sqrt(); + + let N = Gaussian::default(); + + let c = S * ((b - r) * T).exp() * N.cdf(d1) - K_2 * (-r * T).exp() * N.cdf(d2); + let p = -S * ((b - r) * T).exp() * N.cdf(-d1) + K_2 * (-r * T).exp() * N.cdf(-d2); + + (c, p) + } +} + +impl CashOrNothingOption { + /// Cah-or-Nothing option pricer. + /// The payoff from a call is 0 if S < X and K if S > X. + /// The payoff from a put is 0 if S > X and K if S < X. + #[must_use] + pub fn price(&self) -> (f64, f64) { + let S = self.initial_price; + let X = self.strike_price; + let K = self.payout_value; + let T = self.time_to_maturity; + let r = self.risk_free_rate; + let v = self.volatility; + let b = self.cost_of_carry; + + let d = ((S / X).ln() + (b - 0.5 * v * v) * T) / (v * (T).sqrt()); + + let N = Gaussian::default(); + + let c = K * (-r * T).exp() * N.cdf(d); + let p = K * (-r * T).exp() * N.cdf(-d); + + (c, p) + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_approx_equal; + use crate::RUSTQUANT_EPSILON; + + #[test] + fn test_gap_option() { + let gap = GapOption { + initial_price: 50.0, + strike_1: 50.0, + strike_2: 57.0, + risk_free_rate: 0.09, + volatility: 0.2, + time_to_maturity: 0.5, + cost_of_carry: 0.09, + }; + + let prices = gap.price(); + + // Value from Haug's book (note: gap option payoffs can be negative). + assert_approx_equal!(prices.0, -0.005_252_489_258_779_747, RUSTQUANT_EPSILON); + } + + #[test] + fn test_cash_or_nothing_option() { + let CON = CashOrNothingOption { + initial_price: 100.0, + payout_value: 10.0, + strike_price: 80.0, + risk_free_rate: 0.06, + volatility: 0.35, + time_to_maturity: 0.75, + cost_of_carry: 0.0, + }; + + let prices = CON.price(); + + // Value from Haug's book. + assert_approx_equal!(prices.1, 2.671_045_684_461_347, RUSTQUANT_EPSILON); + } +} diff --git a/src/instruments/options/binomial.rs b/src/instruments/options/todo/binomial.rs similarity index 97% rename from src/instruments/options/binomial.rs rename to src/instruments/options/todo/binomial.rs index 41848c3e..ad37e817 100644 --- a/src/instruments/options/binomial.rs +++ b/src/instruments/options/todo/binomial.rs @@ -86,16 +86,16 @@ impl BinomialOption { for j in (0..n).rev() { for i in 0..=j { match ame_eur_flag { - ExerciseFlag::American => { + ExerciseFlag::American { .. } => { option_value[i] = (f64::from(z) * (S * u.powi(i as i32) * d.powi(j as i32 - i as i32) - K)) .max(Df * (p * (option_value[i + 1]) + (1.0 - p) * option_value[i])); } - ExerciseFlag::European => { + ExerciseFlag::European { .. } => { option_value[i] = Df * (p * (option_value[i + 1]) + (1.0 - p) * option_value[i]); } - ExerciseFlag::Bermudan => { + ExerciseFlag::Bermudan { .. } => { panic!("Bermudan option pricing not implemented yet."); } } diff --git a/src/instruments/options/todo/forward_start.rs b/src/instruments/options/todo/forward_start.rs new file mode 100644 index 00000000..34654568 --- /dev/null +++ b/src/instruments/options/todo/forward_start.rs @@ -0,0 +1,128 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// FORWARD START OPTION STRUCT +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use time::Date; + +use crate::{ + math::distributions::{Distribution, Gaussian}, + time::{today, DayCountConvention}, +}; + +/// Forward Start Option parameters struct +#[allow(clippy::module_name_repetitions)] +#[derive(derive_builder::Builder, Debug)] +pub struct ForwardStartOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `alpha` - The proportion of S to set the strike price. + /// Three possibilities: + /// - alpha < 1: call (put) will start (1 - alpha)% in-the-money (out-of-the-money). + /// - alpha = 1: the option starts at-the-money. + /// - alpha > 1: call (put) will start (alpha - 1)% out-of-the-money (in-the-money). + pub alpha: f64, + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `v` - Volatility parameter. + pub volatility: f64, + /// `q` - Dividend rate. + pub dividend_rate: f64, + + /// `valuation_date` - Valuation date. + #[builder(default = "None")] + pub valuation_date: Option, + + /// `start` - Time until the start of the option (`T` in most literature). + pub start: Date, + /// `end` - Time until the end of the option (`t` in most literature). + pub end: Date, +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// FORWARD START OPTION IMPLEMENTATION +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl ForwardStartOption { + /// Rubinstein (1990) Forward Start Option Price formula. + /// Returns a tuple: `(call_price, put_price)` + /// # Note: + /// * `b = r - q` - The cost of carry. + #[must_use] + pub fn price(&self) -> (f64, f64) { + let S = self.initial_price; + let a = self.alpha; + + let r = self.risk_free_rate; + let v = self.volatility; + let q = self.dividend_rate; + + let T = DayCountConvention::default() + .day_count_factor(self.valuation_date.unwrap_or(today()), self.end); + + let t = DayCountConvention::default() + .day_count_factor(self.valuation_date.unwrap_or(today()), self.start); + + let b = r - q; + + let d1 = ((1. / a).ln() + (b + v * v / 2.) * (T - t)) / (v * (T - t).sqrt()); + let d2 = d1 - v * (T - t).sqrt(); + + let norm = Gaussian::default(); + + let Nd1: f64 = norm.cdf(d1); + let Nd2: f64 = norm.cdf(d2); + + let Nd1_: f64 = norm.cdf(-d1); + let Nd2_: f64 = norm.cdf(-d2); + + let c: f64 = S + * ((b - r) * t).exp() + * (((b - r) * (T - t)).exp() * Nd1 - a * (-r * (T - t)).exp() * Nd2); + let p: f64 = S + * ((b - r) * t).exp() + * (-((b - r) * (T - t)).exp() * Nd1_ + a * (-r * (T - t)).exp() * Nd2_); + + (c, p) + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests_forward_start { + use super::*; + use crate::assert_approx_equal; + + #[test] + fn TEST_forward_start_option() { + let start = today() + time::Duration::days(91); + let end = today() + time::Duration::days(365); + + let ForwardStart = ForwardStartOption { + initial_price: 60.0, + alpha: 1.1, + risk_free_rate: 0.08, + volatility: 0.3, + dividend_rate: 0.04, + valuation_date: None, + start, + end, + }; + + let prices = ForwardStart.price(); + + // Call price example from Haug's book. + assert_approx_equal!(prices.0, 4.402888269001168, 1e-2); + } +} diff --git a/src/instruments/options/heston.rs b/src/instruments/options/todo/heston.rs similarity index 100% rename from src/instruments/options/heston.rs rename to src/instruments/options/todo/heston.rs diff --git a/src/instruments/options/todo/lookback.rs b/src/instruments/options/todo/lookback.rs new file mode 100644 index 00000000..14731c6a --- /dev/null +++ b/src/instruments/options/todo/lookback.rs @@ -0,0 +1,402 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use crate::{ + instruments::options::TypeFlag, + math::distributions::{Distribution, Gaussian}, + math::Statistic, + models::geometric_brownian_motion::GeometricBrownianMotion, + stochastics::process::StochasticProcess, +}; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// LOOKBACK OPTION STRUCTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Lookback option strike type enum. +/// The strike can be either fixed or floating. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Copy)] +pub enum LookbackStrike { + /// Floating strike lookback option. + /// Payoffs: + /// - Call: `max(S_T - S_min, 0)` + /// - Put: `max(S_max - S_T, 0)` + Floating, + /// Fixed strike lookback option. + /// Payoffs: + /// - Call: `max(S_max - K, 0)` + /// - Put: `max(K - S_min, 0)` + Fixed, +} + +/// Struct containing Lookback Option parameters. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Copy)] +pub struct LookbackOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `K` - Strike price (only needed for fixed strike lookbacks). + /// If the strike is floating, then this is `None`. + pub strike_price: Option, + /// `v` - Volatility parameter. + pub volatility: f64, + /// `T` - Time to expiry/maturity. + pub time_to_maturity: f64, + /// `q` - dividend yield. + pub dividend_yield: f64, + /// Minimum value of the underlying price observed **so far**. + /// If the contract starts at t=0, then `S_min = S_0`. + /// Used for the closed-form put price. + pub s_min: f64, + /// Maximum value of the underlying price observed **so far**. + /// If the contract starts at t=0, then `S_max = S_0`. + /// Used for the closed-form call price. + pub s_max: f64, + /// Strike type. + pub strike_type: LookbackStrike, +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// LOOKBACK OPTION IMPLEMENTATIONS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl LookbackOption { + /// Closed-form lookback option price. + #[must_use] + pub fn price_analytic(&self) -> (f64, f64) { + let s = self.initial_price; + let r = self.risk_free_rate; + let t = self.time_to_maturity; + let v = self.volatility; + let q = self.dividend_yield; + let s_min = self.s_min; + let s_max = self.s_max; + + let b = r - q; // Cost of carry + + let norm = Gaussian::default(); + + let call: f64; + let put: f64; + + match self.strike_type { + LookbackStrike::Floating => { + let a1 = ((s / s_min).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt()); + let a2 = a1 - v * t.sqrt(); + let b1 = ((s / s_max).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt()); + let b2 = b1 - v * t.sqrt(); + + if b == 0.0 { + call = s * (-r * t).exp() * norm.cdf(a1) + - s_min * (-r * t).exp() * norm.cdf(a2) + + s * (-r * t).exp() + * v + * t.sqrt() + * (norm.pdf(a1) + a1 * (norm.cdf(a1) - 1.0)); + + put = -s * ((b - r) * t).exp() * norm.cdf(-b1) + + s_max * (-r * t).exp() * norm.cdf(-b2) + + s * (-r * t).exp() * v * t.sqrt() * (norm.pdf(b1) + b1 * norm.cdf(b1)); + } else { + call = s * ((b - r) * t).exp() * norm.cdf(a1) + - s_min * (-r * t).exp() * norm.cdf(a2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * ((s / s_min).powf(-2.0 * b / (v * v)) + * norm.cdf(-a1 + 2.0 * b * t.sqrt() / v) + - (b * t).exp() * norm.cdf(-a1)); + + put = -s * ((b - r) * t).exp() * norm.cdf(-b1) + + s_max * (-r * t).exp() * norm.cdf(-b2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * (-(s / s_max).powf(-2.0 * b / (v * v)) + * norm.cdf(b1 - 2.0 * b * t.sqrt() / v) + + (b * t).exp() * norm.cdf(b1)); + } + + (call, put) + } + LookbackStrike::Fixed => { + let x = self.strike_price.unwrap(); + + let d1 = ((s / x).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt()); + let d2 = d1 - v * t.sqrt(); + let e1 = ((s / s_max).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt()); + let e2 = e1 - v * t.sqrt(); + let f1 = ((s / s_min).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt()); + let f2 = f1 - v * t.sqrt(); + + let call = if x > s_max { + s * ((b - r) * t).exp() * norm.cdf(d1) - x * (-r * t).exp() * norm.cdf(d2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * (-(s / x).powf(-2.0 * b / (v * v)) + * norm.cdf(d1 - 2.0 * b * t.sqrt() / v) + + (b * t).exp() * norm.cdf(d1)) + } else { + (-r * t).exp() * (s_max - x) + s * ((b - r) * t).exp() * norm.cdf(e1) + - s_max * (-r * t).exp() * norm.cdf(e2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * (-(s / s_max).powf(-2.0 * b / (v * v)) + * norm.cdf(e1 - 2.0 * b * t.sqrt() / v) + + (b * t).exp() * norm.cdf(e1)) + }; + + let put = if x < s_min { + -s * ((b - r) * t).exp() * norm.cdf(-d1) + + x * (-r * t).exp() * norm.cdf(-d2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * ((s / x).powf(-2.0 * b / (v * v)) + * norm.cdf(-d1 + 2.0 * b * t.sqrt() / v) + - (b * t).exp() * norm.cdf(-d1)) + } else { + (-r * t).exp() * (x - s_min) - s * ((b - r) * t).exp() * norm.cdf(-f1) + + s_min * (-r * t).exp() * norm.cdf(-f2) + + s * (-r * t).exp() + * (v * v / (2.0 * b)) + * ((s / s_min).powf(-2.0 * b / (v * v)) + * norm.cdf(-f1 + 2.0 * b * t.sqrt() / v) + - (b * t).exp() * norm.cdf(-f1)) + }; + + (call, put) + } + } + } + + fn payoff(&self, option_type: TypeFlag, strike_type: LookbackStrike, path: &[f64]) -> f64 { + // let S_min = path.iter().copied().fold(path[0] /*f64::NAN*/, f64::min); + // let S_max = path.iter().copied().fold(path[0] /*f64::NAN*/, f64::max); + + let S_min = *path.iter().min_by(|a, b| a.total_cmp(b)).unwrap(); + let S_max = *path.iter().max_by(|a, b| a.total_cmp(b)).unwrap(); + + let S_T = path.last().unwrap(); + + match option_type { + TypeFlag::Call => match strike_type { + LookbackStrike::Fixed => f64::max(S_max - self.strike_price.unwrap(), 0.0), + LookbackStrike::Floating => f64::max(S_T - S_min, 0.0), + }, + TypeFlag::Put => match strike_type { + LookbackStrike::Fixed => f64::max(self.strike_price.unwrap() - S_min, 0.0), + LookbackStrike::Floating => f64::max(S_max - S_T, 0.0), + }, + } + } + + /// Monte Carlo simulation of the lookback option price. + #[must_use] + pub fn price_simulated(&self, n_steps: usize, n_sims: usize, parallel: bool) -> (f64, f64) { + let x_0 = self.initial_price; + let r = self.risk_free_rate; + let q = self.dividend_yield; + let sigma = self.volatility; + let t_n = self.time_to_maturity; + + // Adjust the drift term to account for the cost of carry. + let cost_of_carry = r - q; + let gbm = GeometricBrownianMotion::new(cost_of_carry, sigma); + + let paths = gbm.euler_maruyama(x_0, 0.0, t_n, n_steps, n_sims, parallel); + + let mut call_payoffs = Vec::with_capacity(n_sims); + let mut put_payoffs = Vec::with_capacity(n_sims); + + match self.strike_type { + LookbackStrike::Fixed => { + for path in &paths.paths { + call_payoffs.push(Self::payoff( + self, + TypeFlag::Call, + LookbackStrike::Fixed, + path, + )); + put_payoffs.push(Self::payoff( + self, + TypeFlag::Put, + LookbackStrike::Fixed, + path, + )); + } + } + LookbackStrike::Floating => { + for path in &paths.paths { + call_payoffs.push(Self::payoff( + self, + TypeFlag::Call, + LookbackStrike::Floating, + path, + )); + put_payoffs.push(Self::payoff( + self, + TypeFlag::Put, + LookbackStrike::Floating, + path, + )); + } + } + } + + ( + // Discounted mean of the call and put payoffs. + (-r * t_n).exp() * call_payoffs.mean(), + (-r * t_n).exp() * put_payoffs.mean(), + ) + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// LOOKBACK OPTION TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests_lookback { + use super::*; + use crate::assert_approx_equal; + + #[test] + fn test_lookback_floating() { + let lbo_floating = LookbackOption { + initial_price: 50.0, + s_max: 50.0, + s_min: 50.0, + time_to_maturity: 0.25, + risk_free_rate: 0.1, + dividend_yield: 0.0, + volatility: 0.4, + strike_price: None, // Floating strike has no strike price input. + strike_type: LookbackStrike::Floating, + }; + + let prices_mc = lbo_floating.price_simulated(500, 10000, true); + let prices_cf = lbo_floating.price_analytic(); + + // Analytic and closed form should match. + assert_approx_equal!(prices_mc.0, prices_cf.0, 0.5); + assert_approx_equal!(prices_mc.1, prices_cf.1, 0.5); + + // Hull p.630: Floating-Strike Lookback Option Values + // Monte Carlo prices. + assert_approx_equal!(prices_mc.0, 8.04, 0.5); + assert_approx_equal!(prices_mc.1, 7.79, 0.5); + // Closed-form prices. + assert_approx_equal!(prices_cf.0, 8.04, 0.2); + assert_approx_equal!(prices_cf.1, 7.79, 0.2); + + // The following example (which includes q = 0.06) from Haug's book + // does not work. + // The cost of carry seems to be the problem. + // Even though it is accounted for in the drift term of the GBM. + // let lbo_floating = LookbackOption { + // initial_price: 120.0, + // s_max: 100.0, + // s_min: 100.0, + // time_to_maturity: 0.5, + // risk_free_rate: 0.1, + // dividend_yield: 0.06, + // volatility: 0.3, + // strike_price: None, // Floating strike has no strike price input. + // strike_type: LookbackStrike::Floating, + // }; + // Haug p.145: Floating-Strike Lookback Option Values + // Monte Carlo prices. + // assert_approx_equal!(prices_mc.0, 25.3533, 0.5); + // assert_approx_equal!(prices_mc.1, 1.0534, 0.5); + // Analytic prices. + // assert_approx_equal!(prices_cf.0, 25.3533, 0.4); + // assert_approx_equal!(prices_cf.1, 1.0534, 0.4); + } + + #[test] + fn test_lookback_fixed() { + let lbo_fixed = LookbackOption { + initial_price: 100.0, + s_max: 100.0, + s_min: 100.0, + time_to_maturity: 1.0, + risk_free_rate: 0.1, + volatility: 0.1, + strike_price: Some(95.0), + dividend_yield: 0.0, + strike_type: LookbackStrike::Fixed, + }; + + let prices_mc = lbo_fixed.price_simulated(1000, 10000, true); + let prices_cf = lbo_fixed.price_analytic(); + + // Analytic and closed form should match. + assert_approx_equal!(prices_mc.0, prices_cf.0, 0.5); + assert_approx_equal!(prices_mc.1, prices_cf.1, 0.5); + + // Haug p.145: Fixed-Strike Lookback Option Values + // Monte Carlo prices. + assert_approx_equal!(prices_mc.0, 18.3241, 0.5); + assert_approx_equal!(prices_mc.1, 1.0534, 0.5); + + // Haug p.145: Fixed-Strike Lookback Option Values + // Analytic prices. + assert_approx_equal!(prices_cf.0, 18.3241, 0.0001); + assert_approx_equal!(prices_cf.1, 1.0534, 0.0001); + } + + #[test] + fn test_lookback_payoff_fixed() { + let lbo_fixed = LookbackOption { + initial_price: 50.0, + s_max: 50.0, + s_min: 50.0, + time_to_maturity: 0.25, + risk_free_rate: 0.1, + dividend_yield: 0.0, + volatility: 0.4, + strike_price: Some(60.0), // Fixed strike has a strike price input. + strike_type: LookbackStrike::Fixed, + }; + + let path = vec![50.0, 55.0, 52.0, 58.0, 54.0]; + + let call_payoff = lbo_fixed.payoff(TypeFlag::Call, LookbackStrike::Fixed, &path); + let put_payoff = lbo_fixed.payoff(TypeFlag::Put, LookbackStrike::Fixed, &path); + + // Payoff values + assert_approx_equal!(call_payoff, 0.0, 0.1); // call payoff = max(S_max - K, 0) = max(58 - 60, 0) = 0 + assert_approx_equal!(put_payoff, 10.0, 0.1); // put payoff = max(K - S_min, 0) = max(60 - 50, 0) = 10 + } + + #[test] + fn test_lookback_payoff_floating() { + let lbo_floating = LookbackOption { + initial_price: 50.0, + s_max: 50.0, + s_min: 50.0, + time_to_maturity: 0.25, + risk_free_rate: 0.1, + dividend_yield: 0.0, + volatility: 0.4, + strike_price: None, // Floating strike has no strike price input. + strike_type: LookbackStrike::Floating, + }; + + let path = vec![50.0, 55.0, 52.0, 58.0, 54.0]; + + let call_payoff = lbo_floating.payoff(TypeFlag::Call, LookbackStrike::Floating, &path); + let put_payoff = lbo_floating.payoff(TypeFlag::Put, LookbackStrike::Floating, &path); + + // Payoff values + assert_approx_equal!(call_payoff, 4.0, 0.1); // call payoff = max(S_T - S_min, 0) = max(54 - 50, 0) = 4 + assert_approx_equal!(put_payoff, 4.0, 0.1); // put payoff = max(S_max - S_T, 0) = max(58 - 54, 0) = 4 + } +} diff --git a/src/instruments/options/todo/power.rs b/src/instruments/options/todo/power.rs new file mode 100644 index 00000000..9d2fd372 --- /dev/null +++ b/src/instruments/options/todo/power.rs @@ -0,0 +1,121 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! # Power Contracts +//! +//! Power contracts are options with the payoff: (S/K)^i +//! where i is the (fixed) power of the contract. + +use crate::time::{today, DayCountConvention}; +use time::Date; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STRUCTS, ENUMS, AND TRAITS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Power Option contract. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, Copy)] +pub struct PowerOption { + /// `S` - Initial price of the underlying. + pub initial_price: f64, + /// `K` - Strike price. + pub strike_price: f64, + /// `i` - Power of the contract. + pub power: f64, + + /// `r` - Risk-free rate parameter. + pub risk_free_rate: f64, + /// `b` - Cost of carry. + pub cost_of_carry: f64, + /// `v` - Volatility parameter. + pub volatility: f64, + + /// `valuation_date` - Valuation date. + pub evaluation_date: Option, + /// `expiry_date` - Expiry date. + pub expiration_date: Date, +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// IMPLEMENTATIONS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +impl PowerOption { + /// New Power Option contract. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + initial_price: f64, + strike_price: f64, + power: f64, + risk_free_rate: f64, + cost_of_carry: f64, + volatility: f64, + evaluation_date: Option, + expiration_date: Date, + ) -> Self { + Self { + initial_price, + strike_price, + power, + risk_free_rate, + cost_of_carry, + volatility, + evaluation_date, + expiration_date, + } + } + + /// Power Option price. + #[must_use] + pub fn price(&self) -> f64 { + let S = self.initial_price; + let K = self.strike_price; + let r = self.risk_free_rate; + let v = self.volatility; + let b = self.cost_of_carry; + let i = self.power; + + // Compute time to maturity. + let T = DayCountConvention::default().day_count_factor( + self.evaluation_date.unwrap_or(today()), + self.expiration_date, + ); + + (S / K).powf(i) * (((b - 0.5 * v.powi(2)) * i - r + 0.5 * (i * v).powi(2)) * T).exp() + } +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// TESTS +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cfg(test)] +mod tests_power_contract { + use super::*; + use crate::{assert_approx_equal, RUSTQUANT_EPSILON}; + use time::Duration; + + #[test] + fn test_power() { + let power_option = PowerOption { + initial_price: 400., + strike_price: 450., + power: 2., + risk_free_rate: 0.08, + cost_of_carry: 0.06, + volatility: 0.25, + evaluation_date: None, + expiration_date: today() + Duration::days(182), + }; + + assert_approx_equal!(power_option.price(), 0.83144001309052, RUSTQUANT_EPSILON); + } +} diff --git a/src/instruments/options/vanilla.rs b/src/instruments/options/vanilla.rs new file mode 100644 index 00000000..d773be7d --- /dev/null +++ b/src/instruments/options/vanilla.rs @@ -0,0 +1,172 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +use super::{OptionContract, TypeFlag}; +use crate::{ + instruments::Payoff, + pricer::MonteCarloPricer, + stochastics::{StochasticProcess, StochasticProcessConfig}, +}; + +/// Vanilla option. +#[derive(Debug, Clone)] +pub struct VanillaOption { + /// The option contract. + pub contract: OptionContract, + + /// Strike price of the option. + pub strike: f64, +} + +impl Payoff for VanillaOption { + type Underlying = f64; + + fn payoff(&self, underlying: Self::Underlying) -> f64 { + match self.contract.type_flag { + TypeFlag::Call => (underlying - self.strike).max(0.0), + TypeFlag::Put => (self.strike - underlying).max(0.0), + } + } +} + +impl VanillaOption { + /// Create a new vanilla option. + pub fn new(contract: OptionContract, strike: f64) -> Self { + Self { contract, strike } + } +} + +impl MonteCarloPricer for VanillaOption +where + S: StochasticProcess, +{ + fn price_monte_carlo(&self, process: S, config: StochasticProcessConfig, rate: f64) -> f64 { + let out = process.euler_maruyama(&config); + + let n = out.paths.len(); + + let df = (-rate * (config.t_n - config.t_0)).exp(); + + out.paths.iter().fold(0.0, |acc, path| { + let payoff = self.payoff(path.last().unwrap().clone()); + acc + df * payoff + }) / n as f64 + } +} + +#[cfg(test)] +mod test_vanilla_option_monte_carlo { + use time::macros::date; + + use crate::{ + instruments::{ExerciseFlag, OptionContractBuilder}, + models::GeometricBrownianMotion, + }; + + use super::*; + + #[test] + fn test_vanilla_option_monte_carlo() { + let underlying = 100.0; + let strike = 100.0; + let interest_rate = 0.05; + let time_to_maturity = 1.0; + let volatility = 0.2; + + let contract = OptionContractBuilder::default() + .type_flag(TypeFlag::Call) + .exercise_flag(ExerciseFlag::European { + expiry: date!(2025 - 01 - 01), + }) + .build() + .unwrap(); + + let option = VanillaOption::new(contract, strike); + let process = GeometricBrownianMotion::new(interest_rate, volatility); + + let config = + StochasticProcessConfig::new(underlying, 0.0, time_to_maturity, 1000, 1000, false); + + let price = option.price_monte_carlo(process, config, interest_rate); + + println!("Price: {}", price); + } +} + +// impl Instrument for VanillaOption { +// fn price(&self) -> f64 { +// 1. +// } + +// fn error(&self) -> Option { +// None +// } + +// fn valuation_date(&self) -> Date { +// todo!() +// } + +// fn instrument_type(&self) -> &'static str { +// todo!() +// } +// } + +// impl Priceable for VanillaOption +// where +// C: Calendar + Clone, +// { +// /// VanillaOption pricer implementation. +// /// +// /// This aksjdfoasj ofdjsod +// fn pricer_impl( +// &self, +// context_data: &Option>, +// market_data: &mut Option>, +// // model: &Option, +// // engine: &Option, +// ) -> f64 { +// // let cal = context_data.as_ref().unwrap().calendar.as_ref().unwrap(); +// let eval = context_data.as_ref().unwrap().evaluation_date.unwrap(); + +// let s = market_data.as_ref().unwrap().underlying_price.unwrap(); +// let k = self.strike; +// let t = match self.contract.exercise_flag { +// ExerciseFlag::European { expiry } => expiry, +// ExerciseFlag::American { .. } => todo!(), +// ExerciseFlag::Bermudan { .. } => todo!(), +// }; +// // let tau = DayCounter::day_count_factor( +// // cal, +// // eval, +// // t, +// // &context_data.as_ref().unwrap().day_count_convention.unwrap(), +// // ); +// let r = market_data +// .as_mut() +// .unwrap() +// .spot_curve +// .as_mut() +// .unwrap() +// .get_rate(t); +// let v = market_data.as_ref().unwrap().volatility.unwrap(); + +// let bsm = BlackScholesMerton { +// cost_of_carry: r, +// underlying_price: s, +// strike_price: k, +// volatility: v, +// risk_free_rate: r, +// evaluation_date: Some(eval), +// expiration_date: t, +// option_type: self.contract.type_flag, +// }; + +// bsm.price() +// } +// } diff --git a/src/instruments/payoff.rs b/src/instruments/payoff.rs new file mode 100644 index 00000000..ca894d23 --- /dev/null +++ b/src/instruments/payoff.rs @@ -0,0 +1,17 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Generic payoff trait for derivatives. +pub trait Payoff { + /// Underlying input type for the payoff function. + type Underlying; + + /// Payoff function for the derivative. + fn payoff(&self, underlying: Self::Underlying) -> f64; +} diff --git a/src/iso/iso_4217.rs b/src/iso/iso_4217.rs index 1b1dbeb4..923372b7 100644 --- a/src/iso/iso_4217.rs +++ b/src/iso/iso_4217.rs @@ -22,7 +22,7 @@ use std::fmt::Formatter; /// - First two letters are the ISO 3166-1 alpha-2 country code. e.g. US = United States /// - Third letter is the first letter of the currency name. e.g. USD = United States Dollar /// - The number is the ISO numeric code. e.g. 840 = USD -#[derive(Debug, Clone, Copy, Eq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] pub struct ISO_4217 { /// The ISO 4217 alphabetic code. @@ -55,11 +55,11 @@ impl ISO_4217 { } } -impl PartialEq for ISO_4217 { - fn eq(&self, other: &Self) -> bool { - self.alphabetic == other.alphabetic && self.numeric == other.numeric - } -} +// impl PartialEq for ISO_4217 { +// fn eq(&self, other: &Self) -> bool { +// self.alphabetic == other.alphabetic && self.numeric == other.numeric +// } +// } impl fmt::Display for ISO_4217 { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { diff --git a/src/lib.rs b/src/lib.rs index 2f824f5f..e25c600e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ pub mod math; pub mod ml; pub mod models; pub mod portfolio; +pub mod pricer; pub mod stochastics; pub mod time; pub mod trading; diff --git a/src/pricer/context_data.rs b/src/pricer/context_data.rs new file mode 100644 index 00000000..815d0fd5 --- /dev/null +++ b/src/pricer/context_data.rs @@ -0,0 +1,57 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! Contextual (reference) data container. + +use crate::instruments::Currency; +use crate::time::{ + Calendar, DateGenerationConvention, DateRollingConvention, DayCountConvention, Frequency, + Schedule, +}; +use ::time::Date; +use derive_builder::Builder; + +/// Contextual (reference) data. +#[derive(Builder, Clone)] +pub struct ContextData +where + C: Calendar, +{ + /// Calendar object. + #[builder(default)] + pub calendar: Option, + + /// Evaluation date. + #[builder(default)] + pub evaluation_date: Option, + + /// Currency. + #[builder(default)] + pub currency: Option, + + /// Frequency. + #[builder(default)] + pub frequency: Option, + + /// Schedule. + #[builder(default)] + pub schedule: Option, + + /// Day count convention. + #[builder(default)] + pub day_count_convention: Option, + + /// Date rolling convention. + #[builder(default)] + pub date_rolling_convention: Option, + + /// Date generation convention. + #[builder(default)] + pub date_generation_convention: Option, +} diff --git a/src/pricer/market_data.rs b/src/pricer/market_data.rs new file mode 100644 index 00000000..66987eed --- /dev/null +++ b/src/pricer/market_data.rs @@ -0,0 +1,55 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! Market data container. + +use crate::data::{DiscountCurve, FlatCurve, ForwardCurve, SpotCurve}; +use crate::instruments::ExchangeRate; +use crate::time::Calendar; +use derive_builder::Builder; +use time::Date; + +/// Market data. +#[derive(Builder, Clone, Debug)] +pub struct MarketData +where + C: Calendar, +{ + /// Underlying price. + #[builder(default)] + pub underlying_price: Option, + + /// Exchange rate. + #[builder(default)] + pub exchange_rate: Option, + + /// Dividend yield. + #[builder(default)] + pub dividend_yield: Option, + + /// Volatility (implied). + #[builder(default)] + pub volatility: Option, + + /// Spot curve. + #[builder(default)] + pub spot_curve: Option>, + + /// Discount curve. + #[builder(default)] + pub discount_curve: Option>, + + /// Forward curve. + #[builder(default)] + pub forward_curve: Option>, + + /// Flat curve. + #[builder(default)] + pub flat_curve: Option>, +} diff --git a/src/pricer/mod.rs b/src/pricer/mod.rs new file mode 100644 index 00000000..9815cc72 --- /dev/null +++ b/src/pricer/mod.rs @@ -0,0 +1,93 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! Pricer module. + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// MODULES +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +pub mod context_data; +pub use context_data::*; + +pub mod market_data; +pub use market_data::*; + +pub mod priceable; +pub use priceable::*; + +pub mod monte_carlo_pricer; +pub use monte_carlo_pricer::*; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// PRICER STRUCT +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Pricing engine for instruments. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PricingMethod { + /// Analytic pricing method (e.g. closed-form solution). + Analytic, + + /// Simulation pricing method (e.g. Monte Carlo). + Simulation, + + /// Numerical method (e.g. PDE, lattice, finite differences). + Numerical, +} + +// /// Pricer type. +// /// This is the main struct for pricing financial instruments. +// #[derive(Builder)] +// pub struct Pricer +// where +// C: Calendar, +// P: Priceable, +// { +// /// The instrument to be priced. +// pub instrument: P, + +// /// Contextual (reference) data for the pricing. +// pub context_data: Option>, + +// /// Market data for the pricing. +// pub market_data: Option>, + +// /// Pricing engine. +// pub method: Option, +// // /// The model to be used to price the instrument. +// // pub model: Option, +// } + +// impl Pricer +// where +// C: Calendar, +// P: Priceable, +// { +// /// Compute the Net Present Value (NPV) of the instrument. +// pub fn npv(&mut self) -> f64 { +// match self.method { +// Some(PricingMethod::Analytic) => self.instrument.price_analytic_impl(), +// Some(PricingMethod::Simulation) => self.instrument.price_simulation_impl(), +// Some(PricingMethod::Numerical) => self.instrument.price_numerical_impl(), +// None => { +// // Default to analytic pricing. +// self.instrument.price_analytic_impl() +// } +// } +// } + +// // self.instrument.pricer_impl( +// // &self.context_data, +// // &mut self.market_data, +// // // &self.model, +// // // &self.engine, +// // ) +// // } +// } diff --git a/src/pricer/monte_carlo_pricer.rs b/src/pricer/monte_carlo_pricer.rs new file mode 100644 index 00000000..ba059845 --- /dev/null +++ b/src/pricer/monte_carlo_pricer.rs @@ -0,0 +1,21 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! Monte-Carlo pricer trait. + +use crate::stochastics::{StochasticProcess, StochasticProcessConfig}; + +/// Monte-Carlo pricer trait. +pub trait MonteCarloPricer +where + S: StochasticProcess, +{ + /// Price the instrument using a Monte-Carlo method. + fn price_monte_carlo(&self, process: S, config: StochasticProcessConfig, rate: f64) -> f64; +} diff --git a/src/pricer/priceable.rs b/src/pricer/priceable.rs new file mode 100644 index 00000000..4b829b76 --- /dev/null +++ b/src/pricer/priceable.rs @@ -0,0 +1,55 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2023 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +//! Priceable trait. + +use super::{ContextData, MarketData}; +use crate::{ + instruments::{Instrument, Payoff}, + stochastics::process::StochasticProcess, + time::Calendar, +}; + +/// Priceable trait. +pub trait Priceable: Payoff + Instrument +where + C: Calendar, + S: StochasticProcess, + P: Payoff, +{ + /// Function to prepare the data for the specific instrument. + fn prepare_data(&self) -> (); + + /// Analytic pricer implementation. + fn price_analytic_impl( + &self, + context_data: &Option>, + market_data: &mut Option>, + model: &Option, + // engine: &Option, + ) -> f64; + + /// Simulation pricer implementation. + fn price_simulation_impl( + &self, + context_data: &Option>, + market_data: &mut Option>, + model: &Option, + // engine: &Option, + ) -> f64; + + /// Numerical pricer implementation. + fn price_numerical_impl( + &self, + context_data: &Option>, + market_data: &mut Option>, + model: &Option, + // engine: &Option, + ) -> f64; +} diff --git a/src/stochastics/arithmetic_brownian_motion.rs b/src/stochastics/arithmetic_brownian_motion.rs index f837d855..1f7a1848 100644 --- a/src/stochastics/arithmetic_brownian_motion.rs +++ b/src/stochastics/arithmetic_brownian_motion.rs @@ -34,13 +34,13 @@ impl StochasticProcess for ArithmeticBrownianMotion { #[cfg(test)] mod tests_abm { use super::*; - use crate::{assert_approx_equal, math::*}; + use crate::{assert_approx_equal, math::*, stochastics::StochasticProcessConfig}; #[test] fn test_arithmetic_brownian_motion() { let abm = ArithmeticBrownianMotion::new(0.05, 0.9); - - let output = abm.euler_maruyama(10.0, 0.0, 0.5, 125, 1000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 125, 1000, false); + let output = abm.euler_maruyama(&config); // let file1 = "./images/ABM1.png"; // plot_vector((&output.trajectories[0]).clone(), file1).unwrap(); diff --git a/src/stochastics/black_derman_toy.rs b/src/stochastics/black_derman_toy.rs index 39c34c43..33b92323 100644 --- a/src/stochastics/black_derman_toy.rs +++ b/src/stochastics/black_derman_toy.rs @@ -42,7 +42,7 @@ pub(crate) fn diff(f: &(dyn Fn(f64) -> f64 + Send + Sync), t: f64) -> f64 { #[cfg(test)] mod tests_black_derman_toy { use super::*; - use crate::math::*; + use crate::{math::*, stochastics::StochasticProcessConfig}; // fn theta_t(_t: f64) -> f64 { // 1.5 @@ -58,7 +58,8 @@ mod tests_black_derman_toy { let hw = BlackDermanToy::new(sigma, theta); - let output = hw.euler_maruyama(0.13, 0.0, 1.0, 100, 100, false); + let config = StochasticProcessConfig::new(0.13, 0.0, 1.0, 100, 1000, false); + let output = hw.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output @@ -78,8 +79,8 @@ mod tests_black_derman_toy { let theta = 1.5; let hw = BlackDermanToy::new(sigma, theta); - - let output = hw.euler_maruyama(0.13, 0.0, 1.0, 100, 1000, false); + let config = StochasticProcessConfig::new(0.13, 0.0, 1.0, 100, 1000, false); + let output = hw.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/brownian_motion.rs b/src/stochastics/brownian_motion.rs index 24664225..a0d10698 100644 --- a/src/stochastics/brownian_motion.rs +++ b/src/stochastics/brownian_motion.rs @@ -33,6 +33,7 @@ mod sde_tests { // use std::time::Instant; use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; #[test] @@ -61,7 +62,8 @@ mod sde_tests { // } // assert!(1 == 2); - let output_serial = bm.euler_maruyama(0.0, 0.0, 0.5, 100, 1000, false); + let config = StochasticProcessConfig::new(0.0, 0.0, 0.5, 100, 1000, false); + let output_serial = bm.euler_maruyama(&config); // let output_parallel = (&bm).euler_maruyama(10.0, 0.0, 0.5, 100, 10, true); // let file1 = "./images/BM1.png"; diff --git a/src/stochastics/constant_elasticity_of_variance.rs b/src/stochastics/constant_elasticity_of_variance.rs index 7afbc09f..3832e8c3 100644 --- a/src/stochastics/constant_elasticity_of_variance.rs +++ b/src/stochastics/constant_elasticity_of_variance.rs @@ -34,13 +34,13 @@ impl StochasticProcess for ConstantElasticityOfVariance { #[cfg(test)] mod tests_cev { use super::*; - use crate::math::*; + use crate::{math::*, stochastics::StochasticProcessConfig}; #[test] fn test_cev_process() { let cev = ConstantElasticityOfVariance::new(0.05, 0.9, 0.45); - - let output = cev.euler_maruyama(10.0, 0.0, 0.5, 100, 100, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 100, 100, false); + let output = cev.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/cox_ingersoll_ross.rs b/src/stochastics/cox_ingersoll_ross.rs index 43324909..275c35b8 100644 --- a/src/stochastics/cox_ingersoll_ross.rs +++ b/src/stochastics/cox_ingersoll_ross.rs @@ -32,13 +32,16 @@ impl StochasticProcess for CoxIngersollRoss { #[cfg(test)] mod tests_cir { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; #[test] fn test_cox_ingersoll_ross() { let cir = CoxIngersollRoss::new(0.15, 0.45, 0.01); - let output = cir.euler_maruyama(10.0, 0.0, 0.5, 100, 100, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 100, 100, false); + + let output = cir.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/extended_vasicek.rs b/src/stochastics/extended_vasicek.rs index 7a0172af..d61821b7 100644 --- a/src/stochastics/extended_vasicek.rs +++ b/src/stochastics/extended_vasicek.rs @@ -31,6 +31,7 @@ impl StochasticProcess for ExtendedVasicek { #[cfg(test)] mod tests_extended_vasicek { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; // fn alpha_t(_t: f64) -> f64 { @@ -48,7 +49,9 @@ mod tests_extended_vasicek { let ev = ExtendedVasicek::new(alpha, sigma, theta); - let output = ev.euler_maruyama(10.0, 0.0, 1.0, 150, 1000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 1.0, 150, 1000, false); + + let output = ev.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/fractional_brownian_motion.rs b/src/stochastics/fractional_brownian_motion.rs index a5d3a346..806e54da 100644 --- a/src/stochastics/fractional_brownian_motion.rs +++ b/src/stochastics/fractional_brownian_motion.rs @@ -21,6 +21,8 @@ use rand::{rngs::StdRng, SeedableRng}; use rand_distr::StandardNormal; use rayon::prelude::*; +use super::StochasticProcessConfig; + /// Method used to generate the Fractional Brownian Motion. #[derive(Debug)] pub enum FractionalProcessGeneratorMethod { @@ -149,15 +151,9 @@ impl StochasticProcess for FractionalBrownianMotion { None } - fn euler_maruyama( - &self, - x_0: f64, - t_0: f64, - t_n: f64, - n_steps: usize, - m_paths: usize, - parallel: bool, - ) -> Trajectories { + fn euler_maruyama(&self, config: &StochasticProcessConfig) -> Trajectories { + let (x_0, t_0, t_n, n_steps, m_paths, parallel) = config.unpack(); + assert!(t_0 < t_n); let dt: f64 = (t_n - t_0) / (n_steps as f64); @@ -293,7 +289,8 @@ mod test_fractional_brownian_motion { #[test] fn test_brownian_motion() { let fbm = FractionalBrownianMotion::new(0.7, FractionalProcessGeneratorMethod::FFT); - let output_serial = fbm.euler_maruyama(0.0, 0.0, 0.5, 100, 1000, false); + let config = StochasticProcessConfig::new(0.0, 0.0, 0.5, 100, 1000, false); + let output_serial = fbm.euler_maruyama(&config); // let output_parallel = (&bm).euler_maruyama(10.0, 0.0, 0.5, 100, 10, true); // Test the distribution of the final values. diff --git a/src/stochastics/fractional_cox_ingersoll_ross.rs b/src/stochastics/fractional_cox_ingersoll_ross.rs index 64b519e5..98cf9429 100644 --- a/src/stochastics/fractional_cox_ingersoll_ross.rs +++ b/src/stochastics/fractional_cox_ingersoll_ross.rs @@ -10,6 +10,7 @@ use super::{ fractional_brownian_motion::FractionalProcessGeneratorMethod, process::{StochasticProcess, Trajectories}, + StochasticProcessConfig, }; use crate::models::{ fractional_brownian_motion::FractionalBrownianMotion, @@ -30,15 +31,9 @@ impl StochasticProcess for FractionalCoxIngersollRoss { Some(0.0) } - fn euler_maruyama( - &self, - x_0: f64, - t_0: f64, - t_n: f64, - n_steps: usize, - m_paths: usize, - parallel: bool, - ) -> Trajectories { + fn euler_maruyama(&self, config: &StochasticProcessConfig) -> Trajectories { + let (t_0, x_0, t_n, n_steps, m_paths, parallel) = config.unpack(); + let fgn = match self.method { FractionalProcessGeneratorMethod::CHOLESKY => { let fbm = FractionalBrownianMotion::new( @@ -100,8 +95,10 @@ mod test_fractional_cir { FractionalProcessGeneratorMethod::FFT, ); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 100, 100, false); + #[allow(dead_code)] - let _output = fou.euler_maruyama(10.0, 0.0, 0.5, 100, 100, false); + let _output = fou.euler_maruyama(&config); std::result::Result::Ok(()) } diff --git a/src/stochastics/fractional_ornstein_uhlenbeck.rs b/src/stochastics/fractional_ornstein_uhlenbeck.rs index 28194af6..49841bc2 100644 --- a/src/stochastics/fractional_ornstein_uhlenbeck.rs +++ b/src/stochastics/fractional_ornstein_uhlenbeck.rs @@ -17,6 +17,8 @@ use crate::{ }; use rayon::prelude::*; +use super::StochasticProcessConfig; + impl StochasticProcess for FractionalOrnsteinUhlenbeck { fn drift(&self, x: f64, t: f64) -> f64 { self.theta.0(t) * (self.mu.0(t) - x) @@ -31,15 +33,9 @@ impl StochasticProcess for FractionalOrnsteinUhlenbeck { None } - fn euler_maruyama( - &self, - x_0: f64, - t_0: f64, - t_n: f64, - n_steps: usize, - m_paths: usize, - parallel: bool, - ) -> Trajectories { + fn euler_maruyama(&self, config: &StochasticProcessConfig) -> Trajectories { + let (x_0, t_0, t_n, n_steps, m_paths, parallel) = config.unpack(); + let fgn = match self.method { FractionalProcessGeneratorMethod::CHOLESKY => { let fbm = FractionalBrownianMotion::new( @@ -100,7 +96,8 @@ mod tests_fractional_ornstein_uhlenbeck { FractionalProcessGeneratorMethod::FFT, ); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 100, 100, false); #[allow(dead_code)] - let _output = fou.euler_maruyama(10.0, 0.0, 0.5, 100, 100, false); + let _output = fou.euler_maruyama(&config); } } diff --git a/src/stochastics/geometric_brownian_bridge.rs b/src/stochastics/geometric_brownian_bridge.rs index 5fc8c6ce..b0cfbd55 100644 --- a/src/stochastics/geometric_brownian_bridge.rs +++ b/src/stochastics/geometric_brownian_bridge.rs @@ -34,6 +34,7 @@ impl StochasticProcess for GeometricBrownianBridge { #[cfg(test)] mod tests_gbm_bridge { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; /// Test the Geometric Brownian Bridge process. @@ -41,7 +42,9 @@ mod tests_gbm_bridge { fn test_geometric_brownian_motion_bridge() { let gbm = GeometricBrownianBridge::new(0.05, 0.9, 10.0, 0.5); - let output = gbm.euler_maruyama(10.0, 0.0, 0.5, 125, 10000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 125, 10000, false); + + let output = gbm.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/geometric_brownian_motion.rs b/src/stochastics/geometric_brownian_motion.rs index e4f64f43..c7fea84e 100644 --- a/src/stochastics/geometric_brownian_motion.rs +++ b/src/stochastics/geometric_brownian_motion.rs @@ -36,13 +36,15 @@ impl StochasticProcess for GeometricBrownianMotion { #[cfg(test)] mod tests_gbm { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; #[test] fn test_geometric_brownian_motion() { let gbm = GeometricBrownianMotion::new(0.05, 0.9); - let output = gbm.euler_maruyama(10.0, 0.0, 0.5, 125, 10000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 125, 10000, false); + let output = gbm.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/ho_lee.rs b/src/stochastics/ho_lee.rs index 55cb87dc..f05976b5 100644 --- a/src/stochastics/ho_lee.rs +++ b/src/stochastics/ho_lee.rs @@ -32,8 +32,8 @@ impl StochasticProcess for HoLee { #[cfg(test)] mod tests_ho_lee { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; - // Test a simple case where theta_t is constant // Should add tests of simple analytically tractable case // fn theta_t(_t: f64) -> f64 { @@ -46,7 +46,8 @@ mod tests_ho_lee { // X_0 = 10.0 // T = 1.0 - let output = hl.euler_maruyama(10.0, 0.0, 1.0, 125, 1000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 1.0, 125, 1000, false); + let output = hl.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/hull_white.rs b/src/stochastics/hull_white.rs index cd91bb1a..193a383d 100644 --- a/src/stochastics/hull_white.rs +++ b/src/stochastics/hull_white.rs @@ -30,8 +30,8 @@ impl StochasticProcess for HullWhite { #[cfg(test)] mod tests_hull_white { use super::*; + use crate::stochastics::StochasticProcessConfig; use crate::{assert_approx_equal, math::*}; - // fn theta_t(_t: f64) -> f64 { // 0.5 // } @@ -43,8 +43,9 @@ mod tests_hull_white { let sigma = 2.0; let hw = HullWhite::new(alpha, sigma, theta); + let config = StochasticProcessConfig::new(10.0, 0.0, 1.0, 150, 1000, false); - let output = hw.euler_maruyama(10.0, 0.0, 1.0, 150, 1000, false); + let output = hw.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/merton_jump_diffusion.rs b/src/stochastics/merton_jump_diffusion.rs index c1a0a01b..ef30369a 100644 --- a/src/stochastics/merton_jump_diffusion.rs +++ b/src/stochastics/merton_jump_diffusion.rs @@ -12,6 +12,8 @@ use crate::models::merton_jump_diffusion::MertonJumpDiffusion; use crate::stochastics::process::{StochasticProcess, Trajectories}; use rand_distr::Distribution; use rayon::prelude::*; + +use super::StochasticProcessConfig; // use statrs::distribution::Normal; impl StochasticProcess for MertonJumpDiffusion { @@ -28,15 +30,9 @@ impl StochasticProcess for MertonJumpDiffusion { self.gaussian.sample(1).unwrap().first().copied() } - fn euler_maruyama( - &self, - x_0: f64, - t_0: f64, - t_n: f64, - n_steps: usize, - m_paths: usize, - parallel: bool, - ) -> Trajectories { + fn euler_maruyama(&self, config: &StochasticProcessConfig) -> Trajectories { + let (x_0, t_0, t_n, n_steps, m_paths, parallel) = config.unpack(); + assert!(t_0 < t_n); let dt: f64 = (t_n - t_0) / (n_steps as f64); @@ -92,8 +88,8 @@ mod tests_gbm_bridge { #[test] fn test_geometric_brownian_motion_bridge() { let mjd = MertonJumpDiffusion::new(0.05, 0.9, 1.0, 0.0, 0.3); - - let output = mjd.euler_maruyama(10.0, 0.0, 0.5, 125, 10000, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 125, 10000, false); + let output = mjd.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/ornstein_uhlenbeck.rs b/src/stochastics/ornstein_uhlenbeck.rs index 6e403b1f..7004eaec 100644 --- a/src/stochastics/ornstein_uhlenbeck.rs +++ b/src/stochastics/ornstein_uhlenbeck.rs @@ -33,13 +33,14 @@ impl StochasticProcess for OrnsteinUhlenbeck { #[cfg(test)] mod tests_ornstein_uhlenbeck { use super::*; - use crate::{assert_approx_equal, math::*}; + use crate::{assert_approx_equal, math::*, stochastics::StochasticProcessConfig}; #[test] fn test_ornstein_uhlenbeck() { let ou = OrnsteinUhlenbeck::new(0.15, 0.45, 0.01); - let output = ou.euler_maruyama(10.0, 0.0, 0.5, 100, 100, false); + let config = StochasticProcessConfig::new(10.0, 0.0, 0.5, 100, 100, false); + let output = ou.euler_maruyama(&config); // Test the distribution of the final values. let X_T: Vec = output diff --git a/src/stochastics/process.rs b/src/stochastics/process.rs index d92eeca2..4a771f98 100644 --- a/src/stochastics/process.rs +++ b/src/stochastics/process.rs @@ -101,6 +101,59 @@ pub trait StochasticVolatilityProcess: Sync { } } +/// Configuration parameters for simulating a stochastic process. +pub struct StochasticProcessConfig { + /// Initial value of the process. + pub x_0: f64, + + /// Initial time point. + pub t_0: f64, + + /// Terminal time point. + pub t_n: f64, + + /// Number of time steps between `t_0` and `t_n`. + pub n_steps: usize, + + /// How many process trajectories to simulate. + pub m_paths: usize, + + /// Run in parallel or not (recommended for > 1000 paths). + pub parallel: bool, +} + +impl StochasticProcessConfig { + /// Create a new configuration for a stochastic process. + pub fn new( + x_0: f64, + t_0: f64, + t_n: f64, + n_steps: usize, + m_paths: usize, + parallel: bool, + ) -> Self { + Self { + x_0, + t_0, + t_n, + n_steps, + m_paths, + parallel, + } + } + + pub(crate) fn unpack(&self) -> (f64, f64, f64, usize, usize, bool) { + ( + self.x_0, + self.t_0, + self.t_n, + self.n_steps, + self.m_paths, + self.parallel, + ) + } +} + /// Trait to implement stochastic processes. #[allow(clippy::module_name_repetitions)] pub trait StochasticProcess: Sync { @@ -122,15 +175,8 @@ pub trait StochasticProcess: Sync { /// * `n_steps` - The number of time steps between `t_0` and `t_n`. /// * `m_paths` - How many process trajectories to simulate. /// * `parallel` - Run in parallel or not (recommended for > 1000 paths). - fn euler_maruyama( - &self, - x_0: f64, - t_0: f64, - t_n: f64, - n_steps: usize, - m_paths: usize, - parallel: bool, - ) -> Trajectories { + fn euler_maruyama(&self, config: &StochasticProcessConfig) -> Trajectories { + let (x_0, t_0, t_n, n_steps, m_paths, parallel) = config.unpack(); assert!(t_0 < t_n); let dt: f64 = (t_n - t_0) / (n_steps as f64); @@ -224,20 +270,22 @@ pub trait StochasticProcess: Sync { mod test_process { use crate::models::geometric_brownian_motion::GeometricBrownianMotion; use crate::stochastics::process::StochasticProcess; + use crate::stochastics::StochasticProcessConfig; use std::time::Instant; #[test] fn test_euler_maruyama() { let gbm = GeometricBrownianMotion::new(0.05, 0.9); + let config = StochasticProcessConfig::new(10.0, 0.0, 1.0, 125, 10000, false); let start = Instant::now(); - gbm.euler_maruyama(10.0, 0.0, 1.0, 125, 10000, false); + gbm.euler_maruyama(&config); let serial = start.elapsed(); println!("Serial: \t {:?}", serial); let start = Instant::now(); - gbm.euler_maruyama(10.0, 0.0, 1.0, 125, 10000, true); + gbm.euler_maruyama(&config); let parallel = start.elapsed(); println!("Parallel: \t {:?}", parallel); diff --git a/src/time/date_generation.rs b/src/time/date_generation.rs index 3d21b6c6..37605826 100644 --- a/src/time/date_generation.rs +++ b/src/time/date_generation.rs @@ -8,6 +8,7 @@ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// Date generation conventions. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DateGenerationConvention { /// Forward from the issue date. Forward, diff --git a/src/time/date_rolling.rs b/src/time/date_rolling.rs index 91feeb04..fbdcaea4 100644 --- a/src/time/date_rolling.rs +++ b/src/time/date_rolling.rs @@ -22,7 +22,7 @@ use time::Date; /// time such that it falls in a business day, according with the /// same business calendar. /// """ -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DateRollingConvention { /// Actual: paid on the actual day, even if it is a non-business day. Actual, diff --git a/src/time/day_counting.rs b/src/time/day_counting.rs index 6c606c8d..9c467723 100644 --- a/src/time/day_counting.rs +++ b/src/time/day_counting.rs @@ -34,7 +34,7 @@ use time::{util::is_leap_year, Date, Duration, Month}; /// payment dates, the seller is eligible to some fraction of the coupon amount. /// """ #[allow(non_camel_case_types)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum DayCountConvention { /// The '1/1' day count, which always returns a day count of 1. One_One, diff --git a/src/time/mod.rs b/src/time/mod.rs index fb47c653..5e9379f1 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -50,3 +50,7 @@ pub use schedule::*; /// Date generation rules. pub mod date_generation; pub use date_generation::*; + +/// Stub generation rules. +pub mod stub_generation; +pub use stub_generation::*; diff --git a/src/time/schedule.rs b/src/time/schedule.rs index 98fc0efb..c94d98c4 100644 --- a/src/time/schedule.rs +++ b/src/time/schedule.rs @@ -26,6 +26,7 @@ use time::Date; /// /// The Schedule struct is used to represent these schedules, /// and pricing methods should be implemented using date/time functionality. +#[derive(Clone, Debug)] pub struct Schedule { /// The dates of the schedule. pub dates: Vec, diff --git a/src/time/stub_generation.rs b/src/time/stub_generation.rs new file mode 100644 index 00000000..ba346048 --- /dev/null +++ b/src/time/stub_generation.rs @@ -0,0 +1,29 @@ +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// RustQuant: A Rust library for quantitative finance tools. +// Copyright (C) 2022-2024 https://github.com/avhz +// Dual licensed under Apache 2.0 and MIT. +// See: +// - LICENSE-APACHE.md +// - LICENSE-MIT.md +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +/// Stub generation rules. +pub enum StubGeneration { + /// No stubs. + None, + + /// Short stub at the beginning. + ShortFront, + + /// Short stub at the end. + ShortBack, + + /// Long stub at the beginning. + LongFront, + + /// Long stub at the end. + LongBack, + + /// Front and back stubs. + Both, +} diff --git a/tests/stochastics.rs b/tests/stochastics.rs new file mode 100644 index 00000000..e69de29b