- 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
140 lines
5.7 KiB
Python
140 lines
5.7 KiB
Python
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
|