from __future__ import annotations from dataclasses import dataclass from app.strategies.base import BaseStrategy, StrategyConfig # Re-export for test access from app.strategies.protective_put import ( DEFAULT_SCENARIO_CHANGES, ProtectivePutSpec, ProtectivePutStrategy, gld_ounces_per_share, # noqa: F401 ) @dataclass(frozen=True) class LadderSpec: label: str weights: tuple[float, ...] strike_pcts: tuple[float, ...] months: int = 12 class LadderedPutStrategy(BaseStrategy): """Multi-strike protective put ladder with blended premium and protection analysis.""" def __init__(self, config: StrategyConfig, spec: LadderSpec) -> None: super().__init__(config) if len(spec.weights) != len(spec.strike_pcts): raise ValueError("weights and strike_pcts must have the same length") if abs(sum(spec.weights) - 1.0) > 1e-9: raise ValueError("weights must sum to 1.0") self.spec = spec @property def name(self) -> str: return f"laddered_put_{self.spec.label.lower()}" def _legs(self) -> list[tuple[float, ProtectivePutStrategy]]: legs: list[tuple[float, ProtectivePutStrategy]] = [] for index, (weight, strike_pct) in enumerate( zip(self.spec.weights, self.spec.strike_pcts, strict=True), start=1 ): leg = ProtectivePutStrategy( self.config, ProtectivePutSpec( label=f"{self.spec.label}_leg_{index}", strike_pct=strike_pct, months=self.spec.months, ), ) legs.append((weight, leg)) return legs def calculate_cost(self) -> dict: blended_cost = 0.0 blended_premium = 0.0 legs_summary: list[dict] = [] for weight, leg in self._legs(): contract = leg.build_contract() weighted_cost = contract.total_premium * weight blended_cost += weighted_cost blended_premium += contract.premium * weight legs_summary.append( { "weight": weight, "strike": round(contract.strike, 2), "premium_per_share": round(contract.premium, 4), "weighted_cost": round(weighted_cost, 2), } ) annualized_cost = blended_cost / (self.spec.months / 12.0) return { "strategy": self.name, "label": self.spec.label, "legs": legs_summary, "blended_premium_per_share": round(blended_premium, 4), "blended_cost": round(blended_cost, 2), "cost_pct_of_portfolio": round(blended_cost / self.config.portfolio.gold_value, 6), "annualized_cost": round(annualized_cost, 2), "annualized_cost_pct": round(annualized_cost / self.config.portfolio.gold_value, 6), } def calculate_protection(self) -> dict: threshold_price = self.config.portfolio.margin_call_price() total_payoff = 0.0 floor_value = 0.0 leg_protection: list[dict] = [] for weight, leg in self._legs(): contract = leg.build_contract() weighted_payoff = contract.payoff(threshold_price) * weight total_payoff += weighted_payoff floor_value += contract.strike * contract.notional_units * weight leg_protection.append( { "weight": weight, "strike": round(contract.strike, 2), "weighted_payoff_at_threshold": round(weighted_payoff, 2), } ) hedged_value_at_threshold = self.config.portfolio.gold_value_at_price(threshold_price) + total_payoff protected_ltv = self.config.portfolio.loan_amount / hedged_value_at_threshold return { "strategy": self.name, "threshold_price": round(threshold_price, 2), "portfolio_floor_value": round(floor_value, 2), "payoff_at_threshold": round(total_payoff, 2), "unhedged_ltv_at_threshold": round(self.config.portfolio.ltv_at_price(threshold_price), 6), "hedged_ltv_at_threshold": round(protected_ltv, 6), "maintains_margin_call_buffer": protected_ltv < self.config.portfolio.margin_call_ltv, "legs": leg_protection, } def get_scenarios(self) -> list[dict]: cost = self.calculate_cost()["blended_cost"] 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 = 0.0 for weight, leg in self._legs(): option_payoff += leg.build_contract().payoff(price) * weight 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(cost, 2), "net_portfolio_value": round(gold_value + option_payoff - 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