- 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
102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
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
|