from __future__ import annotations import math from dataclasses import dataclass from datetime import date, timedelta from app.core.pricing.black_scholes import ( BlackScholesInputs, black_scholes_price_and_greeks, ) from app.domain.instruments import gld_ounces_per_share from app.models.option import Greeks, OptionContract from app.models.strategy import HedgingStrategy from app.strategies.base import BaseStrategy, StrategyConfig DEFAULT_SCENARIO_CHANGES = ( -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, ) @dataclass(frozen=True) class ProtectivePutSpec: label: str strike_pct: float months: int = 12 class ProtectivePutStrategy(BaseStrategy): """Single-leg protective put strategy using ATM or configurable OTM strikes.""" def __init__(self, config: StrategyConfig, spec: ProtectivePutSpec) -> None: super().__init__(config) self.spec = spec @property def name(self) -> str: return f"protective_put_{self.spec.label.lower()}" @property def hedge_units(self) -> float: """Gold ounces to hedge (canonical portfolio weight).""" return self.config.portfolio.gold_ounces @property def strike(self) -> float: return self.config.spot_price * self.spec.strike_pct @property def term_years(self) -> float: return self.spec.months / 12.0 @property def gld_backing(self) -> float: """GLD ounces per share for contract count calculation.""" return float(gld_ounces_per_share()) @property def contract_count(self) -> int: """Number of GLD option contracts needed. GLD options cover 100 shares each. Each share represents ~0.0919 oz (expense-ratio adjusted). Formula: ceil(gold_ounces / (100 * backing)). """ return math.ceil(self.hedge_units / (100 * self.gld_backing)) def build_contract(self) -> OptionContract: pricing = black_scholes_price_and_greeks( BlackScholesInputs( spot=self.config.spot_price, strike=self.strike, time_to_expiry=self.term_years, risk_free_rate=self.config.risk_free_rate, volatility=self.config.volatility, option_type="put", ) ) return OptionContract( option_type="put", strike=self.strike, expiry=date.today() + timedelta(days=max(1, round(365 * self.term_years))), premium=pricing.price, quantity=float(self.contract_count), contract_size=100 * self.gld_backing, underlying_price=self.config.spot_price, greeks=Greeks( delta=pricing.delta, gamma=pricing.gamma, theta=pricing.theta, vega=pricing.vega, rho=pricing.rho, ), ) def build_hedging_strategy(self) -> HedgingStrategy: return HedgingStrategy( strategy_type="single_put", long_contracts=(self.build_contract(),), description=f"{self.spec.label} protective put", ) def calculate_cost(self) -> dict: contract = self.build_contract() total_cost = contract.total_premium return { "strategy": self.name, "label": self.spec.label, "strike": round(contract.strike, 2), "strike_pct": self.spec.strike_pct, "premium_per_share": round(contract.premium, 4), "total_cost": round(total_cost, 2), "cost_pct_of_portfolio": round(total_cost / self.config.portfolio.gold_value, 6), "term_months": self.spec.months, "annualized_cost": round(total_cost / self.term_years, 2), "annualized_cost_pct": round((total_cost / self.term_years) / self.config.portfolio.gold_value, 6), } def calculate_protection(self) -> dict: contract = self.build_contract() threshold_price = self.config.portfolio.margin_call_price() payoff_at_threshold = contract.payoff(threshold_price) hedged_value_at_threshold = self.config.portfolio.gold_value_at_price(threshold_price) + payoff_at_threshold protected_ltv = self.config.portfolio.loan_amount / hedged_value_at_threshold floor_value = contract.strike * contract.notional_units return { "strategy": self.name, "threshold_price": round(threshold_price, 2), "strike": round(contract.strike, 2), "portfolio_floor_value": round(floor_value, 2), "unhedged_ltv_at_threshold": round(self.config.portfolio.ltv_at_price(threshold_price), 6), "hedged_ltv_at_threshold": round(protected_ltv, 6), "payoff_at_threshold": round(payoff_at_threshold, 2), "maintains_margin_call_buffer": protected_ltv < self.config.portfolio.margin_call_ltv, } def get_scenarios(self) -> list[dict]: strategy = self.build_hedging_strategy() scenarios: list[dict] = [] for change in DEFAULT_SCENARIO_CHANGES: price = self.config.spot_price * (1 + change) if price <= 0: continue gold_value = self.config.portfolio.gold_value_at_price(price) option_payoff = strategy.gross_payoff(price) hedged_collateral = gold_value + option_payoff scenarios.append( { "price_change_pct": round(change, 2), "gld_price": round(price, 2), "gold_value": round(gold_value, 2), "option_payoff": round(option_payoff, 2), "hedge_cost": round(strategy.hedge_cost, 2), "net_portfolio_value": round(gold_value + option_payoff - strategy.hedge_cost, 2), "unhedged_ltv": round(self.config.portfolio.loan_amount / gold_value, 6), "hedged_ltv": round(self.config.portfolio.loan_amount / hedged_collateral, 6), "margin_call_without_hedge": (self.config.portfolio.loan_amount / gold_value) >= self.config.portfolio.margin_call_ltv, "margin_call_with_hedge": (self.config.portfolio.loan_amount / hedged_collateral) >= self.config.portfolio.margin_call_ltv, } ) return scenarios