from __future__ import annotations from dataclasses import dataclass, field from typing import Literal from .option import OptionContract StrategyType = Literal["single_put", "laddered_put", "collar"] @dataclass(frozen=True) class ScenarioResult: """Scenario output for a hedging strategy.""" underlying_price: float gross_option_payoff: float hedge_cost: float net_option_benefit: float @dataclass(frozen=True) class HedgingStrategy: """Collection of option positions representing a hedge. Notes: Premiums on long positions are positive cash outflows. Premiums on short positions are handled through ``short_contracts`` and reduce the total hedge cost. """ strategy_type: StrategyType long_contracts: tuple[OptionContract, ...] = field(default_factory=tuple) short_contracts: tuple[OptionContract, ...] = field(default_factory=tuple) description: str = "" def __post_init__(self) -> None: if self.strategy_type not in {"single_put", "laddered_put", "collar"}: raise ValueError("unsupported strategy_type") if not self.long_contracts and not self.short_contracts: raise ValueError("at least one option contract is required") if self.strategy_type == "single_put": if len(self.long_contracts) != 1 or self.long_contracts[0].option_type != "put": raise ValueError("single_put requires exactly one long put contract") if self.short_contracts: raise ValueError("single_put cannot include short contracts") if self.strategy_type == "laddered_put": if len(self.long_contracts) < 2: raise ValueError("laddered_put requires at least two long put contracts") if any(contract.option_type != "put" for contract in self.long_contracts): raise ValueError("laddered_put supports only long put contracts") if self.short_contracts: raise ValueError("laddered_put cannot include short contracts") if self.strategy_type == "collar": if not self.long_contracts or not self.short_contracts: raise ValueError("collar requires both long and short contracts") if any(contract.option_type != "put" for contract in self.long_contracts): raise ValueError("collar long leg must be put options") if any(contract.option_type != "call" for contract in self.short_contracts): raise ValueError("collar short leg must be call options") @property def hedge_cost(self) -> float: """Net upfront hedge cost.""" long_cost = sum(contract.total_premium for contract in self.long_contracts) short_credit = sum(contract.total_premium for contract in self.short_contracts) return long_cost - short_credit def gross_payoff(self, underlying_price: float) -> float: """Gross expiry payoff from all option legs.""" if underlying_price <= 0: raise ValueError("underlying_price must be positive") long_payoff = sum(contract.payoff(underlying_price) for contract in self.long_contracts) short_payoff = sum(contract.payoff(underlying_price) for contract in self.short_contracts) return long_payoff - short_payoff def net_benefit(self, underlying_price: float) -> float: """Net value added by the hedge after premium cost.""" return self.gross_payoff(underlying_price) - self.hedge_cost def scenario_analysis(self, underlying_prices: list[float] | tuple[float, ...]) -> list[ScenarioResult]: """Evaluate the hedge across alternative underlying-price scenarios.""" if not underlying_prices: raise ValueError("underlying_prices must not be empty") results: list[ScenarioResult] = [] for price in underlying_prices: if price <= 0: raise ValueError("scenario prices must be positive") gross_payoff = self.gross_payoff(price) results.append( ScenarioResult( underlying_price=price, gross_option_payoff=gross_payoff, hedge_cost=self.hedge_cost, net_option_benefit=gross_payoff - self.hedge_cost, ) ) return results