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

593
docs/API.md Normal file
View File

@@ -0,0 +1,593 @@
# API Documentation
## Overview
Vault Dashboard exposes a small read-only HTTP API plus a WebSocket stream.
Base capabilities:
- health monitoring
- portfolio snapshot retrieval
- options chain retrieval
- strategy analysis retrieval
- periodic real-time portfolio updates over WebSocket
Unless noted otherwise, all responses are JSON.
---
## Conventions
### Base URL
Examples:
```text
http://localhost:8000
https://vault.example.com
```
### Content type
Responses use:
```http
Content-Type: application/json
```
### Authentication
There is currently **no application-layer authentication** on these endpoints.
If the deployment requires restricted access, enforce it at the network or reverse-proxy layer.
### Errors
The app currently relies mostly on framework defaults. Typical failures may include:
- `422 Unprocessable Entity` for invalid query parameters
- `500 Internal Server Error` for unexpected runtime issues
---
## HTTP endpoints
## 1. Health
### `GET /health`
Deployment and uptime check.
#### Query parameters
None.
#### Response schema
```json
{
"status": "ok",
"environment": "production",
"redis_enabled": false
}
```
#### Field definitions
- `status` (`string`): expected value is currently `"ok"`
- `environment` (`string`): runtime environment from `APP_ENV` or `ENVIRONMENT`
- `redis_enabled` (`boolean`): `true` when Redis is configured and connected
#### Example request
```bash
curl -fsS http://localhost:8000/health
```
#### Example response
```json
{
"status": "ok",
"environment": "development",
"redis_enabled": false
}
```
---
## 2. Portfolio
### `GET /api/portfolio`
Returns a portfolio snapshot derived from the current quote for a symbol.
#### Query parameters
| Name | Type | Required | Default | Description |
|---|---|---:|---|---|
| `symbol` | string | no | `GLD` | Ticker symbol to analyze |
#### Response schema
```json
{
"symbol": "GLD",
"spot_price": 215.0,
"portfolio_value": 215000.0,
"loan_amount": 600000.0,
"ltv_ratio": 2.7907,
"updated_at": "2026-03-21T12:34:56.000000+00:00",
"source": "fallback"
}
```
#### Field definitions
- `symbol` (`string`): requested ticker, uppercased
- `spot_price` (`number`): latest spot/quote price
- `portfolio_value` (`number`): current modeled collateral value, currently `spot_price * 1000`
- `loan_amount` (`number`): modeled loan balance, currently fixed at `600000.0`
- `ltv_ratio` (`number`): `loan_amount / portfolio_value`
- `updated_at` (`string`, ISO 8601): response generation timestamp
- `source` (`string`): quote source such as `yfinance` or `fallback`
#### Example request
```bash
curl "http://localhost:8000/api/portfolio?symbol=GLD"
```
#### Example response
```json
{
"symbol": "GLD",
"spot_price": 215.0,
"portfolio_value": 215000.0,
"loan_amount": 600000.0,
"ltv_ratio": 2.7907,
"updated_at": "2026-03-21T12:34:56.000000+00:00",
"source": "fallback"
}
```
---
## 3. Options chain
### `GET /api/options`
Returns a simplified options chain snapshot for the symbol.
#### Query parameters
| Name | Type | Required | Default | Description |
|---|---|---:|---|---|
| `symbol` | string | no | `GLD` | Ticker symbol to analyze |
#### Response schema
```json
{
"symbol": "GLD",
"updated_at": "2026-03-21T12:34:56.000000+00:00",
"calls": [
{
"strike": 225.75,
"premium": 6.45,
"expiry": "2026-06-19"
}
],
"puts": [
{
"strike": 204.25,
"premium": 6.02,
"expiry": "2026-06-19"
}
],
"source": "fallback"
}
```
#### Array item schema
Each option row in `calls` or `puts` has:
- `strike` (`number`)
- `premium` (`number`)
- `expiry` (`string`, `YYYY-MM-DD`)
#### Field definitions
- `symbol` (`string`): requested ticker, uppercased
- `updated_at` (`string`, ISO 8601): response generation timestamp
- `calls` (`array<object>`): example call rows derived from spot
- `puts` (`array<object>`): example put rows derived from spot
- `source` (`string`): upstream quote source used to derive the chain
#### Notes
The current options chain is synthetic. It is not yet a full broker-grade chain feed.
#### Example request
```bash
curl "http://localhost:8000/api/options?symbol=GLD"
```
---
## 4. Strategies
### `GET /api/strategies`
Returns strategy comparison data, recommendations by risk profile, and sensitivity analysis.
#### Query parameters
| Name | Type | Required | Default | Description |
|---|---|---:|---|---|
| `symbol` | string | no | `GLD` | Ticker symbol to analyze |
#### Top-level response schema
```json
{
"symbol": "GLD",
"updated_at": "2026-03-21T12:34:56.000000+00:00",
"paper_parameters": {},
"strategies": [],
"recommendations": {},
"sensitivity_analysis": {}
}
```
#### Top-level field definitions
- `symbol` (`string`): requested ticker, uppercased
- `updated_at` (`string`, ISO 8601): response generation timestamp
- `paper_parameters` (`object`): engine inputs used for the analysis
- `strategies` (`array<object>`): detailed strategy comparison rows
- `recommendations` (`object`): recommendation results keyed by risk profile
- `sensitivity_analysis` (`object`): recommendation changes across parameter shifts
### 4.1 `paper_parameters` schema
```json
{
"portfolio_value": 1000000.0,
"loan_amount": 600000.0,
"margin_call_threshold": 0.75,
"spot_price": 460.0,
"volatility": 0.16,
"risk_free_rate": 0.045
}
```
Fields:
- `portfolio_value` (`number`)
- `loan_amount` (`number`)
- `margin_call_threshold` (`number`)
- `spot_price` (`number`)
- `volatility` (`number`)
- `risk_free_rate` (`number`)
### 4.2 `strategies[]` schema
Each element in `strategies` is produced by `StrategySelectionEngine.compare_all_strategies()`:
```json
{
"name": "protective_put_atm",
"cost": {},
"protection": {},
"scenarios": [],
"score_inputs": {
"annual_cost": 0.0,
"hedged_ltv_at_threshold": 0.0
}
}
```
Fields:
- `name` (`string`): internal strategy identifier
- `cost` (`object`): strategy-specific cost payload
- `protection` (`object`): strategy-specific protection payload
- `scenarios` (`array<object>`): scenario analysis rows
- `score_inputs` (`object`): normalized inputs used for recommendation scoring
#### Protective put cost schema
Typical `cost` object for `protective_put_*`:
```json
{
"strategy": "protective_put_atm",
"label": "ATM",
"strike": 460.0,
"strike_pct": 1.0,
"premium_per_share": 21.1234,
"total_cost": 45920.43,
"cost_pct_of_portfolio": 0.04592,
"term_months": 12,
"annualized_cost": 45920.43,
"annualized_cost_pct": 0.04592
}
```
#### Protective put protection schema
```json
{
"strategy": "protective_put_atm",
"threshold_price": 368.0,
"strike": 460.0,
"portfolio_floor_value": 1000000.0,
"unhedged_ltv_at_threshold": 0.75,
"hedged_ltv_at_threshold": 0.652174,
"payoff_at_threshold": 200000.0,
"maintains_margin_call_buffer": true
}
```
#### Protective put scenario row schema
```json
{
"price_change_pct": -0.2,
"gld_price": 368.0,
"gold_value": 800000.0,
"option_payoff": 200000.0,
"hedge_cost": 45920.43,
"net_portfolio_value": 954079.57,
"unhedged_ltv": 0.75,
"hedged_ltv": 0.6,
"margin_call_without_hedge": true,
"margin_call_with_hedge": false
}
```
#### Laddered put cost schema
Typical `cost` object for `laddered_put_*`:
```json
{
"strategy": "laddered_put_50_50_atm_otm95",
"label": "50_50_ATM_OTM95",
"legs": [
{
"weight": 0.5,
"strike": 460.0,
"premium_per_share": 21.1234,
"weighted_cost": 22960.22
}
],
"blended_premium_per_share": 18.4567,
"blended_cost": 40123.45,
"cost_pct_of_portfolio": 0.040123,
"annualized_cost": 40123.45,
"annualized_cost_pct": 0.040123
}
```
#### Laddered put protection schema
```json
{
"strategy": "laddered_put_50_50_atm_otm95",
"threshold_price": 368.0,
"portfolio_floor_value": 975000.0,
"payoff_at_threshold": 175000.0,
"unhedged_ltv_at_threshold": 0.75,
"hedged_ltv_at_threshold": 0.615385,
"maintains_margin_call_buffer": true,
"legs": [
{
"weight": 0.5,
"strike": 460.0,
"weighted_payoff_at_threshold": 100000.0
}
]
}
```
#### Lease duration analysis cost schema
Typical `cost` object for `lease_duration_analysis`:
```json
{
"strategy": "lease_duration_analysis",
"comparison": [
{
"months": 3,
"strike": 460.0,
"premium_per_share": 9.1234,
"total_cost": 19833.48,
"annualized_cost": 79333.92,
"annualized_cost_pct": 0.079334,
"rolls_per_year": 4.0,
"recommended_roll_month": 2
}
],
"optimal_duration_months": 12,
"lowest_annual_cost": 45920.43,
"lowest_annual_cost_pct": 0.04592
}
```
#### Lease duration analysis protection schema
```json
{
"strategy": "lease_duration_analysis",
"threshold_price": 368.0,
"durations": [
{
"months": 12,
"payoff_at_threshold": 200000.0,
"hedged_ltv_at_threshold": 0.6,
"maintains_margin_call_buffer": true
}
]
}
```
### 4.3 `recommendations` schema
The object contains keys:
- `conservative`
- `balanced`
- `cost_sensitive`
Each recommendation object has this shape:
```json
{
"risk_profile": "balanced",
"recommended_strategy": "laddered_put_50_50_atm_otm95",
"rationale": {
"portfolio_value": 1000000.0,
"loan_amount": 600000.0,
"margin_call_threshold": 0.75,
"spot_price": 460.0,
"volatility": 0.16,
"risk_free_rate": 0.045
},
"comparison_summary": [
{
"name": "protective_put_atm",
"annual_cost": 45920.43,
"hedged_ltv_at_threshold": 0.6
}
]
}
```
### 4.4 `sensitivity_analysis` schema
```json
{
"volatility": [
{
"volatility": 0.12,
"recommended_strategy": "protective_put_otm_95"
}
],
"spot_price": [
{
"spot_price": 414.0,
"recommended_strategy": "protective_put_otm_95"
}
]
}
```
---
## WebSocket API
## Endpoint
### `WS /ws/updates`
Used for server-pushed real-time updates.
### Connection lifecycle
1. Client opens a WebSocket connection to `/ws/updates`
2. Server accepts the connection
3. Server immediately sends a `connected` event
4. Server periodically broadcasts `portfolio_update` events
5. Client may keep the connection alive by sending text frames
6. Server removes the connection when disconnected or on send failure
### Event: `connected`
Sent once after successful connection.
#### Schema
```json
{
"type": "connected",
"message": "Real-time updates enabled"
}
```
Fields:
- `type` (`string`): event name
- `message` (`string`): human-readable confirmation
### Event: `portfolio_update`
Broadcast on an interval controlled by `WEBSOCKET_INTERVAL_SECONDS`.
#### Schema
```json
{
"type": "portfolio_update",
"connections": 2,
"portfolio": {
"symbol": "GLD",
"spot_price": 215.0,
"portfolio_value": 215000.0,
"loan_amount": 600000.0,
"ltv_ratio": 2.7907,
"updated_at": "2026-03-21T12:34:56.000000+00:00",
"source": "fallback"
}
}
```
Fields:
- `type` (`string`): event name
- `connections` (`integer`): current number of connected WebSocket clients
- `portfolio` (`object`): same schema as `GET /api/portfolio`
### Example JavaScript client
```js
const ws = new WebSocket('ws://localhost:8000/ws/updates');
ws.onmessage = (event) => {
const payload = JSON.parse(event.data);
console.log(payload.type, payload);
};
ws.onopen = () => {
// optional keepalive or ping surrogate
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('ping');
}
}, 10000);
};
```
---
## OpenAPI
Because the app is built on FastAPI, interactive docs are typically available at:
- `/docs`
- `/redoc`
This file is the human-oriented reference for payload semantics and current behavior.
## Notes and limitations
- Endpoints are read-only today
- There are no POST/PUT/DELETE endpoints yet
- The options chain is currently synthetic
- Strategy outputs are paper-analysis results, not execution instructions
- Symbol validation is minimal and currently delegated to downstream quote behavior

437
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,437 @@
# Architecture
## Overview
Vault Dashboard is a FastAPI application with NiceGUI pages for the frontend, a lightweight API layer for market and strategy data, a strategy engine for paper hedge comparisons, and an optional Redis cache.
At runtime the app exposes:
- HTML/UI pages via NiceGUI
- REST-style JSON endpoints under `/api`
- a health endpoint at `/health`
- a WebSocket feed at `/ws/updates`
The system is currently optimized for research, visualization, and paper analysis of Lombard-loan hedging strategies rather than live trade execution.
---
## System components
### 1. Application entry point
**File:** `app/main.py`
Responsibilities:
- create the FastAPI app
- load environment-driven settings
- configure CORS
- initialize cache and data services during lifespan startup
- start a background publisher task for WebSocket updates
- mount NiceGUI onto the app
- expose `/health` and `/ws/updates`
### 2. API layer
**File:** `app/api/routes.py`
Responsibilities:
- expose JSON endpoints under `/api`
- resolve the shared `DataService` from application state
- provide read-only portfolio, options, and strategy data
Current endpoints:
- `GET /api/portfolio`
- `GET /api/options`
- `GET /api/strategies`
### 3. UI layer
**Files:** `app/pages/*.py`, `app/components/*.py`
Responsibilities:
- render dashboard pages using NiceGUI
- present charts, tables, strategy views, and scenario widgets
- consume data generated within the app and, in production, align with API/WebSocket-backed state
Representative pages:
- `app/pages/overview.py`
- `app/pages/options.py`
- `app/pages/hedge.py`
- `app/pages/settings.py`
### 4. Data service
**File:** `app/services/data_service.py`
Responsibilities:
- fetch quote data
- build a synthetic options chain response
- build portfolio snapshots
- invoke the strategy engine and shape strategy comparison responses
- cache results when Redis is available
Data source behavior:
- primary live quote source: `yfinance` when installed and reachable
- fallback quote source: static fallback data if live fetch fails
### 5. Cache service
**File:** `app/services/cache.py`
Responsibilities:
- provide async JSON get/set operations
- wrap Redis without making the rest of the app depend directly on Redis primitives
- degrade gracefully when Redis is unavailable
The app remains functional without Redis; caching is optional.
### 6. Strategy engine
**Files:**
- `app/strategies/engine.py`
- `app/strategies/base.py`
- `app/strategies/protective_put.py`
- `app/strategies/laddered_put.py`
- `app/strategies/lease.py`
Responsibilities:
- construct standardized paper strategies for comparison
- calculate cost and protection metrics
- run scenario analysis across price shocks
- recommend a strategy by risk profile
- run simple sensitivity analysis
### 7. Domain models
**Files:**
- `app/models/portfolio.py`
- `app/models/option.py`
- `app/models/strategy.py`
Responsibilities:
- represent Lombard-backed portfolios
- represent option contracts and Greeks
- represent multi-leg hedging structures
- enforce validation rules at object boundaries
### 8. Pricing layer
**Files:** `app/core/pricing/*.py`
Responsibilities:
- compute option prices and Greeks
- support Black-Scholes-based valuation inputs used by the research strategies
---
## High-level data flow
```mermaid
flowchart TD
A[Browser / NiceGUI Client] -->|HTTP| B[FastAPI + NiceGUI app]
A -->|WebSocket /ws/updates| B
B --> C[API routes]
B --> D[ConnectionManager + background publisher]
C --> E[DataService]
D --> E
E --> F[CacheService]
F -->|optional| G[(Redis)]
E --> H[yfinance]
E --> I[StrategySelectionEngine]
I --> J[ProtectivePutStrategy]
I --> K[LadderedPutStrategy]
I --> L[LeaseStrategy]
J --> M[Pricing + models]
K --> M
L --> M
```
### Request/response flow
1. Client sends an HTTP request to an API endpoint or loads a NiceGUI page
2. FastAPI resolves shared app services from `app.state`
3. `DataService` checks Redis cache first when enabled
4. If cache misses, `DataService` fetches or builds the payload:
- quote via `yfinance` or fallback
- synthetic options chain
- strategy comparison via `StrategySelectionEngine`
5. Response is returned as JSON or used by the UI
6. Background task periodically broadcasts portfolio snapshots over WebSocket
---
## Runtime lifecycle
### Startup
When the app starts:
1. environment variables are loaded into `Settings`
2. `CacheService` is created and attempts Redis connection
3. `DataService` is initialized
4. `ConnectionManager` is initialized
5. background task `publish_updates()` starts
6. NiceGUI is mounted on the FastAPI app
### Steady state
During normal operation:
- API requests are served on demand
- WebSocket clients stay connected to `/ws/updates`
- every `WEBSOCKET_INTERVAL_SECONDS`, the app publishes a fresh portfolio payload
- Redis caches repeated quote/portfolio/options requests when configured
### Shutdown
On shutdown:
- publisher task is cancelled
- cache connection is closed
- FastAPI lifecycle exits cleanly
---
## Strategy engine design
## Core design goals
The strategy subsystem is built to compare paper hedging approaches for a Lombard loan secured by gold exposure. It emphasizes:
- deterministic calculations
- shared configuration across strategies
- comparable output shapes
- easy extension for new strategies
### Base contract
**File:** `app/strategies/base.py`
All strategies implement:
- `name`
- `calculate_cost()`
- `calculate_protection()`
- `get_scenarios()`
All strategies receive a shared `StrategyConfig` containing:
- `portfolio`
- `spot_price`
- `volatility`
- `risk_free_rate`
### Portfolio construction
**File:** `app/strategies/engine.py`
`StrategySelectionEngine` builds a canonical research portfolio using:
- portfolio value
- loan amount
- margin call threshold
- spot price
- volatility
- risk-free rate
The engine converts these into a validated `LombardPortfolio`, then instantiates a suite of candidate strategies.
### Candidate strategies
Current strategy set:
1. `protective_put_atm`
2. `protective_put_otm_95`
3. `protective_put_otm_90`
4. `laddered_put_50_50_atm_otm95`
5. `laddered_put_33_33_33_atm_otm95_otm90`
6. `lease_duration_analysis`
### Strategy outputs
Each strategy returns three complementary views:
#### Cost view
Examples:
- premium per share
- total hedge cost
- annualized cost
- cost as percentage of portfolio
- weighted leg costs for ladders
#### Protection view
Examples:
- threshold price where margin stress occurs
- payoff at threshold
- hedged LTV at threshold
- whether the strategy maintains a buffer below margin-call LTV
- floor value implied by option strikes
#### Scenario view
Examples:
- underlying price change percentage
- simulated spot price
- unhedged vs hedged LTV
- option payoff
- hedge cost
- net portfolio value
- margin call triggered or avoided
### Recommendation model
`StrategySelectionEngine.recommend()` scores strategies using a small heuristic.
Risk profiles:
- `conservative`: prioritize lower hedged LTV, then lower annual cost
- `cost_sensitive`: prioritize lower annual cost, then lower hedged LTV
- `balanced`: combine hedged LTV and normalized annual cost
This is a ranking heuristic, not an optimizer or live execution model.
### Sensitivity analysis
The engine also reruns recommendations across:
- multiple volatility assumptions
- multiple spot-price assumptions
This helps identify whether a recommendation is robust to input changes.
---
## Data flow diagram for strategy computation
```mermaid
sequenceDiagram
participant Client
participant API as /api/strategies
participant DS as DataService
participant SE as StrategySelectionEngine
participant S as Strategy implementations
Client->>API: GET /api/strategies?symbol=GLD
API->>DS: get_strategies(symbol)
DS->>DS: get_quote(symbol)
DS->>SE: create engine with spot and research parameters
SE->>S: compare_all_strategies()
S-->>SE: cost/protection/scenario payloads
SE->>SE: recommend() by risk profile
SE->>SE: sensitivity_analysis()
SE-->>DS: comparison + recommendations + sensitivity
DS-->>API: JSON response
API-->>Client: strategies payload
```
---
## API endpoints
### Health
- `GET /health`
Purpose:
- liveness/readiness-style check for deploy validation
Returns:
- application status
- current environment
- whether Redis is enabled
### Portfolio API
- `GET /api/portfolio?symbol=GLD`
Purpose:
- return a current portfolio snapshot derived from the latest quote
### Options API
- `GET /api/options?symbol=GLD`
Purpose:
- return a simplified options chain snapshot for the selected symbol
### Strategies API
- `GET /api/strategies?symbol=GLD`
Purpose:
- return strategy comparisons, recommendations, and sensitivity analysis
### WebSocket updates
- `WS /ws/updates`
Purpose:
- push periodic `portfolio_update` messages to connected clients
---
## Deployment architecture
Production deployment currently assumes:
- containerized app on a VPS
- image stored in GitLab Container Registry
- deployment initiated by GitLab CI/CD over SSH
- optional Redis, depending on runtime configuration
- VPN-restricted network access preferred
- reverse proxy/TLS termination recommended in front of the app
```mermaid
flowchart LR
A[GitLab CI/CD] -->|build + push| B[GitLab Container Registry]
A -->|SSH deploy| C[VPS]
B -->|docker pull| C
C --> D[vault-dash container]
E[VPN / Reverse Proxy] --> D
```
---
## Architectural constraints and assumptions
- Strategy calculations are currently research-oriented, not broker-executed trades
- Quote retrieval is best-effort and may fall back to static data
- Options chain payloads are synthetic examples, not a full market data feed
- Redis is optional and the app must work without it
- WebSocket updates currently publish portfolio snapshots only
- NiceGUI and API routes run in the same Python application process
## Near-term extension points
Likely future architecture additions:
- real broker integration for positions and option chains
- persistent storage for scenarios, settings, and user sessions
- reverse proxy configuration in deployment Compose files
- authenticated API access
- OAuth provider integration over HTTPS
- richer WebSocket event types

424
docs/STRATEGIES.md Normal file
View File

@@ -0,0 +1,424 @@
# Strategy Documentation
## Overview
Vault Dashboard currently documents and compares hedging approaches for a Lombard-style loan backed by gold exposure. The implementation focuses on paper analysis using option pricing, LTV protection metrics, and scenario analysis.
The strategy subsystem currently includes:
- protective puts
- laddered puts
- lease/LEAPS duration analysis
This document focuses on the two primary hedge structures requested here:
- protective put
- laddered put
---
## Common portfolio assumptions
The default research engine in `app/strategies/engine.py` uses:
- portfolio value: `1,000,000`
- loan amount: `600,000`
- margin-call threshold: `0.75`
- spot price: `460`
- volatility: `0.16`
- risk-free rate: `0.045`
From these values, the portfolio is modeled as a `LombardPortfolio`:
- gold ounces = `portfolio_value / spot_price`
- initial LTV = `loan_amount / portfolio_value`
- margin call price = `loan_amount / (margin_call_ltv * gold_ounces)`
These assumptions create the common basis used to compare all strategies.
---
## Protective put
## What it is
A protective put is the simplest downside hedge in the project.
Structure:
- long the underlying collateral exposure implicitly represented by the gold-backed portfolio
- buy one put hedge sized to the portfolio's underlying units
In this codebase, `ProtectivePutStrategy` creates a single long put whose strike is defined as a percentage of spot.
Examples currently used by the engine:
- ATM protective put: strike = `100%` of spot
- 95% OTM protective put: strike = `95%` of spot
- 90% OTM protective put: strike = `90%` of spot
## Why use it
A protective put sets a floor on downside beyond the strike, helping reduce the chance that falling collateral value pushes the portfolio above the margin-call LTV.
## How it is implemented
**File:** `app/strategies/protective_put.py`
Main properties:
- `hedge_units`: `portfolio.gold_value / spot_price`
- `strike`: `spot_price * strike_pct`
- `term_years`: `months / 12`
A put contract is priced with Black-Scholes inputs:
- current spot
- strike
- time to expiry
- risk-free rate
- volatility
- option type = `put`
The resulting `OptionContract` uses:
- `quantity = 1.0`
- `contract_size = hedge_units`
That means one model contract covers the full portfolio exposure in underlying units.
## Protective put payoff intuition
At expiry:
- if spot is above strike, the put expires worthless
- if spot is below strike, payoff rises linearly as `strike - spot`
Total gross payoff is:
```text
max(strike - spot, 0) * hedge_units
```
## Protective put trade-offs
Advantages:
- simple to explain
- clear downside floor
- strongest protection when strike is high
Costs:
- premium can be expensive, especially at-the-money and for longer tenor
- full notional protection may overspend relative to a client's risk budget
- upside is preserved, but cost drags returns
---
## Laddered put
## What it is
A laddered put splits the hedge across multiple put strikes instead of buying the full hedge at one strike.
Structure:
- multiple long put legs
- each leg covers a weighted fraction of the total hedge
- lower strikes usually reduce premium while preserving some tail protection
Examples currently used by the engine:
- `50/50` ATM + 95% OTM
- `33/33/33` ATM + 95% OTM + 90% OTM
## Why use it
A ladder can reduce hedge cost versus a full ATM protective put, while still providing meaningful protection as the underlying falls.
This is useful when:
- full-cost protection is too expensive
- some drawdown can be tolerated before the hedge fully engages
- the client wants a better cost/protection balance
## How it is implemented
**File:** `app/strategies/laddered_put.py`
A `LadderSpec` defines:
- `weights`
- `strike_pcts`
- `months`
Validation rules:
- number of weights must equal number of strikes
- weights must sum to `1.0`
Each leg is implemented by internally creating a `ProtectivePutStrategy`, then weighting its premium and payoff.
## Ladder payoff intuition
Each leg pays off independently:
```text
max(leg_strike - spot, 0) * hedge_units * weight
```
Total ladder payoff is the sum across legs.
Relative to a single-strike hedge:
- protection turns on in stages
- blended premium is lower when some legs are farther OTM
- downside support is smoother but less absolute near the first loss zone than a full ATM hedge
## Ladder trade-offs
Advantages:
- lower blended hedge cost
- more flexible cost/protection shaping
- better fit for cost-sensitive clients
Costs and limitations:
- weaker immediate protection than a fully ATM hedge
- more complex to explain to users
- floor value depends on weight distribution across strikes
---
## Cost calculations
## Protective put cost calculation
`ProtectivePutStrategy.calculate_cost()` returns:
- `premium_per_share`
- `total_cost`
- `cost_pct_of_portfolio`
- `term_months`
- `annualized_cost`
- `annualized_cost_pct`
### Formula summary
Let:
- `P` = option premium per underlying unit
- `U` = hedge units
- `T` = term in years
- `V` = portfolio value
Then:
```text
total_cost = P * U
cost_pct_of_portfolio = total_cost / V
annualized_cost = total_cost / T
annualized_cost_pct = annualized_cost / V
```
Because the model contract size equals the full hedge units, the total premium directly represents the whole-portfolio hedge cost.
## Laddered put cost calculation
`LadderedPutStrategy.calculate_cost()` computes weighted leg costs.
For each leg `i`:
- `weight_i`
- `premium_i`
- `hedge_units`
Leg cost:
```text
leg_cost_i = premium_i * hedge_units * weight_i
```
Blended totals:
```text
blended_cost = sum(leg_cost_i)
blended_premium_per_share = sum(premium_i * weight_i)
annualized_cost = blended_cost / term_years
cost_pct_of_portfolio = blended_cost / portfolio_value
annualized_cost_pct = annualized_cost / portfolio_value
```
## Why annualized cost matters
The engine compares strategies with different durations, especially in `LeaseStrategy`. Annualizing allows the system to compare short-dated and long-dated hedges on a common yearly basis.
---
## Protection calculations
## Margin-call threshold price
The project defines the collateral price that would trigger a margin call as:
```text
margin_call_price = loan_amount / (margin_call_ltv * gold_ounces)
```
This is a key reference point for all protection calculations.
## Protective put protection calculation
At the threshold price:
1. compute the put payoff
2. add that payoff to the stressed collateral value
3. recompute LTV on the hedged collateral
Formulas:
```text
payoff_at_threshold = max(strike - threshold_price, 0) * hedge_units
hedged_value_at_threshold = gold_value_at_threshold + payoff_at_threshold
hedged_ltv_at_threshold = loan_amount / hedged_value_at_threshold
```
The strategy is flagged as maintaining a margin buffer when:
```text
hedged_ltv_at_threshold < margin_call_ltv
```
## Laddered put protection calculation
For a ladder, threshold payoff is the weighted sum of all leg payoffs:
```text
weighted_payoff_i = max(strike_i - threshold_price, 0) * hedge_units * weight_i
payoff_at_threshold = sum(weighted_payoff_i)
hedged_value_at_threshold = gold_value_at_threshold + payoff_at_threshold
hedged_ltv_at_threshold = loan_amount / hedged_value_at_threshold
```
The ladder's implied floor value is approximated as the weighted strike coverage:
```text
portfolio_floor_value = sum(strike_i * hedge_units * weight_i)
```
---
## Scenario analysis methodology
## Scenario grid
The current scenario engine in `ProtectivePutStrategy` uses a fixed price-change grid:
```text
-60%, -50%, -40%, -30%, -20%, -10%, 0%, +10%, +20%, +30%, +40%, +50%
```
For each change:
```text
scenario_price = spot_price * (1 + change)
```
Negative or zero prices are ignored.
## Metrics produced per scenario
For each scenario, the strategy computes:
- scenario spot price
- unhedged gold value
- option payoff
- hedge cost
- net portfolio value after hedge cost
- unhedged LTV
- hedged LTV
- whether a margin call occurs without the hedge
- whether a margin call occurs with the hedge
### Protective put scenario formulas
Let `S` be scenario spot.
```text
gold_value = gold_ounces * S
option_payoff = max(strike - S, 0) * hedge_units
hedged_collateral = gold_value + option_payoff
net_portfolio_value = gold_value + option_payoff - hedge_cost
unhedged_ltv = loan_amount / gold_value
hedged_ltv = loan_amount / hedged_collateral
```
Margin-call flags:
```text
margin_call_without_hedge = unhedged_ltv >= margin_call_ltv
margin_call_with_hedge = hedged_ltv >= margin_call_ltv
```
### Laddered put scenario formulas
For ladders:
```text
option_payoff = sum(max(strike_i - S, 0) * hedge_units * weight_i)
hedged_collateral = gold_value + option_payoff
net_portfolio_value = gold_value + option_payoff - blended_cost
```
All other LTV and margin-call logic is the same.
## Interpretation methodology
Scenario analysis is used to answer four practical questions:
1. **Cost:** How much premium is paid upfront?
2. **Activation:** At what downside level does protection meaningfully start?
3. **Buffer:** Does the hedge keep LTV below the margin-call threshold under stress?
4. **Efficiency:** How much protection is obtained per dollar of annualized hedge cost?
This is why each strategy exposes both:
- a `calculate_protection()` summary around the threshold price
- a full `get_scenarios()` table across broad upside/downside moves
---
## Comparing protective puts vs laddered puts
| Dimension | Protective put | Laddered put |
|---|---|---|
| Structure | Single put strike | Multiple weighted put strikes |
| Simplicity | Highest | Moderate |
| Upfront cost | Usually higher | Usually lower |
| Near-threshold protection | Stronger if ATM-heavy | Depends on ladder weights |
| Tail downside protection | Strong | Strong, but blended |
| Customization | Limited | High |
| Best fit | conservative protection | balanced or cost-sensitive protection |
---
## Important limitations
- The strategy engine is currently research-oriented, not an execution engine
- Black-Scholes assumptions simplify real-world market behavior
- Transaction costs, slippage, taxes, liquidity, and early exercise effects are not modeled here
- The API payloads should be treated as analytical outputs, not trade recommendations
- For non-`GLD` symbols, the engine currently still uses research-style assumptions rather than a complete live instrument-specific calibration
## Future strategy extensions
Natural follow-ups for this subsystem:
- collars and financed hedges
- partial notional hedging
- dynamic re-hedging rules
- volatility surface-based pricing
- broker-native contract sizing and expiries
- user-configurable scenario grids