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:
17
app/strategies/__init__.py
Normal file
17
app/strategies/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .base import BaseStrategy, StrategyConfig
|
||||
from .engine import StrategySelectionEngine
|
||||
from .laddered_put import LadderSpec, LadderedPutStrategy
|
||||
from .lease import LeaseAnalysisSpec, LeaseStrategy
|
||||
from .protective_put import ProtectivePutSpec, ProtectivePutStrategy
|
||||
|
||||
__all__ = [
|
||||
"BaseStrategy",
|
||||
"StrategyConfig",
|
||||
"ProtectivePutSpec",
|
||||
"ProtectivePutStrategy",
|
||||
"LadderSpec",
|
||||
"LadderedPutStrategy",
|
||||
"LeaseAnalysisSpec",
|
||||
"LeaseStrategy",
|
||||
"StrategySelectionEngine",
|
||||
]
|
||||
40
app/strategies/base.py
Normal file
40
app/strategies/base.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.models.portfolio import LombardPortfolio
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrategyConfig:
|
||||
"""Common research inputs used by all strategy implementations."""
|
||||
|
||||
portfolio: LombardPortfolio
|
||||
spot_price: float
|
||||
volatility: float
|
||||
risk_free_rate: float
|
||||
|
||||
|
||||
class BaseStrategy(ABC):
|
||||
"""Abstract strategy interface for paper-based hedge analysis."""
|
||||
|
||||
def __init__(self, config: StrategyConfig) -> None:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str: # pragma: no cover - interface only
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def calculate_cost(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def calculate_protection(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_scenarios(self) -> list[dict]:
|
||||
raise NotImplementedError
|
||||
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
|
||||
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
|
||||
95
app/strategies/lease.py
Normal file
95
app/strategies/lease.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.strategies.base import BaseStrategy, StrategyConfig
|
||||
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LeaseAnalysisSpec:
|
||||
strike_pct: float = 1.0
|
||||
durations_months: tuple[int, ...] = (3, 6, 12, 18, 24)
|
||||
|
||||
|
||||
class LeaseStrategy(BaseStrategy):
|
||||
"""LEAPS duration analysis with roll timing and annualized cost comparison."""
|
||||
|
||||
def __init__(self, config: StrategyConfig, spec: LeaseAnalysisSpec | None = None) -> None:
|
||||
super().__init__(config)
|
||||
self.spec = spec or LeaseAnalysisSpec()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "lease_duration_analysis"
|
||||
|
||||
def _protective_put(self, months: int) -> ProtectivePutStrategy:
|
||||
return ProtectivePutStrategy(
|
||||
self.config,
|
||||
ProtectivePutSpec(label=f"LEAPS_{months}M", strike_pct=self.spec.strike_pct, months=months),
|
||||
)
|
||||
|
||||
def _duration_rows(self) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
for months in self.spec.durations_months:
|
||||
strategy = self._protective_put(months)
|
||||
cost = strategy.calculate_cost()
|
||||
rolls_per_year = 12 / months
|
||||
rows.append(
|
||||
{
|
||||
"months": months,
|
||||
"strike": cost["strike"],
|
||||
"premium_per_share": cost["premium_per_share"],
|
||||
"total_cost": cost["total_cost"],
|
||||
"annualized_cost": cost["annualized_cost"],
|
||||
"annualized_cost_pct": cost["annualized_cost_pct"],
|
||||
"rolls_per_year": round(rolls_per_year, 4),
|
||||
"recommended_roll_month": max(1, months - 1),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
def calculate_cost(self) -> dict:
|
||||
rows = self._duration_rows()
|
||||
optimal = min(rows, key=lambda item: item["annualized_cost"])
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"comparison": rows,
|
||||
"optimal_duration_months": optimal["months"],
|
||||
"lowest_annual_cost": optimal["annualized_cost"],
|
||||
"lowest_annual_cost_pct": optimal["annualized_cost_pct"],
|
||||
}
|
||||
|
||||
def calculate_protection(self) -> dict:
|
||||
threshold_price = self.config.portfolio.margin_call_price()
|
||||
rows: list[dict] = []
|
||||
for months in self.spec.durations_months:
|
||||
strategy = self._protective_put(months)
|
||||
protection = strategy.calculate_protection()
|
||||
rows.append(
|
||||
{
|
||||
"months": months,
|
||||
"payoff_at_threshold": protection["payoff_at_threshold"],
|
||||
"hedged_ltv_at_threshold": protection["hedged_ltv_at_threshold"],
|
||||
"maintains_margin_call_buffer": protection["maintains_margin_call_buffer"],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"threshold_price": round(threshold_price, 2),
|
||||
"durations": rows,
|
||||
}
|
||||
|
||||
def get_scenarios(self) -> list[dict]:
|
||||
scenarios: list[dict] = []
|
||||
for months in self.spec.durations_months:
|
||||
strategy = self._protective_put(months)
|
||||
scenarios.append(
|
||||
{
|
||||
"months": months,
|
||||
"annualized_cost": strategy.calculate_cost()["annualized_cost"],
|
||||
"annualized_cost_pct": strategy.calculate_cost()["annualized_cost_pct"],
|
||||
"sample_scenarios": strategy.get_scenarios(),
|
||||
}
|
||||
)
|
||||
return scenarios
|
||||
139
app/strategies/protective_put.py
Normal file
139
app/strategies/protective_put.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
|
||||
from app.core.pricing.black_scholes import BlackScholesInputs, black_scholes_price_and_greeks
|
||||
from app.models.option import Greeks, OptionContract
|
||||
from app.models.strategy import HedgingStrategy
|
||||
from app.strategies.base import BaseStrategy, StrategyConfig
|
||||
|
||||
DEFAULT_SCENARIO_CHANGES = (-0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProtectivePutSpec:
|
||||
label: str
|
||||
strike_pct: float
|
||||
months: int = 12
|
||||
|
||||
|
||||
class ProtectivePutStrategy(BaseStrategy):
|
||||
"""Single-leg protective put strategy using ATM or configurable OTM strikes."""
|
||||
|
||||
def __init__(self, config: StrategyConfig, spec: ProtectivePutSpec) -> None:
|
||||
super().__init__(config)
|
||||
self.spec = spec
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"protective_put_{self.spec.label.lower()}"
|
||||
|
||||
@property
|
||||
def hedge_units(self) -> float:
|
||||
return self.config.portfolio.gold_value / self.config.spot_price
|
||||
|
||||
@property
|
||||
def strike(self) -> float:
|
||||
return self.config.spot_price * self.spec.strike_pct
|
||||
|
||||
@property
|
||||
def term_years(self) -> float:
|
||||
return self.spec.months / 12.0
|
||||
|
||||
def build_contract(self) -> OptionContract:
|
||||
pricing = black_scholes_price_and_greeks(
|
||||
BlackScholesInputs(
|
||||
spot=self.config.spot_price,
|
||||
strike=self.strike,
|
||||
time_to_expiry=self.term_years,
|
||||
risk_free_rate=self.config.risk_free_rate,
|
||||
volatility=self.config.volatility,
|
||||
option_type="put",
|
||||
)
|
||||
)
|
||||
return OptionContract(
|
||||
option_type="put",
|
||||
strike=self.strike,
|
||||
expiry=date.today() + timedelta(days=max(1, round(365 * self.term_years))),
|
||||
premium=pricing.price,
|
||||
quantity=1.0,
|
||||
contract_size=self.hedge_units,
|
||||
underlying_price=self.config.spot_price,
|
||||
greeks=Greeks(
|
||||
delta=pricing.delta,
|
||||
gamma=pricing.gamma,
|
||||
theta=pricing.theta,
|
||||
vega=pricing.vega,
|
||||
rho=pricing.rho,
|
||||
),
|
||||
)
|
||||
|
||||
def build_hedging_strategy(self) -> HedgingStrategy:
|
||||
return HedgingStrategy(
|
||||
strategy_type="single_put",
|
||||
long_contracts=(self.build_contract(),),
|
||||
description=f"{self.spec.label} protective put",
|
||||
)
|
||||
|
||||
def calculate_cost(self) -> dict:
|
||||
contract = self.build_contract()
|
||||
total_cost = contract.total_premium
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"label": self.spec.label,
|
||||
"strike": round(contract.strike, 2),
|
||||
"strike_pct": self.spec.strike_pct,
|
||||
"premium_per_share": round(contract.premium, 4),
|
||||
"total_cost": round(total_cost, 2),
|
||||
"cost_pct_of_portfolio": round(total_cost / self.config.portfolio.gold_value, 6),
|
||||
"term_months": self.spec.months,
|
||||
"annualized_cost": round(total_cost / self.term_years, 2),
|
||||
"annualized_cost_pct": round((total_cost / self.term_years) / self.config.portfolio.gold_value, 6),
|
||||
}
|
||||
|
||||
def calculate_protection(self) -> dict:
|
||||
contract = self.build_contract()
|
||||
threshold_price = self.config.portfolio.margin_call_price()
|
||||
payoff_at_threshold = contract.payoff(threshold_price)
|
||||
hedged_value_at_threshold = self.config.portfolio.gold_value_at_price(threshold_price) + payoff_at_threshold
|
||||
protected_ltv = self.config.portfolio.loan_amount / hedged_value_at_threshold
|
||||
floor_value = contract.strike * self.hedge_units
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"threshold_price": round(threshold_price, 2),
|
||||
"strike": round(contract.strike, 2),
|
||||
"portfolio_floor_value": round(floor_value, 2),
|
||||
"unhedged_ltv_at_threshold": round(self.config.portfolio.ltv_at_price(threshold_price), 6),
|
||||
"hedged_ltv_at_threshold": round(protected_ltv, 6),
|
||||
"payoff_at_threshold": round(payoff_at_threshold, 2),
|
||||
"maintains_margin_call_buffer": protected_ltv < self.config.portfolio.margin_call_ltv,
|
||||
}
|
||||
|
||||
def get_scenarios(self) -> list[dict]:
|
||||
strategy = self.build_hedging_strategy()
|
||||
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 = strategy.gross_payoff(price)
|
||||
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(strategy.hedge_cost, 2),
|
||||
"net_portfolio_value": round(gold_value + option_payoff - strategy.hedge_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