- 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
160 lines
6.7 KiB
Python
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
|