1mod 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 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}