Initial commit: Vault Dashboard for options hedging
- FastAPI + NiceGUI web application - QuantLib-based Black-Scholes pricing with Greeks - Protective put, laddered, and LEAPS strategies - Real-time WebSocket updates - TradingView-style charts via Lightweight-Charts - Docker containerization - GitLab CI/CD pipeline for VPS deployment - VPN-only access configuration
This commit is contained in:
127
app/core/pricing/volatility.py
Normal file
127
app/core/pricing/volatility.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
from typing import Literal
|
||||
|
||||
import QuantLib as ql
|
||||
|
||||
OptionType = Literal["call", "put"]
|
||||
|
||||
DEFAULT_RISK_FREE_RATE: float = 0.045
|
||||
DEFAULT_VOLATILITY_GUESS: float = 0.16
|
||||
DEFAULT_DIVIDEND_YIELD: float = 0.0
|
||||
|
||||
|
||||
def _validate_option_type(option_type: str) -> OptionType:
|
||||
option = option_type.lower()
|
||||
if option not in {"call", "put"}:
|
||||
raise ValueError("option_type must be either 'call' or 'put'")
|
||||
return option # type: ignore[return-value]
|
||||
|
||||
|
||||
def _to_quantlib_option_type(option_type: OptionType) -> ql.Option.Type:
|
||||
return ql.Option.Call if option_type == "call" else ql.Option.Put
|
||||
|
||||
|
||||
def implied_volatility(
|
||||
option_price: float,
|
||||
spot: float,
|
||||
strike: float,
|
||||
time_to_expiry: float,
|
||||
risk_free_rate: float = DEFAULT_RISK_FREE_RATE,
|
||||
option_type: OptionType = "put",
|
||||
dividend_yield: float = DEFAULT_DIVIDEND_YIELD,
|
||||
valuation_date: date | None = None,
|
||||
initial_guess: float = DEFAULT_VOLATILITY_GUESS,
|
||||
min_vol: float = 1e-4,
|
||||
max_vol: float = 4.0,
|
||||
accuracy: float = 1e-8,
|
||||
max_evaluations: int = 500,
|
||||
) -> float:
|
||||
"""Invert the Black-Scholes-Merton model to solve for implied volatility.
|
||||
|
||||
Assumptions:
|
||||
- European option exercise
|
||||
- Flat rate, dividend, and volatility term structures
|
||||
- GLD dividend yield defaults to zero
|
||||
|
||||
Args:
|
||||
option_price: Observed market premium.
|
||||
spot: Current underlying price.
|
||||
strike: Option strike price.
|
||||
time_to_expiry: Time to maturity in years.
|
||||
risk_free_rate: Annual risk-free rate.
|
||||
option_type: ``"call"`` or ``"put"``.
|
||||
dividend_yield: Continuous dividend yield.
|
||||
valuation_date: Pricing date, defaults to today.
|
||||
initial_guess: Starting volatility guess used in the pricing process.
|
||||
min_vol: Lower volatility search bound.
|
||||
max_vol: Upper volatility search bound.
|
||||
accuracy: Root-finding tolerance.
|
||||
max_evaluations: Maximum solver iterations.
|
||||
|
||||
Returns:
|
||||
The annualized implied volatility as a decimal.
|
||||
|
||||
Example:
|
||||
>>> vol = implied_volatility(
|
||||
... option_price=12.0,
|
||||
... spot=460.0,
|
||||
... strike=430.0,
|
||||
... time_to_expiry=0.5,
|
||||
... risk_free_rate=0.045,
|
||||
... option_type="put",
|
||||
... )
|
||||
>>> vol > 0
|
||||
True
|
||||
"""
|
||||
|
||||
if option_price <= 0 or spot <= 0 or strike <= 0 or time_to_expiry <= 0:
|
||||
raise ValueError("option_price, spot, strike, and time_to_expiry must be positive")
|
||||
if initial_guess <= 0 or min_vol <= 0 or max_vol <= min_vol:
|
||||
raise ValueError("invalid volatility bounds or initial_guess")
|
||||
|
||||
option_type = _validate_option_type(option_type)
|
||||
valuation = valuation_date or date.today()
|
||||
maturity = valuation + timedelta(days=max(1, round(time_to_expiry * 365)))
|
||||
|
||||
valuation_ql = ql.Date(valuation.day, valuation.month, valuation.year)
|
||||
maturity_ql = ql.Date(maturity.day, maturity.month, maturity.year)
|
||||
ql.Settings.instance().evaluationDate = valuation_ql
|
||||
|
||||
day_count = ql.Actual365Fixed()
|
||||
calendar = ql.NullCalendar()
|
||||
|
||||
spot_handle = ql.QuoteHandle(ql.SimpleQuote(spot))
|
||||
dividend_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, dividend_yield, day_count)
|
||||
)
|
||||
risk_free_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, risk_free_rate, day_count)
|
||||
)
|
||||
volatility_curve = ql.BlackVolTermStructureHandle(
|
||||
ql.BlackConstantVol(valuation_ql, calendar, initial_guess, day_count)
|
||||
)
|
||||
|
||||
process = ql.BlackScholesMertonProcess(
|
||||
spot_handle,
|
||||
dividend_curve,
|
||||
risk_free_curve,
|
||||
volatility_curve,
|
||||
)
|
||||
|
||||
payoff = ql.PlainVanillaPayoff(_to_quantlib_option_type(option_type), strike)
|
||||
exercise = ql.EuropeanExercise(maturity_ql)
|
||||
option = ql.VanillaOption(payoff, exercise)
|
||||
option.setPricingEngine(ql.AnalyticEuropeanEngine(process))
|
||||
|
||||
return float(
|
||||
option.impliedVolatility(
|
||||
option_price,
|
||||
process,
|
||||
accuracy,
|
||||
max_evaluations,
|
||||
min_vol,
|
||||
max_vol,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user