Files
vault-dash/app/strategies/engine.py
Bu5hm4nn 00a68bc767 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
2026-03-21 19:21:40 +01:00

160 lines
6.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from app.core.pricing.black_scholes import DEFAULT_GLD_PRICE, DEFAULT_RISK_FREE_RATE, DEFAULT_VOLATILITY
from app.models.portfolio import LombardPortfolio
from app.strategies.base import BaseStrategy, StrategyConfig
from app.strategies.laddered_put import LadderSpec, LadderedPutStrategy
from app.strategies.lease import LeaseStrategy
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
RiskProfile = Literal["conservative", "balanced", "cost_sensitive"]
RESEARCH_PORTFOLIO_VALUE = 1_000_000.0
RESEARCH_LOAN_AMOUNT = 600_000.0
RESEARCH_MARGIN_CALL_THRESHOLD = 0.75
RESEARCH_GLD_SPOT = 460.0
RESEARCH_VOLATILITY = 0.16
RESEARCH_RISK_FREE_RATE = 0.045
@dataclass(frozen=True)
class StrategySelectionEngine:
"""Compare paper strategies and recommend the best fit by risk profile."""
portfolio_value: float = RESEARCH_PORTFOLIO_VALUE
loan_amount: float = RESEARCH_LOAN_AMOUNT
margin_call_threshold: float = RESEARCH_MARGIN_CALL_THRESHOLD
spot_price: float = RESEARCH_GLD_SPOT
volatility: float = RESEARCH_VOLATILITY
risk_free_rate: float = RESEARCH_RISK_FREE_RATE
def _config(self) -> StrategyConfig:
portfolio = LombardPortfolio(
gold_ounces=self.portfolio_value / self.spot_price,
gold_price_per_ounce=self.spot_price,
loan_amount=self.loan_amount,
initial_ltv=self.loan_amount / self.portfolio_value,
margin_call_ltv=self.margin_call_threshold,
)
return StrategyConfig(
portfolio=portfolio,
spot_price=self.spot_price,
volatility=self.volatility,
risk_free_rate=self.risk_free_rate,
)
def _strategies(self) -> list[BaseStrategy]:
config = self._config()
return [
ProtectivePutStrategy(config, ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12)),
ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_95", strike_pct=0.95, months=12)),
ProtectivePutStrategy(config, ProtectivePutSpec(label="OTM_90", strike_pct=0.90, months=12)),
LadderedPutStrategy(
config,
LadderSpec(label="50_50_ATM_OTM95", weights=(0.5, 0.5), strike_pcts=(1.0, 0.95), months=12),
),
LadderedPutStrategy(
config,
LadderSpec(label="33_33_33_ATM_OTM95_OTM90", weights=(1 / 3, 1 / 3, 1 / 3), strike_pcts=(1.0, 0.95, 0.90), months=12),
),
LeaseStrategy(config),
]
def compare_all_strategies(self) -> list[dict]:
comparisons: list[dict] = []
for strategy in self._strategies():
cost = strategy.calculate_cost()
protection = strategy.calculate_protection()
scenarios = strategy.get_scenarios()
annual_cost = cost.get("annualized_cost", cost.get("lowest_annual_cost", 0.0))
protection_ltv = protection.get("hedged_ltv_at_threshold")
if protection_ltv is None:
duration_rows = protection.get("durations", [])
protection_ltv = min((row["hedged_ltv_at_threshold"] for row in duration_rows), default=1.0)
comparisons.append(
{
"name": strategy.name,
"cost": cost,
"protection": protection,
"scenarios": scenarios,
"score_inputs": {
"annual_cost": annual_cost,
"hedged_ltv_at_threshold": protection_ltv,
},
}
)
return comparisons
def recommend(self, risk_profile: RiskProfile = "balanced") -> dict:
comparisons = self.compare_all_strategies()
def score(item: dict) -> tuple[float, float]:
annual_cost = item["score_inputs"]["annual_cost"]
hedged_ltv = item["score_inputs"]["hedged_ltv_at_threshold"]
if risk_profile == "conservative":
return (hedged_ltv, annual_cost)
if risk_profile == "cost_sensitive":
return (annual_cost, hedged_ltv)
return (hedged_ltv + (annual_cost / self.portfolio_value), annual_cost)
recommended = min(comparisons, key=score)
return {
"risk_profile": risk_profile,
"recommended_strategy": recommended["name"],
"rationale": {
"portfolio_value": self.portfolio_value,
"loan_amount": self.loan_amount,
"margin_call_threshold": self.margin_call_threshold,
"spot_price": self.spot_price,
"volatility": self.volatility,
"risk_free_rate": self.risk_free_rate,
},
"comparison_summary": [
{
"name": item["name"],
"annual_cost": round(item["score_inputs"]["annual_cost"], 2),
"hedged_ltv_at_threshold": round(item["score_inputs"]["hedged_ltv_at_threshold"], 6),
}
for item in comparisons
],
}
def sensitivity_analysis(self) -> dict:
results: dict[str, list[dict]] = {"volatility": [], "spot_price": []}
for volatility in (0.12, 0.16, 0.20):
engine = StrategySelectionEngine(
portfolio_value=self.portfolio_value,
loan_amount=self.loan_amount,
margin_call_threshold=self.margin_call_threshold,
spot_price=self.spot_price,
volatility=volatility,
risk_free_rate=self.risk_free_rate,
)
recommendation = engine.recommend("balanced")
results["volatility"].append(
{
"volatility": volatility,
"recommended_strategy": recommendation["recommended_strategy"],
}
)
for spot_price in (DEFAULT_GLD_PRICE * 0.9, DEFAULT_GLD_PRICE, DEFAULT_GLD_PRICE * 1.1):
engine = StrategySelectionEngine(
portfolio_value=self.portfolio_value,
loan_amount=self.loan_amount,
margin_call_threshold=self.margin_call_threshold,
spot_price=spot_price,
volatility=DEFAULT_VOLATILITY,
risk_free_rate=DEFAULT_RISK_FREE_RATE,
)
recommendation = engine.recommend("balanced")
results["spot_price"].append(
{
"spot_price": round(spot_price, 2),
"recommended_strategy": recommendation["recommended_strategy"],
}
)
return results