hypercall/shared/
black_scholes_utils.rs1use crate::types::OptionType;
2use hypercall_types::Greeks;
3
4fn normal_cdf(x: f64) -> f64 {
5 0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
6}
7
8fn normal_pdf(x: f64) -> f64 {
9 (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
10}
11
12fn adjusted_cdf(x: f64, skew: f64, excess_kurtosis: f64) -> f64 {
14 if skew == 0.0 && excess_kurtosis == 0.0 {
15 return normal_cdf(x);
16 }
17
18 let base_cdf = normal_cdf(x);
20
21 let pdf = normal_pdf(x);
24 let skew_adjustment = skew * pdf * (x * x - 1.0) / 6.0;
25
26 let kurtosis_adjustment = excess_kurtosis * pdf * (x * x * x * x - 6.0 * x * x + 3.0) / 24.0;
29
30 #[allow(clippy::manual_clamp)]
32 (base_cdf + skew_adjustment + kurtosis_adjustment)
33 .max(0.0)
34 .min(1.0)
35}
36
37#[allow(dead_code)]
39fn adjusted_pdf(x: f64, skew: f64, excess_kurtosis: f64) -> f64 {
40 if skew == 0.0 && excess_kurtosis == 0.0 {
41 return normal_pdf(x);
42 }
43
44 let n = normal_pdf(x);
45 let adjustment = 1.0
46 + (skew / 6.0) * (x * x * x - 3.0 * x)
47 + (excess_kurtosis / 24.0) * (x * x * x * x - 6.0 * x * x + 3.0)
48 + (skew * skew / 72.0)
49 * (x * x * x * x * x * x - 15.0 * x * x * x * x + 45.0 * x * x - 15.0);
50
51 n * adjustment.max(0.0) }
53
54pub fn black_scholes(
55 option_type: &OptionType,
56 spot: f64,
57 strike: f64,
58 time_to_expiry: f64,
59 risk_free_rate: f64,
60 volatility: f64,
61) -> f64 {
62 black_scholes_with_moments(
63 option_type,
64 spot,
65 strike,
66 time_to_expiry,
67 risk_free_rate,
68 volatility,
69 0.0,
70 0.0,
71 )
72}
73
74pub fn black_scholes_with_moments(
75 option_type: &OptionType,
76 spot: f64,
77 strike: f64,
78 time_to_expiry: f64,
79 risk_free_rate: f64,
80 volatility: f64,
81 skew: f64,
82 excess_kurtosis: f64,
83) -> f64 {
84 if time_to_expiry <= 0.0 {
85 return match option_type {
86 OptionType::Call => (spot - strike).max(0.0),
87 OptionType::Put => (strike - spot).max(0.0),
88 };
89 }
90
91 let d1 = ((spot / strike).ln()
92 + (risk_free_rate + 0.5 * volatility * volatility) * time_to_expiry)
93 / (volatility * time_to_expiry.sqrt());
94 let d2 = d1 - volatility * time_to_expiry.sqrt();
95
96 match option_type {
97 OptionType::Call => {
98 spot * adjusted_cdf(d1, skew, excess_kurtosis)
99 - strike
100 * (-risk_free_rate * time_to_expiry).exp()
101 * adjusted_cdf(d2, skew, excess_kurtosis)
102 }
103 OptionType::Put => {
104 strike
105 * (-risk_free_rate * time_to_expiry).exp()
106 * adjusted_cdf(-d2, skew, excess_kurtosis)
107 - spot * adjusted_cdf(-d1, skew, excess_kurtosis)
108 }
109 }
110}
111
112pub fn calculate_greeks(
113 option_type: &OptionType,
114 spot: f64,
115 strike: f64,
116 time_to_expiry: f64,
117 risk_free_rate: f64,
118 volatility: f64,
119) -> Greeks {
120 if time_to_expiry <= 0.0 {
121 let intrinsic = match option_type {
122 OptionType::Call => (spot - strike).max(0.0),
123 OptionType::Put => (strike - spot).max(0.0),
124 };
125 let delta = match option_type {
126 OptionType::Call => {
127 if spot > strike {
128 1.0
129 } else {
130 0.0
131 }
132 }
133 OptionType::Put => {
134 if spot < strike {
135 -1.0
136 } else {
137 0.0
138 }
139 }
140 };
141 return Greeks {
142 delta,
143 gamma: 0.0,
144 theta: 0.0,
145 vega: 0.0,
146 rho: 0.0,
147 implied_vol: volatility,
148 theoretical_price: intrinsic,
149 market_mid_price: None,
150 };
151 }
152
153 let sqrt_t = time_to_expiry.sqrt();
154 let d1 = ((spot / strike).ln()
155 + (risk_free_rate + 0.5 * volatility * volatility) * time_to_expiry)
156 / (volatility * sqrt_t);
157 let d2 = d1 - volatility * sqrt_t;
158 let discount_factor = (-risk_free_rate * time_to_expiry).exp();
159
160 let price = match option_type {
161 OptionType::Call => spot * normal_cdf(d1) - strike * discount_factor * normal_cdf(d2),
162 OptionType::Put => strike * discount_factor * normal_cdf(-d2) - spot * normal_cdf(-d1),
163 };
164
165 let delta = match option_type {
166 OptionType::Call => normal_cdf(d1),
167 OptionType::Put => -normal_cdf(-d1),
168 };
169
170 let gamma = normal_pdf(d1) / (spot * volatility * sqrt_t);
171 let vega = spot * normal_pdf(d1) * sqrt_t / 100.0; let theta = match option_type {
173 OptionType::Call => {
174 (-spot * normal_pdf(d1) * volatility / (2.0 * sqrt_t)
175 - risk_free_rate * strike * discount_factor * normal_cdf(d2))
176 / 365.0
177 }
178 OptionType::Put => {
179 (-spot * normal_pdf(d1) * volatility / (2.0 * sqrt_t)
180 + risk_free_rate * strike * discount_factor * normal_cdf(-d2))
181 / 365.0
182 }
183 };
184
185 let rho = match option_type {
186 OptionType::Call => time_to_expiry * strike * discount_factor * normal_cdf(d2) / 100.0,
187 OptionType::Put => -time_to_expiry * strike * discount_factor * normal_cdf(-d2) / 100.0,
188 };
189
190 Greeks {
191 delta,
192 gamma,
193 theta,
194 vega,
195 rho,
196 implied_vol: volatility,
197 theoretical_price: price,
198 market_mid_price: None,
199 }
200}
201
202pub fn calculate_implied_volatility(
204 option_type: &OptionType,
205 spot: f64,
206 strike: f64,
207 time_to_expiry: f64,
208 risk_free_rate: f64,
209 market_price: f64,
210 initial_vol: Option<f64>,
211) -> Option<f64> {
212 if time_to_expiry <= 0.0 {
213 return None;
214 }
215
216 let intrinsic = match option_type {
218 OptionType::Call => (spot - strike).max(0.0),
219 OptionType::Put => (strike - spot).max(0.0),
220 };
221
222 if market_price < intrinsic {
223 return None;
224 }
225
226 let mut vol = initial_vol.unwrap_or(0.3); let max_iterations = 50;
228 let tolerance = 1e-6;
229
230 for _ in 0..max_iterations {
231 let greeks = calculate_greeks(
232 option_type,
233 spot,
234 strike,
235 time_to_expiry,
236 risk_free_rate,
237 vol,
238 );
239 let price_diff = greeks.theoretical_price - market_price;
240
241 if price_diff.abs() < tolerance {
242 return Some(vol);
243 }
244
245 let vega_full = greeks.vega * 100.0; if vega_full.abs() < 1e-10 {
248 break;
249 }
250
251 vol -= price_diff / vega_full;
252
253 #[allow(clippy::manual_clamp)]
255 {
256 vol = vol.max(0.001).min(5.0);
257 }
258 }
259
260 None
261}