| import math |
| import logging |
| import pandas as pd |
| import numpy as np |
| from typing import Dict, Any, Optional |
|
|
| logger = logging.getLogger("bqe.engine.volatility_engine") |
|
|
| def std_normal_cdf(x: float) -> float: |
| """ |
| Standard normal cumulative distribution function (CDF) approximation using math.erf. |
| """ |
| return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) |
|
|
| def black_scholes_option_price( |
| S: float, |
| K: float, |
| T: float, |
| r: float, |
| sigma: float, |
| is_call: bool |
| ) -> float: |
| """ |
| Prices an option using the standard Black-Scholes-Merton formula. |
| |
| Args: |
| S: Spot price of the underlying asset. |
| K: Strike price of the option contract. |
| T: Time to expiration in years. |
| r: Annual risk-free interest rate (e.g. 0.07 for 7%). |
| sigma: Implied Volatility (annualized, e.g. 0.20 for 20%). |
| is_call: True for Call option ('CE'), False for Put option ('PE'). |
| |
| Returns: |
| float: Theoretical option premium price. |
| """ |
| if T <= 0.0 or sigma <= 0.0: |
| |
| if is_call: |
| return max(0.0, S - K) |
| else: |
| return max(0.0, K - S) |
| |
| d1 = (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * math.sqrt(T)) |
| d2 = d1 - sigma * math.sqrt(T) |
| |
| if is_call: |
| return S * std_normal_cdf(d1) - K * math.exp(-r * T) * std_normal_cdf(d2) |
| else: |
| return K * math.exp(-r * T) * std_normal_cdf(-d2) - S * std_normal_cdf(-d1) |
|
|
|
|
| class DerivativeAnalyticsEngine: |
| """ |
| Unified Derivative Analytics Engine for index option contracts. |
| Calculates Put-Call Ratios and extracts Implied Volatility (IV). |
| """ |
|
|
| def calculate_pcr_vectors(self, instruments_df: pd.DataFrame) -> Dict[str, float]: |
| """ |
| Groups option instruments by expiry date and calculates the Put-Call Ratio (PCR). |
| Utilizes 'volume' (or 'open_interest' / counts as fallback). |
| |
| Args: |
| instruments_df: Options instrument contracts DataFrame. |
| |
| Returns: |
| Dict[str, float]: Expiry date keys mapped to Put-Call Ratio values. |
| """ |
| pcr_dict = {} |
| if instruments_df.empty: |
| logger.warning("Empty instruments DataFrame passed to PCR calculator.") |
| return pcr_dict |
|
|
| |
| grouped = instruments_df.groupby('expiry') |
| |
| for expiry, group in grouped: |
| |
| puts = group[group['instrument_type'] == 'PE'] |
| calls = group[group['instrument_type'] == 'CE'] |
| |
| |
| if 'volume' in group.columns and group['volume'].sum() > 0: |
| put_vol = puts['volume'].sum() |
| call_vol = calls['volume'].sum() |
| elif 'open_interest' in group.columns and group['open_interest'].sum() > 0: |
| put_vol = puts['open_interest'].sum() |
| call_vol = calls['open_interest'].sum() |
| else: |
| put_vol = len(puts) |
| call_vol = len(calls) |
| |
| pcr = float(put_vol / call_vol) if call_vol > 0 else 0.0 |
| pcr_dict[str(expiry)] = pcr |
| |
| logger.info(f"Calculated PCR vectors across {len(pcr_dict)} unique expiration dates.") |
| return pcr_dict |
|
|
| def calculate_atm_iv( |
| self, |
| spot_price: float, |
| option_premium: float, |
| strike: float, |
| time_to_expiry: float, |
| is_call: bool, |
| risk_free_rate: float = 0.07 |
| ) -> float: |
| """ |
| Backs out the annualized Implied Volatility (IV) from standard market option premium |
| using an iterative numerical Bisection solver. |
| |
| Args: |
| spot_price: Underlying price. |
| option_premium: Market option price. |
| strike: Strike price. |
| time_to_expiry: Time to expiration in years. |
| is_call: True if Call ('CE'), False if Put ('PE'). |
| risk_free_rate: Risk free rate (default 0.07). |
| |
| Returns: |
| float: Annualized Implied Volatility (e.g. 0.165 for 16.5% IV). |
| """ |
| |
| intrinsic_val = (spot_price - strike) if is_call else (strike - spot_price) |
| intrinsic_val = max(0.0, intrinsic_val) |
| |
| if option_premium <= intrinsic_val: |
| logger.debug("Option premium is below or equal to intrinsic value. Returning 0 IV.") |
| return 0.0 |
| |
| |
| low = 1e-6 |
| high = 5.0 |
| max_iter = 100 |
| tolerance = 1e-5 |
| |
| for i in range(max_iter): |
| mid = 0.5 * (low + high) |
| theoretical_price = black_scholes_option_price( |
| S=spot_price, |
| K=strike, |
| T=time_to_expiry, |
| r=risk_free_rate, |
| sigma=mid, |
| is_call=is_call |
| ) |
| |
| |
| if abs(theoretical_price - option_premium) < tolerance: |
| return mid |
| |
| if theoretical_price < option_premium: |
| low = mid |
| else: |
| high = mid |
| |
| |
| return 0.5 * (low + high) |
|
|