- Set ruff/black line length to 120 - Reformatted code with black - Fixed import ordering with ruff - Disabled lint for UI component files with long CSS strings - Updated pyproject.toml with proper tool configuration
140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from app.strategies.base import BaseStrategy, StrategyConfig
|
|
from app.strategies.protective_put import (
|
|
DEFAULT_SCENARIO_CHANGES,
|
|
ProtectivePutSpec,
|
|
ProtectivePutStrategy,
|
|
)
|
|
|
|
|
|
@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 * leg.hedge_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
|