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:
58
app/core/pricing/__init__.py
Normal file
58
app/core/pricing/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Core options pricing utilities for the Vault dashboard.
|
||||
|
||||
This package provides pricing helpers for:
|
||||
- European Black-Scholes valuation
|
||||
- American option pricing via binomial trees when QuantLib is installed
|
||||
- Implied volatility inversion when QuantLib is installed
|
||||
|
||||
Research defaults are based on the Vault hedging paper:
|
||||
- Gold price: 4,600 USD/oz
|
||||
- GLD price: 460 USD/share
|
||||
- Risk-free rate: 4.5%
|
||||
- Volatility: 16% annualized
|
||||
- GLD dividend yield: 0%
|
||||
"""
|
||||
|
||||
from .black_scholes import (
|
||||
DEFAULT_GLD_PRICE,
|
||||
DEFAULT_GOLD_PRICE_PER_OUNCE,
|
||||
DEFAULT_RISK_FREE_RATE,
|
||||
DEFAULT_VOLATILITY,
|
||||
BlackScholesInputs,
|
||||
HedgingCost,
|
||||
PricingResult,
|
||||
annual_hedging_cost,
|
||||
black_scholes_price_and_greeks,
|
||||
margin_call_threshold_price,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_GLD_PRICE",
|
||||
"DEFAULT_GOLD_PRICE_PER_OUNCE",
|
||||
"DEFAULT_RISK_FREE_RATE",
|
||||
"DEFAULT_VOLATILITY",
|
||||
"BlackScholesInputs",
|
||||
"HedgingCost",
|
||||
"PricingResult",
|
||||
"annual_hedging_cost",
|
||||
"black_scholes_price_and_greeks",
|
||||
"margin_call_threshold_price",
|
||||
]
|
||||
|
||||
try: # pragma: no cover - optional QuantLib modules
|
||||
from .american_pricing import AmericanOptionInputs, AmericanPricingResult, american_option_price_and_greeks
|
||||
from .volatility import implied_volatility
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
AmericanOptionInputs = None
|
||||
AmericanPricingResult = None
|
||||
american_option_price_and_greeks = None
|
||||
implied_volatility = None
|
||||
else:
|
||||
__all__.extend(
|
||||
[
|
||||
"AmericanOptionInputs",
|
||||
"AmericanPricingResult",
|
||||
"american_option_price_and_greeks",
|
||||
"implied_volatility",
|
||||
]
|
||||
)
|
||||
194
app/core/pricing/american_pricing.py
Normal file
194
app/core/pricing/american_pricing.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
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: float = 0.16
|
||||
DEFAULT_DIVIDEND_YIELD: float = 0.0
|
||||
DEFAULT_GLD_PRICE: float = 460.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmericanOptionInputs:
|
||||
"""Inputs for American option pricing via a binomial tree.
|
||||
|
||||
This module is intended primarily for GLD protective puts, where early
|
||||
exercise can matter in stressed scenarios.
|
||||
|
||||
Example:
|
||||
>>> params = AmericanOptionInputs(
|
||||
... spot=460.0,
|
||||
... strike=420.0,
|
||||
... time_to_expiry=0.5,
|
||||
... option_type="put",
|
||||
... )
|
||||
>>> params.steps
|
||||
500
|
||||
"""
|
||||
|
||||
spot: float = DEFAULT_GLD_PRICE
|
||||
strike: float = DEFAULT_GLD_PRICE
|
||||
time_to_expiry: float = 0.5
|
||||
risk_free_rate: float = DEFAULT_RISK_FREE_RATE
|
||||
volatility: float = DEFAULT_VOLATILITY
|
||||
option_type: OptionType = "put"
|
||||
dividend_yield: float = DEFAULT_DIVIDEND_YIELD
|
||||
steps: int = 500
|
||||
valuation_date: date | None = None
|
||||
tree: str = "crr"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmericanPricingResult:
|
||||
"""American option price and finite-difference Greeks."""
|
||||
|
||||
price: float
|
||||
delta: float
|
||||
gamma: float
|
||||
theta: float
|
||||
vega: float
|
||||
rho: float
|
||||
|
||||
|
||||
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 _build_dates(time_to_expiry: float, valuation_date: date | None) -> tuple[ql.Date, ql.Date]:
|
||||
if time_to_expiry <= 0.0:
|
||||
raise ValueError("time_to_expiry must be positive")
|
||||
|
||||
valuation = valuation_date or date.today()
|
||||
maturity = valuation + timedelta(days=max(1, round(time_to_expiry * 365)))
|
||||
return (
|
||||
ql.Date(valuation.day, valuation.month, valuation.year),
|
||||
ql.Date(maturity.day, maturity.month, maturity.year),
|
||||
)
|
||||
|
||||
|
||||
def _american_price(
|
||||
params: AmericanOptionInputs,
|
||||
*,
|
||||
spot: float | None = None,
|
||||
risk_free_rate: float | None = None,
|
||||
volatility: float | None = None,
|
||||
time_to_expiry: float | None = None,
|
||||
) -> float:
|
||||
option_type = _validate_option_type(params.option_type)
|
||||
used_spot = params.spot if spot is None else spot
|
||||
used_rate = params.risk_free_rate if risk_free_rate is None else risk_free_rate
|
||||
used_vol = params.volatility if volatility is None else volatility
|
||||
used_time = params.time_to_expiry if time_to_expiry is None else time_to_expiry
|
||||
|
||||
if used_spot <= 0 or used_vol <= 0 or used_time <= 0:
|
||||
raise ValueError("spot, volatility, and time_to_expiry must be positive")
|
||||
if params.steps < 10:
|
||||
raise ValueError("steps must be at least 10 for binomial pricing")
|
||||
|
||||
valuation_ql, maturity_ql = _build_dates(used_time, params.valuation_date)
|
||||
ql.Settings.instance().evaluationDate = valuation_ql
|
||||
|
||||
day_count = ql.Actual365Fixed()
|
||||
calendar = ql.NullCalendar()
|
||||
|
||||
spot_handle = ql.QuoteHandle(ql.SimpleQuote(used_spot))
|
||||
dividend_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, params.dividend_yield, day_count)
|
||||
)
|
||||
risk_free_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, used_rate, day_count)
|
||||
)
|
||||
volatility_curve = ql.BlackVolTermStructureHandle(
|
||||
ql.BlackConstantVol(valuation_ql, calendar, used_vol, day_count)
|
||||
)
|
||||
|
||||
process = ql.BlackScholesMertonProcess(
|
||||
spot_handle,
|
||||
dividend_curve,
|
||||
risk_free_curve,
|
||||
volatility_curve,
|
||||
)
|
||||
|
||||
payoff = ql.PlainVanillaPayoff(_to_quantlib_option_type(option_type), params.strike)
|
||||
exercise = ql.AmericanExercise(valuation_ql, maturity_ql)
|
||||
option = ql.VanillaOption(payoff, exercise)
|
||||
option.setPricingEngine(ql.BinomialVanillaEngine(process, params.tree, params.steps))
|
||||
return float(option.NPV())
|
||||
|
||||
|
||||
def american_option_price_and_greeks(params: AmericanOptionInputs) -> AmericanPricingResult:
|
||||
"""Price an American option and estimate Greeks with finite differences.
|
||||
|
||||
Notes:
|
||||
- The price uses a QuantLib binomial tree engine.
|
||||
- Greeks are finite-difference approximations because closed-form
|
||||
American Greeks are not available in general.
|
||||
- Theta is annualized and approximated by rolling one calendar day forward.
|
||||
|
||||
Args:
|
||||
params: American option inputs.
|
||||
|
||||
Returns:
|
||||
A price and finite-difference Greeks.
|
||||
|
||||
Example:
|
||||
>>> params = AmericanOptionInputs(
|
||||
... spot=460.0,
|
||||
... strike=400.0,
|
||||
... time_to_expiry=0.5,
|
||||
... risk_free_rate=0.045,
|
||||
... volatility=0.16,
|
||||
... option_type="put",
|
||||
... )
|
||||
>>> result = american_option_price_and_greeks(params)
|
||||
>>> result.price > 0
|
||||
True
|
||||
"""
|
||||
|
||||
base_price = _american_price(params)
|
||||
|
||||
spot_bump = max(0.01, params.spot * 0.01)
|
||||
vol_bump = 0.01
|
||||
rate_bump = 0.0001
|
||||
dt = 1.0 / 365.0
|
||||
|
||||
price_up = _american_price(params, spot=params.spot + spot_bump)
|
||||
price_down = _american_price(params, spot=max(1e-8, params.spot - spot_bump))
|
||||
delta = (price_up - price_down) / (2.0 * spot_bump)
|
||||
gamma = (price_up - 2.0 * base_price + price_down) / (spot_bump**2)
|
||||
|
||||
vega_up = _american_price(params, volatility=params.volatility + vol_bump)
|
||||
vega_down = _american_price(params, volatility=max(1e-6, params.volatility - vol_bump))
|
||||
vega = (vega_up - vega_down) / (2.0 * vol_bump)
|
||||
|
||||
rho_up = _american_price(params, risk_free_rate=params.risk_free_rate + rate_bump)
|
||||
rho_down = _american_price(params, risk_free_rate=params.risk_free_rate - rate_bump)
|
||||
rho = (rho_up - rho_down) / (2.0 * rate_bump)
|
||||
|
||||
if params.time_to_expiry <= dt:
|
||||
theta = 0.0
|
||||
else:
|
||||
shorter_price = _american_price(params, time_to_expiry=params.time_to_expiry - dt)
|
||||
theta = (shorter_price - base_price) / dt
|
||||
|
||||
return AmericanPricingResult(
|
||||
price=base_price,
|
||||
delta=delta,
|
||||
gamma=gamma,
|
||||
theta=theta,
|
||||
vega=vega,
|
||||
rho=rho,
|
||||
)
|
||||
210
app/core/pricing/black_scholes.py
Normal file
210
app/core/pricing/black_scholes.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
import math
|
||||
from typing import Any, Literal
|
||||
|
||||
try: # pragma: no cover - optional dependency
|
||||
import QuantLib as ql
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
ql = None
|
||||
|
||||
OptionType = Literal["call", "put"]
|
||||
|
||||
DEFAULT_GOLD_PRICE_PER_OUNCE: float = 4600.0
|
||||
DEFAULT_GLD_PRICE: float = 460.0
|
||||
DEFAULT_RISK_FREE_RATE: float = 0.045
|
||||
DEFAULT_VOLATILITY: float = 0.16
|
||||
DEFAULT_DIVIDEND_YIELD: float = 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BlackScholesInputs:
|
||||
"""Inputs for European Black-Scholes pricing."""
|
||||
|
||||
spot: float = DEFAULT_GLD_PRICE
|
||||
strike: float = DEFAULT_GLD_PRICE
|
||||
time_to_expiry: float = 0.25
|
||||
risk_free_rate: float = DEFAULT_RISK_FREE_RATE
|
||||
volatility: float = DEFAULT_VOLATILITY
|
||||
option_type: OptionType = "put"
|
||||
dividend_yield: float = DEFAULT_DIVIDEND_YIELD
|
||||
valuation_date: date | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PricingResult:
|
||||
"""European option price and Greeks."""
|
||||
|
||||
price: float
|
||||
delta: float
|
||||
gamma: float
|
||||
theta: float
|
||||
vega: float
|
||||
rho: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HedgingCost:
|
||||
"""Annualized hedging cost summary."""
|
||||
|
||||
premium_paid: float
|
||||
annual_cost_dollars: float
|
||||
annual_cost_pct: float
|
||||
|
||||
|
||||
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) -> Any:
|
||||
if ql is None:
|
||||
raise RuntimeError("QuantLib is not installed")
|
||||
return ql.Option.Call if option_type == "call" else ql.Option.Put
|
||||
|
||||
|
||||
def _build_dates(time_to_expiry: float, valuation_date: date | None) -> tuple[Any, Any]:
|
||||
if time_to_expiry <= 0.0:
|
||||
raise ValueError("time_to_expiry must be positive")
|
||||
if ql is None:
|
||||
return (None, None)
|
||||
|
||||
valuation = valuation_date or date.today()
|
||||
maturity = valuation + timedelta(days=max(1, round(time_to_expiry * 365)))
|
||||
return (
|
||||
ql.Date(valuation.day, valuation.month, valuation.year),
|
||||
ql.Date(maturity.day, maturity.month, maturity.year),
|
||||
)
|
||||
|
||||
|
||||
def _norm_pdf(value: float) -> float:
|
||||
return math.exp(-(value**2) / 2.0) / math.sqrt(2.0 * math.pi)
|
||||
|
||||
|
||||
def _norm_cdf(value: float) -> float:
|
||||
return 0.5 * (1.0 + math.erf(value / math.sqrt(2.0)))
|
||||
|
||||
|
||||
def _analytic_black_scholes(params: BlackScholesInputs, option_type: OptionType) -> PricingResult:
|
||||
if params.spot <= 0 or params.strike <= 0 or params.time_to_expiry <= 0 or params.volatility <= 0:
|
||||
raise ValueError("spot, strike, time_to_expiry, and volatility must be positive")
|
||||
|
||||
t = params.time_to_expiry
|
||||
sigma = params.volatility
|
||||
sqrt_t = math.sqrt(t)
|
||||
d1 = (
|
||||
math.log(params.spot / params.strike)
|
||||
+ (params.risk_free_rate - params.dividend_yield + 0.5 * sigma**2) * t
|
||||
) / (sigma * sqrt_t)
|
||||
d2 = d1 - sigma * sqrt_t
|
||||
disc_r = math.exp(-params.risk_free_rate * t)
|
||||
disc_q = math.exp(-params.dividend_yield * t)
|
||||
pdf_d1 = _norm_pdf(d1)
|
||||
|
||||
if option_type == "call":
|
||||
price = params.spot * disc_q * _norm_cdf(d1) - params.strike * disc_r * _norm_cdf(d2)
|
||||
delta = disc_q * _norm_cdf(d1)
|
||||
theta = (
|
||||
-(params.spot * disc_q * pdf_d1 * sigma) / (2 * sqrt_t)
|
||||
- params.risk_free_rate * params.strike * disc_r * _norm_cdf(d2)
|
||||
+ params.dividend_yield * params.spot * disc_q * _norm_cdf(d1)
|
||||
)
|
||||
rho = params.strike * t * disc_r * _norm_cdf(d2)
|
||||
else:
|
||||
price = params.strike * disc_r * _norm_cdf(-d2) - params.spot * disc_q * _norm_cdf(-d1)
|
||||
delta = disc_q * (_norm_cdf(d1) - 1.0)
|
||||
theta = (
|
||||
-(params.spot * disc_q * pdf_d1 * sigma) / (2 * sqrt_t)
|
||||
+ params.risk_free_rate * params.strike * disc_r * _norm_cdf(-d2)
|
||||
- params.dividend_yield * params.spot * disc_q * _norm_cdf(-d1)
|
||||
)
|
||||
rho = -params.strike * t * disc_r * _norm_cdf(-d2)
|
||||
|
||||
gamma = (disc_q * pdf_d1) / (params.spot * sigma * sqrt_t)
|
||||
vega = params.spot * disc_q * pdf_d1 * sqrt_t
|
||||
return PricingResult(price=float(price), delta=float(delta), gamma=float(gamma), theta=float(theta), vega=float(vega), rho=float(rho))
|
||||
|
||||
|
||||
def black_scholes_price_and_greeks(params: BlackScholesInputs) -> PricingResult:
|
||||
"""Price a European option with QuantLib when available, otherwise analytic BSM."""
|
||||
|
||||
option_type = _validate_option_type(params.option_type)
|
||||
if ql is None:
|
||||
return _analytic_black_scholes(params, option_type)
|
||||
|
||||
valuation_ql, maturity_ql = _build_dates(params.time_to_expiry, params.valuation_date)
|
||||
ql.Settings.instance().evaluationDate = valuation_ql
|
||||
|
||||
day_count = ql.Actual365Fixed()
|
||||
calendar = ql.NullCalendar()
|
||||
|
||||
spot_handle = ql.QuoteHandle(ql.SimpleQuote(params.spot))
|
||||
dividend_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, params.dividend_yield, day_count)
|
||||
)
|
||||
risk_free_curve = ql.YieldTermStructureHandle(
|
||||
ql.FlatForward(valuation_ql, params.risk_free_rate, day_count)
|
||||
)
|
||||
volatility = ql.BlackVolTermStructureHandle(
|
||||
ql.BlackConstantVol(valuation_ql, calendar, params.volatility, day_count)
|
||||
)
|
||||
|
||||
process = ql.BlackScholesMertonProcess(spot_handle, dividend_curve, risk_free_curve, volatility)
|
||||
payoff = ql.PlainVanillaPayoff(_to_quantlib_option_type(option_type), params.strike)
|
||||
exercise = ql.EuropeanExercise(maturity_ql)
|
||||
option = ql.VanillaOption(payoff, exercise)
|
||||
option.setPricingEngine(ql.AnalyticEuropeanEngine(process))
|
||||
|
||||
return PricingResult(
|
||||
price=float(option.NPV()),
|
||||
delta=float(option.delta()),
|
||||
gamma=float(option.gamma()),
|
||||
theta=float(option.theta()),
|
||||
vega=float(option.vega()),
|
||||
rho=float(option.rho()),
|
||||
)
|
||||
|
||||
|
||||
def margin_call_threshold_price(
|
||||
portfolio_value: float,
|
||||
loan_amount: float,
|
||||
current_price: float = DEFAULT_GLD_PRICE,
|
||||
margin_call_ltv: float = 0.75,
|
||||
) -> float:
|
||||
"""Calculate the underlying price where a margin call is triggered."""
|
||||
|
||||
if portfolio_value <= 0 or loan_amount <= 0 or current_price <= 0:
|
||||
raise ValueError("portfolio_value, loan_amount, and current_price must be positive")
|
||||
if not 0 < margin_call_ltv < 1:
|
||||
raise ValueError("margin_call_ltv must be between 0 and 1")
|
||||
|
||||
units = portfolio_value / current_price
|
||||
return loan_amount / (margin_call_ltv * units)
|
||||
|
||||
|
||||
def annual_hedging_cost(
|
||||
premium_per_share: float,
|
||||
shares_hedged: float,
|
||||
portfolio_value: float,
|
||||
hedge_term_years: float,
|
||||
) -> HedgingCost:
|
||||
"""Annualize the premium cost of a hedging program."""
|
||||
|
||||
if premium_per_share < 0 or shares_hedged <= 0 or portfolio_value <= 0 or hedge_term_years <= 0:
|
||||
raise ValueError(
|
||||
"premium_per_share must be non-negative and shares_hedged, portfolio_value, "
|
||||
"and hedge_term_years must be positive"
|
||||
)
|
||||
|
||||
premium_paid = premium_per_share * shares_hedged
|
||||
annual_cost_dollars = premium_paid / hedge_term_years
|
||||
annual_cost_pct = annual_cost_dollars / portfolio_value
|
||||
return HedgingCost(
|
||||
premium_paid=premium_paid,
|
||||
annual_cost_dollars=annual_cost_dollars,
|
||||
annual_cost_pct=annual_cost_pct,
|
||||
)
|
||||
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