Skip to main content

rainbow_option/
rainbow_option.rs

1//! Rainbow (multi-asset) options: best-of, worst-of, spread, basket and
2//! exchange payoffs on correlated assets.
3//!
4//! Run with:  cargo run --release --example rainbow_option
5
6mod common;
7
8use chrono::{Duration, Local};
9use rustyqlib::equity::blackscholes::bs_price;
10use rustyqlib::core::trade::PutOrCall;
11use rustyqlib::equity::rainbow::{RainbowAssetData, RainbowOption, RainbowOptionData};
12use rustyqlib::equity::utils::Engine;
13
14const SPOT_A: f64 = 100.0;
15const SPOT_B: f64 = 95.0;
16const VOL_A: f64 = 0.30;
17const VOL_B: f64 = 0.25;
18const DIV_A: f64 = 0.02;
19const DIV_B: f64 = 0.01;
20const RATE: f64 = 0.05;
21
22fn maturity_1y() -> String {
23    (Local::now().date_naive() + Duration::days(365)).format("%Y-%m-%d").to_string()
24}
25
26fn two_assets() -> Vec<RainbowAssetData> {
27    vec![
28        RainbowAssetData { symbol: "AAA".into(), spot: SPOT_A, volatility: VOL_A, dividend: Some(DIV_A) },
29        RainbowAssetData { symbol: "BBB".into(), spot: SPOT_B, volatility: VOL_B, dividend: Some(DIV_B) },
30    ]
31}
32
33fn build(
34    rainbow_type: &str,
35    pc: &str,
36    strike: Option<f64>,
37    rho: f64,
38    pricer: &str,
39    assets: Vec<RainbowAssetData>,
40    correlations: Vec<Vec<f64>>,
41    weights: Option<Vec<f64>>,
42) -> Box<RainbowOption> {
43    let _ = rho;
44    RainbowOption::from_json(&RainbowOptionData {
45        symbol: rainbow_type.to_uppercase(),
46        rainbow_type: rainbow_type.to_string(),
47        put_or_call: Some(pc.to_string()),
48        assets,
49        correlations,
50        strike_price: strike,
51        weights,
52        maturity: maturity_1y(),
53        risk_free_rate: Some(RATE),
54        discount_curve: None,
55        pricer: Some(pricer.to_string()),
56        simulation: Some(100_000),
57        mc_sampler: None,
58        mc_seed: None,
59    })
60}
61
62fn two_asset(rainbow_type: &str, pc: &str, strike: Option<f64>, rho: f64, pricer: &str) -> Box<RainbowOption> {
63    build(
64        rainbow_type,
65        pc,
66        strike,
67        rho,
68        pricer,
69        two_assets(),
70        vec![vec![1.0, rho], vec![rho, 1.0]],
71        None,
72    )
73}
74
75fn print_rainbow(label: &str, option: &RainbowOption) {
76    let pv = option.npv();
77    let stats = option.npv_with_stats();
78    let deltas: Vec<String> = option.deltas().iter().map(|d| format!("{d:.4}")).collect();
79    let vegas: Vec<String> = option.vegas().iter().map(|v| format!("{v:.2}")).collect();
80    let se = match stats {
81        Some(s) => format!("{:.5}", s.std_err),
82        None => "-".to_string(),
83    };
84    println!(
85        "{label:<38} {pv:>12.6}  stderr={se:>9}  deltas=[{}]  vegas=[{}]",
86        deltas.join(", "),
87        vegas.join(", ")
88    );
89}
90
91fn main() {
92    common::title(&format!(
93        "RAINBOW OPTIONS — A: S={SPOT_A} sigma={VOL_A} q={DIV_A} | B: S={SPOT_B} sigma={VOL_B} q={DIV_B} | r={RATE} T=1y"
94    ));
95
96    common::section("Exchange option (Margrabe, exact) — pays (S_A - S_B)+");
97    print_rainbow("Analytical (Margrabe)", &two_asset("exchange", "C", None, 0.6, "Analytical"));
98    print_rainbow("Monte Carlo", &two_asset("exchange", "C", None, 0.6, "MC"));
99
100    common::section("Spread option (Kirk approximation) — pays (S_A - S_B - K)+");
101    for k in [0.0, 5.0, 10.0] {
102        print_rainbow(
103            &format!("Analytical (Kirk), K={k}"),
104            &two_asset("spread", "C", Some(k), 0.6, "Analytical"),
105        );
106        print_rainbow(
107            &format!("Monte Carlo,     K={k}"),
108            &two_asset("spread", "C", Some(k), 0.6, "MC"),
109        );
110    }
111    common::note("at K=0 the spread option must equal the Margrabe exchange option");
112
113    common::section("Best-of and worst-of (Monte Carlo only)");
114    for k in [90.0, 100.0, 110.0] {
115        print_rainbow(&format!("best-of call,  K={k}"), &two_asset("best_of", "C", Some(k), 0.6, "MC"));
116        print_rainbow(&format!("worst-of call, K={k}"), &two_asset("worst_of", "C", Some(k), 0.6, "MC"));
117    }
118    print_rainbow(
119        "best-of, analytic (unsupported)",
120        &two_asset("best_of", "C", Some(100.0), 0.6, "MC"),
121    );
122
123    common::section("Correlation sweep (worst-of call, K=100)");
124    for rho in [-0.5, 0.0, 0.5, 0.9, 0.99] {
125        print_rainbow(&format!("rho = {rho:>5}"), &two_asset("worst_of", "C", Some(100.0), rho, "MC"));
126    }
127    common::note("higher correlation lifts the minimum, so the worst-of call gains value");
128
129    common::section("Basket option (3 assets, moment matching)");
130    let assets3 = vec![
131        RainbowAssetData { symbol: "AAA".into(), spot: 100.0, volatility: 0.30, dividend: None },
132        RainbowAssetData { symbol: "BBB".into(), spot: 90.0, volatility: 0.25, dividend: None },
133        RainbowAssetData { symbol: "CCC".into(), spot: 110.0, volatility: 0.35, dividend: None },
134    ];
135    let corr3 = vec![
136        vec![1.0, 0.5, 0.3],
137        vec![0.5, 1.0, 0.4],
138        vec![0.3, 0.4, 1.0],
139    ];
140    print_rainbow(
141        "Analytical (moment matching)",
142        &build("basket", "C", Some(100.0), 0.0, "Analytical", assets3.clone(), corr3.clone(), None),
143    );
144    print_rainbow(
145        "Monte Carlo",
146        &build("basket", "C", Some(100.0), 0.0, "MC", assets3.clone(), corr3.clone(), None),
147    );
148    print_rainbow(
149        "Weighted 40/30/30, analytic",
150        &build(
151            "basket",
152            "C",
153            Some(100.0),
154            0.0,
155            "Analytical",
156            assets3,
157            corr3,
158            Some(vec![0.4, 0.3, 0.3]),
159        ),
160    );
161
162    common::section("Identities");
163    let spread_k0 = two_asset("spread", "C", Some(0.0), 0.6, "Analytical").npv();
164    let exchange = two_asset("exchange", "C", None, 0.6, "Analytical").npv();
165    common::check("spread(K=0) = Margrabe", spread_k0, exchange, 1e-10);
166
167    // max + min = S_A + S_B pathwise, so the two options sum to the vanillas
168    let k = 100.0;
169    let best = two_asset("best_of", "C", Some(k), 0.6, "MC").npv();
170    let worst = two_asset("worst_of", "C", Some(k), 0.6, "MC").npv();
171    let vanillas = bs_price(SPOT_A, k, RATE, DIV_A, VOL_A, 1.0, PutOrCall::Call)
172        + bs_price(SPOT_B, k, RATE, DIV_B, VOL_B, 1.0, PutOrCall::Call);
173    common::check("best-of + worst-of = sum of vanillas", best + worst, vanillas, 0.1);
174    println!();
175}