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:
Bu5hm4nn
2026-03-21 19:21:40 +01:00
commit 00a68bc767
63 changed files with 6239 additions and 0 deletions

15
app/models/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
"""Application domain models."""
from .option import Greeks, OptionContract, OptionMoneyness
from .portfolio import LombardPortfolio
from .strategy import HedgingStrategy, ScenarioResult, StrategyType
__all__ = [
"Greeks",
"HedgingStrategy",
"LombardPortfolio",
"OptionContract",
"OptionMoneyness",
"ScenarioResult",
"StrategyType",
]

109
app/models/option.py Normal file
View File

@@ -0,0 +1,109 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Literal
OptionType = Literal["call", "put"]
OptionMoneyness = Literal["ITM", "ATM", "OTM"]
@dataclass(frozen=True)
class Greeks:
"""Option Greeks container."""
delta: float = 0.0
gamma: float = 0.0
theta: float = 0.0
vega: float = 0.0
rho: float = 0.0
@dataclass(frozen=True)
class OptionContract:
"""Vanilla option contract used in hedging strategies.
Attributes:
option_type: Contract type, either ``"put"`` or ``"call"``.
strike: Strike price.
expiry: Expiration date.
premium: Premium paid or received per unit of underlying.
quantity: Number of contracts or units.
contract_size: Underlying units per contract.
underlying_price: Current underlying spot price for classification.
greeks: Stored option Greeks.
"""
option_type: OptionType
strike: float
expiry: date
premium: float
quantity: float = 1.0
contract_size: float = 1.0
underlying_price: float | None = None
greeks: Greeks = Greeks()
def __post_init__(self) -> None:
option = self.option_type.lower()
if option not in {"call", "put"}:
raise ValueError("option_type must be either 'call' or 'put'")
object.__setattr__(self, "option_type", option)
if self.strike <= 0:
raise ValueError("strike must be positive")
if self.premium < 0:
raise ValueError("premium must be non-negative")
if self.quantity <= 0:
raise ValueError("quantity must be positive")
if self.contract_size <= 0:
raise ValueError("contract_size must be positive")
if self.expiry <= date.today():
raise ValueError("expiry must be in the future")
if self.underlying_price is not None and self.underlying_price <= 0:
raise ValueError("underlying_price must be positive when provided")
@property
def notional_units(self) -> float:
"""Underlying units covered by the contract position."""
return self.quantity * self.contract_size
@property
def total_premium(self) -> float:
"""Total premium paid or received for the position."""
return self.premium * self.notional_units
def classify_moneyness(self, underlying_price: float | None = None, *, atm_tolerance: float = 0.01) -> OptionMoneyness:
"""Classify the contract as ITM, ATM, or OTM.
Args:
underlying_price: Spot price used for classification. Falls back to
``self.underlying_price``.
atm_tolerance: Relative tolerance around strike treated as at-the-money.
"""
spot = self.underlying_price if underlying_price is None else underlying_price
if spot is None:
raise ValueError("underlying_price must be provided for strategy classification")
if spot <= 0:
raise ValueError("underlying_price must be positive")
if atm_tolerance < 0:
raise ValueError("atm_tolerance must be non-negative")
relative_gap = abs(spot - self.strike) / self.strike
if relative_gap <= atm_tolerance:
return "ATM"
if self.option_type == "put":
return "ITM" if self.strike > spot else "OTM"
return "ITM" if self.strike < spot else "OTM"
def intrinsic_value(self, underlying_price: float) -> float:
"""Intrinsic value per underlying unit at a given spot price."""
if underlying_price <= 0:
raise ValueError("underlying_price must be positive")
if self.option_type == "put":
return max(self.strike - underlying_price, 0.0)
return max(underlying_price - self.strike, 0.0)
def payoff(self, underlying_price: float) -> float:
"""Gross payoff of the option position at expiry."""
return self.intrinsic_value(underlying_price) * self.notional_units

71
app/models/portfolio.py Normal file
View File

@@ -0,0 +1,71 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class LombardPortfolio:
"""Lombard loan portfolio backed by physical gold.
Attributes:
gold_ounces: Quantity of pledged gold in troy ounces.
gold_price_per_ounce: Current gold spot price per ounce.
loan_amount: Outstanding Lombard loan balance.
initial_ltv: Origination or current reference loan-to-value ratio.
margin_call_ltv: LTV threshold at which a margin call is triggered.
"""
gold_ounces: float
gold_price_per_ounce: float
loan_amount: float
initial_ltv: float
margin_call_ltv: float
def __post_init__(self) -> None:
if self.gold_ounces <= 0:
raise ValueError("gold_ounces must be positive")
if self.gold_price_per_ounce <= 0:
raise ValueError("gold_price_per_ounce must be positive")
if self.loan_amount < 0:
raise ValueError("loan_amount must be non-negative")
if not 0 < self.initial_ltv < 1:
raise ValueError("initial_ltv must be between 0 and 1")
if not 0 < self.margin_call_ltv < 1:
raise ValueError("margin_call_ltv must be between 0 and 1")
if self.initial_ltv > self.margin_call_ltv:
raise ValueError("initial_ltv cannot exceed margin_call_ltv")
if self.loan_amount > self.gold_value:
raise ValueError("loan_amount cannot exceed current gold value")
@property
def gold_value(self) -> float:
"""Current market value of pledged gold."""
return self.gold_ounces * self.gold_price_per_ounce
@property
def current_ltv(self) -> float:
"""Current loan-to-value ratio."""
return self.loan_amount / self.gold_value
@property
def net_equity(self) -> float:
"""Equity remaining after subtracting the loan from gold value."""
return self.gold_value - self.loan_amount
def gold_value_at_price(self, gold_price_per_ounce: float) -> float:
"""Gold value under an alternative spot-price scenario."""
if gold_price_per_ounce <= 0:
raise ValueError("gold_price_per_ounce must be positive")
return self.gold_ounces * gold_price_per_ounce
def ltv_at_price(self, gold_price_per_ounce: float) -> float:
"""Portfolio LTV under an alternative gold-price scenario."""
return self.loan_amount / self.gold_value_at_price(gold_price_per_ounce)
def net_equity_at_price(self, gold_price_per_ounce: float) -> float:
"""Net equity under an alternative gold-price scenario."""
return self.gold_value_at_price(gold_price_per_ounce) - self.loan_amount
def margin_call_price(self) -> float:
"""Gold price per ounce at which the portfolio breaches the margin LTV."""
return self.loan_amount / (self.margin_call_ltv * self.gold_ounces)

101
app/models/strategy.py Normal file
View File

@@ -0,0 +1,101 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
from .option import OptionContract
StrategyType = Literal["single_put", "laddered_put", "collar"]
@dataclass(frozen=True)
class ScenarioResult:
"""Scenario output for a hedging strategy."""
underlying_price: float
gross_option_payoff: float
hedge_cost: float
net_option_benefit: float
@dataclass(frozen=True)
class HedgingStrategy:
"""Collection of option positions representing a hedge.
Notes:
Premiums on long positions are positive cash outflows. Premiums on
short positions are handled through ``short_contracts`` and reduce the
total hedge cost.
"""
strategy_type: StrategyType
long_contracts: tuple[OptionContract, ...] = field(default_factory=tuple)
short_contracts: tuple[OptionContract, ...] = field(default_factory=tuple)
description: str = ""
def __post_init__(self) -> None:
if self.strategy_type not in {"single_put", "laddered_put", "collar"}:
raise ValueError("unsupported strategy_type")
if not self.long_contracts and not self.short_contracts:
raise ValueError("at least one option contract is required")
if self.strategy_type == "single_put":
if len(self.long_contracts) != 1 or self.long_contracts[0].option_type != "put":
raise ValueError("single_put requires exactly one long put contract")
if self.short_contracts:
raise ValueError("single_put cannot include short contracts")
if self.strategy_type == "laddered_put":
if len(self.long_contracts) < 2:
raise ValueError("laddered_put requires at least two long put contracts")
if any(contract.option_type != "put" for contract in self.long_contracts):
raise ValueError("laddered_put supports only long put contracts")
if self.short_contracts:
raise ValueError("laddered_put cannot include short contracts")
if self.strategy_type == "collar":
if not self.long_contracts or not self.short_contracts:
raise ValueError("collar requires both long and short contracts")
if any(contract.option_type != "put" for contract in self.long_contracts):
raise ValueError("collar long leg must be put options")
if any(contract.option_type != "call" for contract in self.short_contracts):
raise ValueError("collar short leg must be call options")
@property
def hedge_cost(self) -> float:
"""Net upfront hedge cost."""
long_cost = sum(contract.total_premium for contract in self.long_contracts)
short_credit = sum(contract.total_premium for contract in self.short_contracts)
return long_cost - short_credit
def gross_payoff(self, underlying_price: float) -> float:
"""Gross expiry payoff from all option legs."""
if underlying_price <= 0:
raise ValueError("underlying_price must be positive")
long_payoff = sum(contract.payoff(underlying_price) for contract in self.long_contracts)
short_payoff = sum(contract.payoff(underlying_price) for contract in self.short_contracts)
return long_payoff - short_payoff
def net_benefit(self, underlying_price: float) -> float:
"""Net value added by the hedge after premium cost."""
return self.gross_payoff(underlying_price) - self.hedge_cost
def scenario_analysis(self, underlying_prices: list[float] | tuple[float, ...]) -> list[ScenarioResult]:
"""Evaluate the hedge across alternative underlying-price scenarios."""
if not underlying_prices:
raise ValueError("underlying_prices must not be empty")
results: list[ScenarioResult] = []
for price in underlying_prices:
if price <= 0:
raise ValueError("scenario prices must be positive")
gross_payoff = self.gross_payoff(price)
results.append(
ScenarioResult(
underlying_price=price,
gross_option_payoff=gross_payoff,
hedge_cost=self.hedge_cost,
net_option_benefit=gross_payoff - self.hedge_cost,
)
)
return results