Skip to main content

hypercall/shared/
black_scholes_utils.rs

1use 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
12// Adjusted CDF using a simplified approach for skew and kurtosis
13fn 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    // Get base normal CDF value
19    let base_cdf = normal_cdf(x);
20
21    // Apply skew adjustment using Edgeworth expansion approximation
22    // Positive skew increases right tail probability
23    let pdf = normal_pdf(x);
24    let skew_adjustment = skew * pdf * (x * x - 1.0) / 6.0;
25
26    // Apply kurtosis adjustment
27    // Positive excess kurtosis increases tail probabilities
28    let kurtosis_adjustment = excess_kurtosis * pdf * (x * x * x * x - 6.0 * x * x + 3.0) / 24.0;
29
30    // Apply adjustments and ensure result is in [0, 1]
31    #[allow(clippy::manual_clamp)]
32    (base_cdf + skew_adjustment + kurtosis_adjustment)
33        .max(0.0)
34        .min(1.0)
35}
36
37// Adjusted PDF for skew and kurtosis
38#[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) // Ensure non-negative PDF
52}
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; // Divided by 100 for 1% vol move
172    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
202// Calculate implied volatility using Newton-Raphson method
203pub 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    // Check if price is within valid bounds
217    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); // Start with 30% if no initial guess
227    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        // Newton-Raphson update
246        let vega_full = greeks.vega * 100.0; // Convert back to full vega
247        if vega_full.abs() < 1e-10 {
248            break;
249        }
250
251        vol -= price_diff / vega_full;
252
253        // Keep volatility in reasonable bounds
254        #[allow(clippy::manual_clamp)]
255        {
256            vol = vol.max(0.001).min(5.0);
257        }
258    }
259
260    None
261}