- 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
425 lines
10 KiB
Markdown
425 lines
10 KiB
Markdown
# 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
|