Skip to main content

asian_option/
asian_option.rs

1//! Asian (average) options: arithmetic / geometric, fixed / floating strike.
2//!
3//! Run with:  cargo run --release --example asian_option
4
5mod common;
6
7use chrono::NaiveDate;
8use rustyqlib::core::trade::PutOrCall;
9use rustyqlib::core::traits::Instrument;
10use rustyqlib::equity::asian::{
11    geometric_asian_price, turnbull_wakeman_price, AsianStrikeType, AveragingType,
12};
13use rustyqlib::equity::builder::EquityOptionBuilder;
14use rustyqlib::equity::montecarlo::DiscretizationScheme;
15use rustyqlib::equity::utils::Engine;
16
17const SPOT: f64 = 100.0;
18const STRIKE: f64 = 100.0;
19const VOL: f64 = 0.30;
20const RATE: f64 = 0.05;
21const DIV: f64 = 0.02;
22
23fn base() -> EquityOptionBuilder {
24    EquityOptionBuilder::new()
25        .symbol("ASIAN")
26        .spot(SPOT)
27        .strike(STRIKE)
28        .flat_vol(VOL)
29        .flat_rate(RATE)
30        .dividend_yield(DIV)
31        .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
32        .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
33}
34
35fn main() {
36    common::title("ASIAN OPTIONS — S=100 K=100 sigma=30% r=5% q=2% T=1y");
37
38    common::section("Fixed strike (average price) call");
39    common::table_header();
40    common::row(
41        "Geometric, analytic (exact)",
42        &base()
43            .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
44            .engine(Engine::BlackScholes)
45            .build(),
46    );
47    common::row(
48        "Geometric, Monte Carlo",
49        &base()
50            .asian(PutOrCall::Call, AveragingType::Geometric, AsianStrikeType::FixedStrike)
51            .engine(Engine::MonteCarlo)
52            .paths(50_000)
53            .build(),
54    );
55    common::row(
56        "Arithmetic, Turnbull-Wakeman",
57        &base()
58            .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
59            .engine(Engine::BlackScholes)
60            .build(),
61    );
62    common::row(
63        "Arithmetic, MC + geometric CV",
64        &base()
65            .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
66            .engine(Engine::MonteCarlo)
67            .paths(50_000)
68            .build(),
69    );
70
71    common::section("Control variate effect (same path count)");
72    common::table_header();
73    let with_cv = base()
74        .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
75        .engine(Engine::MonteCarlo)
76        .paths(20_000)
77        .build();
78    let without_cv = base()
79        .asian(PutOrCall::Call, AveragingType::Arithmetic, AsianStrikeType::FixedStrike)
80        .engine(Engine::MonteCarlo)
81        .paths(20_000)
82        .mc_config({
83            // Euler stepping disables the control variate precondition
84            let mut c = rustyqlib::equity::montecarlo::MonteCarloConfig::default();
85            c.paths = 20_000;
86            c.scheme = DiscretizationScheme::Euler;
87            c.time_steps = 100;
88            c
89        })
90        .build();
91    common::row("with geometric control variate", &with_cv);
92    common::row("without (Euler path route)", &without_cv);
93    common::note("compare the std err column: the CV collapses the variance");
94
95    common::section("Floating strike (average strike)");
96    common::table_header();
97    for pc in [PutOrCall::Call, PutOrCall::Put] {
98        common::row(
99            &format!("Monte Carlo, {pc:?}"),
100            &base()
101                .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
102                .engine(Engine::MonteCarlo)
103                .paths(50_000)
104                .build(),
105        );
106        common::row(
107            &format!("Analytic (unsupported), {pc:?}"),
108            &base()
109                .asian(pc, AveragingType::Arithmetic, AsianStrikeType::FloatingStrike)
110                .engine(Engine::BlackScholes)
111                .build(),
112        );
113    }
114
115    common::section("Orderings and limits");
116    let vanilla = base().vanilla(PutOrCall::Call).engine(Engine::BlackScholes).build().npv();
117    let geo = geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, None, PutOrCall::Call);
118    let arith = turnbull_wakeman_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, PutOrCall::Call);
119    println!("  geometric {geo:.6} < arithmetic {arith:.6} < vanilla {vanilla:.6}");
120    common::note("AM-GM: the arithmetic average dominates the geometric one");
121    common::note("averaging reduces effective volatility (sigma^2 T / 3), so both sit below vanilla");
122    common::check(
123        "discrete geometric (n=1e5) -> continuous",
124        geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(100_000), PutOrCall::Call),
125        geo,
126        1e-3,
127    );
128
129    common::section("Averaging frequency (geometric, exact)");
130    for n in [4usize, 12, 52, 252] {
131        let price =
132            geometric_asian_price(SPOT, STRIKE, RATE, DIV, VOL, 1.0, Some(n), PutOrCall::Call);
133        println!("  {n:>4} fixings: {price:.6}");
134    }
135    println!();
136}