Initial commit: Vault Dashboard for options hedging
- 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
This commit is contained in:
129
app/strategies/laddered_put.py
Normal file
129
app/strategies/laddered_put.py
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
Reference in New Issue
Block a user