Files
vault-dash/app/strategies/laddered_put.py
Bu5hm4nn 874b4a5a02 Fix linting issues: line length, import sorting, unused variables
- 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
2026-03-22 10:30:12 +01:00

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