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