1use crate::black_scholes::{black_scholes, calculate_greeks};
9use crate::types::OptionType;
10
11pub fn black_76_price(
18 option_type: &OptionType,
19 forward: f64,
20 strike: f64,
21 time_to_expiry: f64,
22 risk_free_rate: f64,
23 volatility: f64,
24) -> f64 {
25 if time_to_expiry <= 0.0 {
26 return match option_type {
27 OptionType::Call => (forward - strike).max(0.0),
28 OptionType::Put => (strike - forward).max(0.0),
29 };
30 }
31
32 let discount = (-risk_free_rate * time_to_expiry).exp();
33 discount
34 * black_scholes(
35 option_type,
36 forward,
37 strike,
38 time_to_expiry,
39 0.0,
40 volatility,
41 )
42}
43
44pub fn black_76_implied_vol(
48 option_type: &OptionType,
49 forward: f64,
50 strike: f64,
51 time_to_expiry: f64,
52 risk_free_rate: f64,
53 market_price: f64,
54) -> Option<f64> {
55 let undiscounted_price = market_price * (risk_free_rate * time_to_expiry).exp();
56 calculate_implied_volatility(
57 option_type,
58 forward,
59 strike,
60 time_to_expiry,
61 0.0,
62 undiscounted_price,
63 None,
64 )
65}
66
67fn calculate_implied_volatility(
68 option_type: &OptionType,
69 spot: f64,
70 strike: f64,
71 time_to_expiry: f64,
72 risk_free_rate: f64,
73 market_price: f64,
74 initial_vol: Option<f64>,
75) -> Option<f64> {
76 if time_to_expiry <= 0.0 {
77 return None;
78 }
79
80 let intrinsic = match option_type {
81 OptionType::Call => (spot - strike).max(0.0),
82 OptionType::Put => (strike - spot).max(0.0),
83 };
84 if market_price < intrinsic {
85 return None;
86 }
87
88 let mut vol = initial_vol.unwrap_or(0.3);
89 let max_iterations = 50;
90 let tolerance = 1e-6;
91
92 for _ in 0..max_iterations {
93 let (price, _, _, vega, _) = calculate_greeks(
94 option_type,
95 spot,
96 strike,
97 time_to_expiry,
98 risk_free_rate,
99 vol,
100 );
101 let price_diff = price - market_price;
102
103 if price_diff.abs() < tolerance {
104 return Some(vol);
105 }
106
107 let vega_full = vega * 100.0;
108 if vega_full.abs() < 1e-10 {
109 break;
110 }
111
112 vol -= price_diff / vega_full;
113 vol = vol.clamp(0.001, 5.0);
114 }
115
116 None
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn black_76_implied_vol_roundtrips_a_known_sigma() {
125 let forward = 4700.0;
126 let strike = 4700.0;
127 let tau = 0.1;
128 let r = 0.05;
129 let sigma = 0.35;
130
131 let call_price = black_76_price(&OptionType::Call, forward, strike, tau, r, sigma);
132 let recovered =
133 black_76_implied_vol(&OptionType::Call, forward, strike, tau, r, call_price)
134 .expect("IV solver must converge for a well-formed input");
135
136 assert!(
137 (recovered - sigma).abs() < 1e-5,
138 "roundtrip failed: input sigma = {sigma}, recovered = {recovered}",
139 );
140 }
141
142 #[test]
143 fn black_76_put_call_parity_holds_otm() {
144 let forward = 4700.0;
145 let strike = 4800.0;
146 let tau = 0.25;
147 let r = 0.05;
148 let sigma = 0.35;
149
150 let call = black_76_price(&OptionType::Call, forward, strike, tau, r, sigma);
151 let put = black_76_price(&OptionType::Put, forward, strike, tau, r, sigma);
152 let discount = (-r * tau).exp();
153 let rhs = discount * (forward - strike);
154
155 assert!(
156 ((call - put) - rhs).abs() < 1e-9,
157 "put-call parity failed: C - P = {}, expected {}",
158 call - put,
159 rhs,
160 );
161 }
162
163 #[test]
164 fn black_76_implied_vol_returns_none_below_intrinsic() {
165 let forward = 5000.0;
166 let strike = 4000.0;
167 let tau = 0.25;
168 let r = 0.05;
169 let impossible_price = 500.0;
170
171 let result =
172 black_76_implied_vol(&OptionType::Call, forward, strike, tau, r, impossible_price);
173 assert!(
174 result.is_none(),
175 "solver should refuse arb-violating prices, got {result:?}",
176 );
177 }
178
179 #[test]
180 fn black_76_implied_vol_returns_none_at_zero_tau() {
181 let result = black_76_implied_vol(&OptionType::Call, 4700.0, 4700.0, 0.0, 0.05, 0.0);
182 assert!(result.is_none(), "zero tau must yield None");
183 }
184
185 #[test]
186 fn black_76_price_returns_intrinsic_at_or_past_expiry() {
187 let call_zero = black_76_price(&OptionType::Call, 4700.0, 4500.0, 0.0, 0.05, 0.35);
188 assert!(
189 (call_zero - 200.0).abs() < 1e-9,
190 "expired ITM call must equal intrinsic, got {call_zero}"
191 );
192
193 let call_neg = black_76_price(&OptionType::Call, 4700.0, 4500.0, -0.01, 0.05, 0.35);
194 assert!(
195 (call_neg - 200.0).abs() < 1e-9,
196 "past-expiry ITM call must equal intrinsic, got {call_neg}"
197 );
198
199 let otm_call = black_76_price(&OptionType::Call, 4500.0, 4700.0, 0.0, 0.05, 0.35);
200 assert!(
201 otm_call.abs() < 1e-9,
202 "expired OTM call must be zero, got {otm_call}"
203 );
204
205 let put_zero = black_76_price(&OptionType::Put, 4500.0, 4700.0, 0.0, 0.05, 0.35);
206 assert!(
207 (put_zero - 200.0).abs() < 1e-9,
208 "expired ITM put must equal intrinsic, got {put_zero}"
209 );
210 }
211}