Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ src/.DS_Store

# Local development examples
/examples/Local/

# Local draft issue / PR bodies kept at the repo root
/.issue-*.md
/.pr-*.md
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.17.1] - 2026-04-26

Minor release adding the Garman–Kohlhagen (1983) closed-form pricing
model for European FX options. The new variant
`PricingEngine::ClosedFormGK` is appended at the tail of the enum so
the existing variants keep their implicit discriminants and no major
bump is required (`PricingEngine` has been `#[non_exhaustive]` since
0.17.0).

### Added

- `pricing::garman_kohlhagen`: closed-form
`garman_kohlhagen(option) -> Result<Decimal, PricingError>` for
European options on FX spot rates. Structurally identical to BSM
with `q = r_f`; the implementation delegates to `black_scholes`
after type validation, guaranteeing bit-exact equivalence (verified
to `1e-9` in the tests).
- `pricing::GarmanKohlhagen` trait with default
`calculate_price_garman_kohlhagen` (mirrors the `BlackScholes`
trait pattern).
- `pricing::PricingEngine::ClosedFormGK` variant + dispatch from
`price_option`.
- `examples/examples_pricing/src/bin/garman_kohlhagen.rs`: runnable
demo (Hull canonical USD/GBP, ITM EUR/USD with FX parity check,
unified-API dispatch, symmetric-rate degenerate case).
- `lib.rs` mermaid: new `FX / Currency` subgraph routing
`garman_kohlhagen -> FX Spot`.

### Changed

- `pricing/mod.rs` Core Models / Model Selection Guidelines /
Performance Considerations now include Garman–Kohlhagen with the
explicit field mapping `risk_free_rate -> r_d`,
`dividend_yield -> r_f`, `underlying_price -> S`.

## [0.17.0] - 2026-04-26

Major release adding the Black-76 closed-form pricing model for European
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "optionstratlib"
version = "0.17.0"
version = "0.17.1"
edition = "2024"
authors = ["Joaquin Bejar <jb@taunais.com>"]
description = "OptionStratLib is a comprehensive Rust library for options trading and strategy development across multiple asset classes."
Expand Down
4 changes: 4 additions & 0 deletions examples/examples_pricing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] }
[[bin]]
name = "black_76"
path = "src/bin/black_76.rs"

[[bin]]
name = "garman_kohlhagen"
path = "src/bin/garman_kohlhagen.rs"
140 changes: 140 additions & 0 deletions examples/examples_pricing/src/bin/garman_kohlhagen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/******************************************************************************
Author: Joaquín Béjar García
Email: jb@taunais.com
Date: 2026-04-26
******************************************************************************/

//! Garman–Kohlhagen pricing example.
//!
//! Demonstrates pricing European FX options using the Garman–Kohlhagen
//! (1983) model: the Hull canonical example (USD/GBP, 4-month ATM), an
//! ITM EUR/USD scenario with FX put-call-parity check, dispatch through
//! the unified `PricingEngine`, and the symmetric-rate degenerate case.

use optionstratlib::model::types::{OptionStyle, OptionType, Side};
use optionstratlib::pricing::{PricingEngine, garman_kohlhagen, price_option};
use optionstratlib::{ExpirationDate, Options};
use positive::pos_or_panic;
use rust_decimal::MathematicalOps;
use rust_decimal_macros::dec;
use tracing::info;

fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter("info")
.without_time()
.init();

info!("=== Garman-Kohlhagen FX Pricing Examples ===");

// ---- Example 1: Hull canonical USD/GBP example ----------------------
// S = K = 1.6 USD/GBP, r_d = 0.08, r_f = 0.11, sigma = 0.2, T = 4/12
// -> call ~ 0.0639 (Hull, "Options, Futures and Other Derivatives").
info!("");
info!("Example 1: Hull canonical USD/GBP (ATM, 4-month)");
let hull_t_days = 365.0 / 3.0;
let option1 = Options::new(
OptionType::European,
Side::Long,
"GBPUSD".to_string(),
pos_or_panic!(1.6),
ExpirationDate::Days(pos_or_panic!(hull_t_days)),
pos_or_panic!(0.2),
pos_or_panic!(1.0),
pos_or_panic!(1.6),
dec!(0.08),
OptionStyle::Call,
pos_or_panic!(0.11),
None,
);
let call_price = garman_kohlhagen(&option1)?;
let mut put1 = option1.clone();
put1.option_style = OptionStyle::Put;
let put_price = garman_kohlhagen(&put1)?;

let years1 = option1.expiration_date.get_years()?.to_dec();
let df_d = (-option1.risk_free_rate * years1).exp();
let df_f = (-option1.dividend_yield.to_dec() * years1).exp();
let parity_lhs = call_price - put_price;
let parity_rhs =
option1.underlying_price.to_dec() * df_f - option1.strike_price.to_dec() * df_d;

info!(" S = 1.6, K = 1.6, r_d = 0.08, r_f = 0.11, T = 1/3 yr, sigma = 0.2");
info!(
" Call price = {} (Hull reference ~ 0.0639)",
call_price
);
info!(" Put price = {}", put_price);
info!(" C - P = {}", parity_lhs);
info!(" S e^(-r_f T) - K e^(-r_d T) = {}", parity_rhs);

// ---- Example 2: ITM EUR/USD call ------------------------------------
info!("");
info!("Example 2: ITM EUR/USD call (FX parity check)");
let option2 = Options::new(
OptionType::European,
Side::Long,
"EURUSD".to_string(),
pos_or_panic!(1.20),
ExpirationDate::Days(pos_or_panic!(180.0)),
pos_or_panic!(0.10),
pos_or_panic!(1.0),
pos_or_panic!(1.25),
dec!(0.045),
OptionStyle::Call,
pos_or_panic!(0.025),
None,
);
let call2 = garman_kohlhagen(&option2)?;
let mut put2 = option2.clone();
put2.option_style = OptionStyle::Put;
let put2_price = garman_kohlhagen(&put2)?;

let years2 = option2.expiration_date.get_years()?.to_dec();
let df_d2 = (-option2.risk_free_rate * years2).exp();
let df_f2 = (-option2.dividend_yield.to_dec() * years2).exp();
let parity2 = option2.underlying_price.to_dec() * df_f2 - option2.strike_price.to_dec() * df_d2;

info!(" S = 1.25, K = 1.20, r_d = 4.5%, r_f = 2.5%, T = 180d, sigma = 0.10");
info!(" Call price = {}", call2);
info!(" Put price = {}", put2_price);
info!(" C - P = {}", call2 - put2_price);
info!(" Expected parity = {}", parity2);

// ---- Example 3: Unified API dispatch --------------------------------
info!("");
info!("Example 3: Unified API via PricingEngine::ClosedFormGK");
let direct = garman_kohlhagen(&option2)?;
let via_engine = price_option(&option2, &PricingEngine::ClosedFormGK)?;
info!(" Direct garman_kohlhagen() = {}", direct);
info!(" Via PricingEngine dispatch = {}", via_engine);

// ---- Example 4: Symmetric rates collapse to forward parity ----------
info!("");
info!("Example 4: Symmetric rates (r_d = r_f) collapse to forward parity");
let option4 = Options::new(
OptionType::European,
Side::Long,
"USDUSD".to_string(),
pos_or_panic!(1.20),
ExpirationDate::Days(pos_or_panic!(180.0)),
pos_or_panic!(0.10),
pos_or_panic!(1.0),
pos_or_panic!(1.25),
dec!(0.04),
OptionStyle::Call,
pos_or_panic!(0.04),
None,
);
let call4 = garman_kohlhagen(&option4)?;
let mut put4 = option4.clone();
put4.option_style = OptionStyle::Put;
let put4_price = garman_kohlhagen(&put4)?;
let years4 = option4.expiration_date.get_years()?.to_dec();
let df = (-option4.risk_free_rate * years4).exp();
let collapsed = df * (option4.underlying_price.to_dec() - option4.strike_price.to_dec());
info!(" C - P = {}", call4 - put4_price);
info!(" e^(-r T) (S - K) = {}", collapsed);

Ok(())
}
14 changes: 10 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// into, so the lint is silenced in `#[cfg(test)]` only.
#![cfg_attr(test, allow(clippy::indexing_slicing))]

//! # OptionStratLib v0.17.0: Financial Options Library
//! # OptionStratLib v0.17.1: Financial Options Library
//!
//! ## Table of Contents
//! 1. [Introduction](#introduction)
Expand Down Expand Up @@ -244,6 +244,7 @@
//! Advanced pricing engines for options valuation:
//! - `black_scholes_model.rs`: European options pricing with Greeks
//! - `black_76.rs`: European options on futures/forwards (Black 1976)
//! - `garman_kohlhagen.rs`: European FX options (Garman-Kohlhagen 1983)
//! - `binomial_model.rs`: American/European options with early exercise
//! - `monte_carlo.rs`: Path-dependent and exotic options pricing
//! - `telegraph.rs`: Jump-diffusion process modeling
Expand Down Expand Up @@ -562,11 +563,16 @@
//! FWD[Forward]
//! end
//!
//! subgraph FX["FX / Currency"]
//! FX_S[FX Spot]
//! end
//!
//! BS[black_scholes] --> EU
//! BS --> PathDependent
//! BS --> MultiAsset
//! BS --> Special
//! B76[black_76] --> Forward
//! GK[garman_kohlhagen] --> FX
//! BAW[barone_adesi_whaley] --> AM
//! BIN[binomial_model] --> AM
//! BIN --> BE
Expand Down Expand Up @@ -794,7 +800,7 @@
//!
//! ```toml
//! [dependencies]
//! optionstratlib = "0.17.0"
//! optionstratlib = "0.17.1"
//! ```
//!
//! Or use cargo to add it to your project:
Expand All @@ -809,7 +815,7 @@
//!
//! ```toml
//! [dependencies]
//! optionstratlib = { version = "0.17.0", features = ["plotly"] }
//! optionstratlib = { version = "0.17.1", features = ["plotly"] }
//! ```
//!
//! - `plotly`: Enables interactive visualization using plotly.rs
Expand Down Expand Up @@ -1200,7 +1206,7 @@
//!
//! ---
//!
//! **OptionStratLib v0.17.0** - Built with ❤️ in Rust for the financial community
//! **OptionStratLib v0.17.1** - Built with ❤️ in Rust for the financial community
//!

/// # OptionsStratLib: Financial Options Trading Library
Expand Down
Loading
Loading