mirror of
https://github.com/Mihirkansara/nexus-quant-terminal.git
synced 2026-11-02 09:36:11 +00:00
Volatile instruments
- Add XAGUSD (SI=F, silver lease rate as r_f), BTCUSD and ETHUSD end to end:
pair metadata, live rates/OHLC/signals, CFTC positioning mappings
- Asset-class calendars: crypto annualised on 365 days, FX/metals on 252,
threaded through the signal engine
- Pair switch loads a typical ATM vol, rescales legs by moneyness, and
strategy templates use a ~1%-of-spot strike step
Vol Lab tab
- /api/vol/crypto/{BTC,ETH}: Deribit book summaries -> raw SVI fit per
expiry with Durrleman butterfly-arbitrage check, ATM / 25d RR / 25d BF
(Black-76 forward delta), DVOL, 30d constant-maturity IV, realised vol
and vol risk premium; cached 60s, 503 when Deribit is unreachable
- /api/vol/cone/{pair}: Burghardt-Lane realised-vol cone on the asset's
calendar
- UI: KPIs, cone with implied term-structure overlay, per-expiry smile
(market vs SVI), term-structure table with 'Use' to load T/sigma/RR/BF
into pricing and the Vol Smile tab
Fixes in the signal engine
- OU half-life divided by 252 twice; stationary sd used sqrt(252k) not sqrt(2k)
- Fallback realised vol returned decimals under *_pct keys
Tests: backend/tests (pytest) validate SVI recovery, exact 25d strikes,
cone calibration on both calendars and the endpoints with mocked feeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpiqUMAyo4RL5DdQHMWbwE
30 lines
1.7 KiB
Python
30 lines
1.7 KiB
Python
import numpy as np
|
|
from datetime import datetime, timezone, timedelta
|
|
from app.core.vol_analytics import svi_total_variance
|
|
NOW = datetime(2026, 9, 29, 5, 0, tzinfo=timezone.utc)
|
|
NOW_MS = NOW.timestamp() * 1000
|
|
MON = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]
|
|
def deribit_book(spot=83000.0, r=0.04, ccy="BTC", rng=None, noise=0.0):
|
|
"""Deribit-shaped book summaries from a known SVI surface (BTC-like skew)."""
|
|
rng = rng or np.random.default_rng(1)
|
|
rows, truth = [], {}
|
|
for days in [2, 9, 30, 58, 93, 184, 275]:
|
|
exp = (NOW + timedelta(days=days)).replace(hour=8, minute=0)
|
|
T = (exp - NOW).total_seconds() / (365*86400)
|
|
F = spot * np.exp(r * T)
|
|
atm = 0.38 + 0.04 * np.sqrt(T) # upward term structure
|
|
p = (atm**2*T*0.8, 0.12*np.sqrt(T), -0.25, 0.02, 0.15) # a,b,rho,m,sig
|
|
# rescale a so w(0)=atm^2 T
|
|
w0 = svi_total_variance(0, *p); p = (p[0] + atm**2*T - w0,) + p[1:]
|
|
truth[exp.strftime("%Y-%m-%d")] = (p, T, F)
|
|
step = 1000 if ccy == "BTC" else 50
|
|
for K in np.arange(round(F*0.55/step)*step, F*1.8, step * (1 if T > 0.1 else 1)):
|
|
k = np.log(K/F); iv = np.sqrt(svi_total_variance(k, *p)/T)
|
|
if abs(k) > 3.5*atm*np.sqrt(T) + 0.1: continue
|
|
for cp in "CP":
|
|
name = f"{ccy}-{exp.day}{MON[exp.month-1]}{exp.strftime('%y')}-{int(K)}-{cp}"
|
|
rows.append({"instrument_name": name, "mark_iv": round(100*iv*(1+noise*rng.standard_normal()), 2),
|
|
"underlying_price": F, "underlying_index": f"{ccy}-{exp.strftime('%d%b%y').upper()}",
|
|
"open_interest": float(rng.integers(0, 500)), "mark_price": 0.01})
|
|
return rows, truth
|