Files
vault-dash/app/models/option.py
Bu5hm4nn 00a68bc767 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
2026-03-21 19:21:40 +01:00

110 lines
3.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Literal
OptionType = Literal["call", "put"]
OptionMoneyness = Literal["ITM", "ATM", "OTM"]
@dataclass(frozen=True)
class Greeks:
"""Option Greeks container."""
delta: float = 0.0
gamma: float = 0.0
theta: float = 0.0
vega: float = 0.0
rho: float = 0.0
@dataclass(frozen=True)
class OptionContract:
"""Vanilla option contract used in hedging strategies.
Attributes:
option_type: Contract type, either ``"put"`` or ``"call"``.
strike: Strike price.
expiry: Expiration date.
premium: Premium paid or received per unit of underlying.
quantity: Number of contracts or units.
contract_size: Underlying units per contract.
underlying_price: Current underlying spot price for classification.
greeks: Stored option Greeks.
"""
option_type: OptionType
strike: float
expiry: date
premium: float
quantity: float = 1.0
contract_size: float = 1.0
underlying_price: float | None = None
greeks: Greeks = Greeks()
def __post_init__(self) -> None:
option = self.option_type.lower()
if option not in {"call", "put"}:
raise ValueError("option_type must be either 'call' or 'put'")
object.__setattr__(self, "option_type", option)
if self.strike <= 0:
raise ValueError("strike must be positive")
if self.premium < 0:
raise ValueError("premium must be non-negative")
if self.quantity <= 0:
raise ValueError("quantity must be positive")
if self.contract_size <= 0:
raise ValueError("contract_size must be positive")
if self.expiry <= date.today():
raise ValueError("expiry must be in the future")
if self.underlying_price is not None and self.underlying_price <= 0:
raise ValueError("underlying_price must be positive when provided")
@property
def notional_units(self) -> float:
"""Underlying units covered by the contract position."""
return self.quantity * self.contract_size
@property
def total_premium(self) -> float:
"""Total premium paid or received for the position."""
return self.premium * self.notional_units
def classify_moneyness(self, underlying_price: float | None = None, *, atm_tolerance: float = 0.01) -> OptionMoneyness:
"""Classify the contract as ITM, ATM, or OTM.
Args:
underlying_price: Spot price used for classification. Falls back to
``self.underlying_price``.
atm_tolerance: Relative tolerance around strike treated as at-the-money.
"""
spot = self.underlying_price if underlying_price is None else underlying_price
if spot is None:
raise ValueError("underlying_price must be provided for strategy classification")
if spot <= 0:
raise ValueError("underlying_price must be positive")
if atm_tolerance < 0:
raise ValueError("atm_tolerance must be non-negative")
relative_gap = abs(spot - self.strike) / self.strike
if relative_gap <= atm_tolerance:
return "ATM"
if self.option_type == "put":
return "ITM" if self.strike > spot else "OTM"
return "ITM" if self.strike < spot else "OTM"
def intrinsic_value(self, underlying_price: float) -> float:
"""Intrinsic value per underlying unit at a given spot price."""
if underlying_price <= 0:
raise ValueError("underlying_price must be positive")
if self.option_type == "put":
return max(self.strike - underlying_price, 0.0)
return max(underlying_price - self.strike, 0.0)
def payoff(self, underlying_price: float) -> float:
"""Gross payoff of the option position at expiry."""
return self.intrinsic_value(underlying_price) * self.notional_units