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:
159
app/strategies/engine.py
Normal file
159
app/strategies/engine.py
Normal file
@@ -0,0 +1,159 @@
|
||||
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
|
||||
Reference in New Issue
Block a user