maxkru92's picture
Upload app.py with huggingface_hub
7508c21 verified
Raw
History Blame Contribute Delete
130 kB
"""
MK Quant Monitor — Institutional Trading Terminal v7.0
Volatility Vince Edition
Tabs: Markets Overview | SPX & VIX Analytics | Volatility Vince | MarketGuardian Pro | Crypto Ultra | Insider Trades | Settings
"""
import os, sys, json, importlib.util, pathlib, logging
from datetime import datetime, timedelta, date
from collections import defaultdict
from typing import Optional, Dict, Any, List, Tuple
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from matplotlib.patches import Rectangle, Wedge
from matplotlib.collections import PatchCollection
import requests
from scipy import stats as _scipy_stats
# -- CBOE Adapter --
CBOE_BASE_URL = "https://cdn.cboe.com/api/global/delayed_quotes/options"
CBOE_SYMBOLS = {"SPX":"SPX","VIX":"VIX","NDX":"NDX","RUT":"RUT","DJX":"DJX","SPY":"SPY","QQQ":"QQQ"}
_CBOE_HEADERS = {"User-Agent":"Mozilla/5.0","Accept":"application/json"}
def _cboe_fetch_chain(symbol):
sym = CBOE_SYMBOLS.get(symbol.upper(), symbol.upper())
url = f"{CBOE_BASE_URL}/{sym}.json"
try:
r = requests.get(url, headers=_CBOE_HEADERS, timeout=15)
r.raise_for_status()
data = r.json()
if "data" not in data: return None
return data
except: return None
def _cboe_parse_chain(raw_data):
if not raw_data or "data" not in raw_data:
return {"spot":0,"records":[],"calls_df":pd.DataFrame(),"puts_df":pd.DataFrame(),"expirations":[],"timestamp":""}
data = raw_data["data"]
spot = float(data.get("current_price", 0))
options = data.get("options", [])
ts = datetime.utcnow().isoformat()
if not options:
return {"spot":spot,"records":[],"calls_df":pd.DataFrame(),"puts_df":pd.DataFrame(),"expirations":[],"timestamp":ts}
records, calls_r, puts_r = [], [], []
for opt in options:
try:
rec = {
"strike": float(opt.get("strike",0)),
"expiry": str(opt.get("expiration","")),
"option_type": str(opt.get("option_type","")).upper(),
"iv": float(opt.get("iv",0) or 0),
"delta": float(opt.get("delta",0) or 0),
"gamma": float(opt.get("gamma",0) or 0),
"theta": float(opt.get("theta",0) or 0),
"vega": float(opt.get("vega",0) or 0),
"open_interest": int(opt.get("open_interest",0) or 0),
"bid": float(opt.get("bid",0) or 0),
"ask": float(opt.get("ask",0) or 0),
"last": float(opt.get("last_price",0) or 0),
"volume": int(opt.get("volume",0) or 0),
}
if rec["strike"] <= 0: continue
records.append(rec)
if rec["option_type"] == "C": calls_r.append(rec)
elif rec["option_type"] == "P": puts_r.append(rec)
except: continue
expirations = sorted(set(r["expiry"] for r in records if r["expiry"]))
return {"spot":spot,"records":records,"calls_df":pd.DataFrame(calls_r),"puts_df":pd.DataFrame(puts_r),"expirations":expirations,"timestamp":ts,"source":"cboe_live"}
def cboe_fetch_spx_chain():
raw = _cboe_fetch_chain("SPX")
return _cboe_parse_chain(raw) if raw else None
# -- GEX Calculator --
def _safe_float(v, default=0.0):
try:
if v is None: return default
f = float(v)
return f if np.isfinite(f) else default
except: return default
def _safe_int(v, default=0):
try:
if v is None: return default
return int(float(v))
except: return default
def compute_gex_plus(records, spot):
if not records or not spot or spot <= 0: return 0.0
spot = float(spot)
total, disagreements = 0.0, 0
for s in records:
net_oi = _safe_int(s.get("oi_call")) - _safe_int(s.get("oi_put"))
gamma = (_safe_float(s.get("gamma_call")) + _safe_float(s.get("gamma_put"))) / 2.0
total += net_oi * gamma * spot * spot / 100.0
iv_c = _safe_float(s.get("iv_call", 0.001))
iv_p = _safe_float(s.get("iv_put", 0.001))
if iv_p > iv_c * 1.02 and iv_c > 0: disagreements += 1
rate = disagreements / len(records) if records else 0.0
return total * max(0.0, 1.0 - 2.0 * rate)
def compute_vanna_exposure(records, spot):
if not records or not spot or spot <= 0: return 0.0
spot = float(spot)
total = 0.0
for s in records:
vanna_c = _safe_float(s.get("vanna_call"))
vanna_p = _safe_float(s.get("vanna_put"))
if vanna_c == 0 and vanna_p == 0:
gamma_c = _safe_float(s.get("gamma_call"))
gamma_p = _safe_float(s.get("gamma_put"))
iv_c = _safe_float(s.get("iv_call", 0.20))
iv_p = _safe_float(s.get("iv_put", 0.20))
strike = _safe_float(s.get("strike"))
if iv_c > 0 and strike > 0:
d1 = (np.log(spot/strike) + 0.5*iv_c**2) / iv_c
vanna_c = -gamma_c * d1 / iv_c
if iv_p > 0 and strike > 0:
d1 = (np.log(spot/strike) + 0.5*iv_p**2) / iv_p
vanna_p = -gamma_p * d1 / iv_p
oi_c = _safe_int(s.get("oi_call"))
oi_p = _safe_int(s.get("oi_put"))
total += vanna_c * oi_c * spot + vanna_p * oi_p * spot
return total
def find_zero_gamma(records, spot, max_range_pct=25.0, step=0.1):
if not records or not spot or spot <= 0: return spot
gex_at = compute_gex_plus(records, spot)
if gex_at == 0: return spot
sign_at = np.sign(gex_at)
for pct in np.arange(0.0, max_range_pct+step, step):
test = spot * (1+pct/100)
scaled = [{**s, "gamma_call":_safe_float(s.get("gamma_call"))*spot/test, "gamma_put":_safe_float(s.get("gamma_put"))*spot/test} for s in records]
if np.sign(compute_gex_plus(scaled, test)) != sign_at: return test
for pct in np.arange(0.0, -max_range_pct-step, -step):
test = spot * (1+pct/100)
scaled = [{**s, "gamma_call":_safe_float(s.get("gamma_call"))*spot/test, "gamma_put":_safe_float(s.get("gamma_put"))*spot/test} for s in records]
if np.sign(compute_gex_plus(scaled, test)) != sign_at: return test
return spot * 1.05
def compute_crash_profile(records, spot, range_pct=(-15.0, 10.0), step=0.25):
if not records or not spot or spot <= 0: return []
profile = []
for pct in np.arange(range_pct[0], range_pct[1]+step, step):
test = spot * (1+pct/100)
scaled = [{**s, "gamma_call":_safe_float(s.get("gamma_call"))*spot/test, "gamma_put":_safe_float(s.get("gamma_put"))*spot/test} for s in records]
profile.append({"spot_pct":round(float(pct),2),"spx":round(test,1),"gex_plus":compute_gex_plus(scaled,test)})
return profile
def compute_chain_greeks(chain_records, spot, r=0.05, dte=None):
if not chain_records: return pd.DataFrame()
rows = []
for rec in chain_records:
strike = float(rec.get("strike",0))
if strike <= 0: continue
T = dte/365.0 if dte else 30.0/365.0
iv_c = float(rec.get("iv_call", 0.20))
iv_p = float(rec.get("iv_put", 0.20))
oi_c = int(rec.get("oi_call", 0))
oi_p = int(rec.get("oi_put", 0))
if iv_c > 0 and T > 0:
d1 = (np.log(spot/strike) + (r+0.5*iv_c**2)*T) / (iv_c*np.sqrt(T))
d2 = d1 - iv_c*np.sqrt(T)
delta_c = float(_scipy_stats.norm.cdf(d1))
gamma_c = float(_scipy_stats.norm.pdf(d1) / (spot*iv_c*np.sqrt(T)))
theta_c = float((-(spot*_scipy_stats.norm.pdf(d1)*iv_c)/(2*np.sqrt(T)) - r*strike*np.exp(-r*T)*_scipy_stats.norm.cdf(d2)) / 365.0)
vega_c = float(spot * _scipy_stats.norm.pdf(d1) * np.sqrt(T) * 0.01)
else:
delta_c = gamma_c = theta_c = vega_c = 0.0
if iv_p > 0 and T > 0:
d1 = (np.log(spot/strike) + (r+0.5*iv_p**2)*T) / (iv_p*np.sqrt(T))
d2 = d1 - iv_p*np.sqrt(T)
delta_p = float(_scipy_stats.norm.cdf(d1) - 1.0)
gamma_p = float(_scipy_stats.norm.pdf(d1) / (spot*iv_p*np.sqrt(T)))
theta_p = float((-(spot*_scipy_stats.norm.pdf(d1)*iv_p)/(2*np.sqrt(T)) + r*strike*np.exp(-r*T)*_scipy_stats.norm.cdf(-d2)) / 365.0)
vega_p = float(spot * _scipy_stats.norm.pdf(d1) * np.sqrt(T) * 0.01)
else:
delta_p = gamma_p = theta_p = vega_p = 0.0
rows.append({"strike":strike,"T":T,"dte":int(T*365),"iv_call":iv_c,"iv_put":iv_p,"delta_call":delta_c,"delta_put":delta_p,"gamma_call":gamma_c,"gamma_put":gamma_p,"theta_call":theta_c,"theta_put":theta_p,"vega_call":vega_c,"vega_put":vega_p,"oi_call":oi_c,"oi_put":oi_p})
return pd.DataFrame(rows).sort_values("strike").reset_index(drop=True)
def realized_vol(prices, window=20, annualize=True):
if prices is None or len(prices) < 2: return pd.Series(dtype=float)
lr = np.log(prices / prices.shift(1))
vol = lr.rolling(window=window).std()
if annualize: vol = vol * np.sqrt(252)
return vol
def iv_term_structure(chain_records, spot):
if not chain_records: return pd.DataFrame()
by_exp = {}
for rec in chain_records:
exp = rec.get("expiry","unknown")
by_exp.setdefault(exp, []).append(rec)
rows = []
for exp, recs in sorted(by_exp.items()):
atm = sorted(recs, key=lambda r: abs(r.get("strike",0)-spot))[:4]
iv_c = np.mean([r.get("iv_call",0) for r in atm if r.get("iv_call",0)>0]) if any(r.get("iv_call",0)>0 for r in atm) else 0
iv_p = np.mean([r.get("iv_put",0) for r in atm if r.get("iv_put",0)>0]) if any(r.get("iv_put",0)>0 for r in atm) else 0
try:
dte = max(0, (pd.to_datetime(exp) - pd.Timestamp.now()).days)
except: dte = 0
rows.append({"expiry":exp,"dte":dte,"atm_iv_call":iv_c,"atm_iv_put":iv_p,"avg_iv":(iv_c+iv_p)/2.0,"skew":iv_p-iv_c})
return pd.DataFrame(rows).sort_values("dte").reset_index(drop=True)
def compute_iv_skew(chain_records, spot):
if not chain_records: return {"raw_skew":0,"atm_skew":0,"wing_skew":0,"skew_slope":0}
data = []
for rec in chain_records:
strike = rec.get("strike",0)
if strike <= 0: continue
iv_c = rec.get("iv_call",0)
iv_p = rec.get("iv_put",0)
moneyness = (strike/spot-1)*100
data.append({"strike":strike,"moneyness":moneyness,"iv_call":iv_c,"iv_put":iv_p,"raw_skew":iv_p-iv_c})
if not data: return {"raw_skew":0,"atm_skew":0,"wing_skew":0,"skew_slope":0}
df = pd.DataFrame(data)
raw_skew = df["raw_skew"].mean()
atm_skew = df[df["moneyness"].abs()<1.0]["raw_skew"].mean() if len(df[df["moneyness"].abs()<1.0])>0 else raw_skew
otm_puts = df[df["moneyness"]<-2]
otm_calls = df[df["moneyness"]>2]
wing_skew = (otm_puts["iv_put"].mean() - otm_calls["iv_call"].mean()) if len(otm_puts)>0 and len(otm_calls)>0 else 0
return {"raw_skew":raw_skew,"atm_skew":atm_skew,"wing_skew":wing_skew,"skew_slope":0}
def vol_regime(current_iv, rv_20d, rv_60d, iv_rank, iv_pct, term_slope):
if iv_rank > 75: regime, color = "High Vol", "red"
elif iv_rank > 50: regime, color = "Above Normal", "orange"
elif iv_rank > 25: regime, color = "Below Normal", "yellow"
else: regime, color = "Low Vol", "green"
vrp = current_iv - rv_20d
if term_slope > 2: term_sig = "Strong Contango"
elif term_slope > 0.5: term_sig = "Contango"
elif term_slope > -0.5: term_sig = "Flat"
elif term_slope > -2: term_sig = "Backwardation"
else: term_sig = "Strong Backwardation"
signals = []
if iv_rank > 80: signals.append("IV Elevated - Consider selling vol")
elif iv_rank < 20: signals.append("IV Compressed - Consider buying vol")
if vrp > 5: signals.append("High Vol Risk Premium")
if "Backwardation" in term_sig: signals.append("Term Structure Inverted - Stress")
return {"regime":regime,"regime_color":color,"iv_rank":iv_rank,"vrp":vrp,"term_signal":term_sig,"signals":signals}
def _compute_heatmap_inline(strikes_list, spot, spot_range=(10,-15), iv_range=(-10,40), n_spot=40, n_iv=50):
spot_steps = np.linspace(spot_range[0], spot_range[1], n_spot)
iv_steps = np.linspace(iv_range[0], iv_range[1], n_iv)
grid = np.zeros((n_spot, n_iv))
for i, sp in enumerate(spot_steps):
test_spot = spot * (1 + sp/100)
scaled = [{**s, "gamma_call": s.get("gamma_call",0)*spot/test_spot, "gamma_put": s.get("gamma_put",0)*spot/test_spot} for s in strikes_list]
for j, iv_shock in enumerate(iv_steps):
iv_adj = [{**s, "iv_call": max(0.001, s.get("iv_call",0)+iv_shock/100), "iv_put": max(0.001, s.get("iv_put",0)+iv_shock/100)} for s in scaled]
grid[i,j] = compute_gex_plus(iv_adj, test_spot)
max_abs = np.abs(grid).max()
if max_abs > 0: grid = grid / max_abs
return {"spot_range": list(spot_range), "iv_range": list(iv_range), "values": grid.tolist()}
def _compute_bl_forecast_inline(strikes_list, spot, dte=30, target_dte_1d=1, target_dte_1w=5):
from scipy import stats
atm = min(strikes_list, key=lambda s: abs(s.get("strike",0) - spot)) if strikes_list else {"iv_call":0.18,"iv_put":0.19}
atm_iv = (atm.get("iv_call",0.18) + atm.get("iv_put",0.19)) / 2
down = min(strikes_list, key=lambda s: abs(s.get("strike",0) - spot*0.95)) if strikes_list else {"iv_put":0.20}
up = min(strikes_list, key=lambda s: abs(s.get("strike",0) - spot*1.05)) if strikes_list else {"iv_call":0.16}
skew = (down.get("iv_put",0.20) - up.get("iv_call",0.16)) * 10
def make_fc(target):
sig = atm_iv * np.sqrt(target/252)
fwd = spot
z_map = {"p5":-1.645,"p25":-0.674,"p50":0.0,"p75":0.674,"p95":1.645}
def cf(z): return z + (skew/6)*(z**2-1)
pctiles = {k: round(fwd*np.exp(cf(z)*sig-0.5*sig**2),1) for k,z in z_map.items()}
levels = sorted({round(spot), round(spot*0.95), round(spot*1.05)})
table = []
for lvl in levels:
if sig > 0:
z = (np.log(lvl/fwd)+0.5*sig**2)/sig
p_below = round(float(stats.norm.cdf(z))*100,1)
else: p_below = 100.0 if lvl >= spot else 0.0
table.append({"level":lvl,"p_below":p_below,"p_above":round(100-p_below,1)})
return {"sigma_pts":round(sig*spot),"sigma_pct":round(sig*100,2),"forward":round(fwd,1),"p5":pctiles["p5"],"p25":pctiles["p25"],"median":pctiles["p50"],"p75":pctiles["p75"],"p95":pctiles["p95"],"range_90":[pctiles["p5"],pctiles["p95"]],"table":table}
return {"one_day": make_fc(target_dte_1d), "one_week": make_fc(target_dte_1w)}
# =============================================================================
# VOLATILITY VINTAGE INTELLIGENCE FUNCTIONS
# =============================================================================
def compute_vvix_percentile(vvix_series, lookback=252):
"""Compute VVIX percentile/rank over lookback window."""
if vvix_series is None or len(vvix_series) < 20:
return None, None, None
current = vvix_series.iloc[-1]
window = vvix_series.tail(min(lookback, len(vvix_series)))
percentile = (window < current).sum() / len(window) * 100
rank = percentile
return float(current), float(percentile), float(rank)
def compute_vix_vvix_ratio(vix_series, vvix_series):
"""VIX/VVIX ratio — mean reversion signal."""
if vix_series is None or vvix_series is None:
return None, None, None
min_len = min(len(vix_series), len(vvix_series))
if min_len < 2:
return None, None, None
vix = vix_series.tail(min_len)
vvix = vvix_series.tail(min_len)
ratio = vix.values / vvix.values
current_ratio = ratio[-1]
# Z-score of ratio over 60 days
if len(ratio) >= 60:
window = ratio[-60:]
z = (current_ratio - np.mean(window)) / (np.std(window) + 1e-9)
else:
z = 0.0
return float(current_ratio), float(z), ratio
def compute_vol_regime_detection(vix_series, vvix_series, spx_series):
"""
HMM-inspired regime detection using VIX, VVIX, and SPX returns.
Returns current regime, probabilities, and transition matrix.
"""
regimes = ["Low Vol", "Normal", "Elevated", "Crisis"]
regime_colors = {"Low Vol": "#00ff88", "Normal": "#00d4ff", "Elevated": "#ffaa00", "Crisis": "#ff4444"}
if vix_series is None or len(vix_series) < 60:
return {"regime": "Unknown", "probabilities": {}, "transition_matrix": {},
"regime_color": "#8899aa", "history": []}
vix = vix_series.tail(252).values
current_vix = vix[-1]
# Compute SPX returns if available
if spx_series is not None and len(spx_series) > 1:
spx_ret = spx_series.pct_change().tail(252).dropna().values
vol_spx = np.std(spx_ret[-20:]) * np.sqrt(252) * 100 if len(spx_ret) >= 20 else 15.0
else:
vol_spx = 15.0
# VVIX level
current_vvix = vvix_series.iloc[-1] if vvix_series is not None and len(vvix_series) > 0 else 85.0
# Regime thresholds based on VIX percentiles
vix_p25 = np.percentile(vix, 25)
vix_p50 = np.percentile(vix, 50)
vix_p75 = np.percentile(vix, 75)
# Compute regime probabilities using Gaussian likelihoods
regime_params = {
"Low Vol": {"vix_mean": vix_p25 * 0.7, "vix_std": 3.0, "vvix_mean": 70, "vvix_std": 8},
"Normal": {"vix_mean": vix_p50, "vix_std": 4.0, "vvix_mean": 82, "vvix_std": 10},
"Elevated": {"vix_mean": vix_p75, "vix_std": 5.0, "vvix_mean": 95, "vvix_std": 12},
"Crisis": {"vix_mean": vix_p75 * 1.5, "vix_std": 8.0, "vvix_mean": 115, "vvix_std": 18},
}
log_probs = {}
for reg, params in regime_params.items():
lp_vix = -0.5 * ((current_vix - params["vix_mean"]) / params["vix_std"])**2
lp_vvix = -0.5 * ((current_vvix - params["vvix_mean"]) / params["vvix_std"])**2
log_probs[reg] = lp_vix + lp_vvix
# Convert to probabilities
max_lp = max(log_probs.values())
probs = {r: np.exp(lp - max_lp) for r, lp in log_probs.items()}
total = sum(probs.values())
probs = {r: p/total for r, p in probs.items()}
current_regime = max(probs, key=probs.get)
# Build transition matrix from historical data
trans_matrix = compute_regime_transition_matrix(vix, vvix_series)
# Regime history (last 30 days)
hist = []
for i in range(max(0, len(vix)-30), len(vix)):
day_probs = {}
for reg, params in regime_params.items():
lp = -0.5 * ((vix[i] - params["vix_mean"]) / params["vix_std"])**2
day_probs[reg] = lp
max_lp = max(day_probs.values())
day_probs = {r: np.exp(lp - max_lp) for r, lp in day_probs.items()}
t = sum(day_probs.values())
day_probs = {r: round(p/t*100, 1) for r, p in day_probs.items()}
hist.append({"idx": i, "vix": vix[i], "regime": max(day_probs, key=day_probs.get), **day_probs})
return {
"regime": current_regime,
"probabilities": {r: round(p*100, 1) for r, p in probs.items()},
"transition_matrix": trans_matrix,
"regime_color": regime_colors.get(current_regime, "#8899aa"),
"history": hist,
"vix_percentile": float((vix < current_vix).sum() / len(vix) * 100),
}
def compute_regime_transition_matrix(vix_values, vvix_series):
"""Compute empirical regime transition matrix from VIX history."""
if vix_values is None or len(vix_values) < 60:
return {}
vix = vix_values
p25, p50, p75 = np.percentile(vix, 25), np.percentile(vix, 50), np.percentile(vix, 75)
labels = ["Low Vol", "Normal", "Elevated", "Crisis"]
def classify(v):
if v < p25: return 0
elif v < p50: return 1
elif v < p75: return 2
else: return 3
states = [classify(v) for v in vix]
counts = np.zeros((4, 4))
for i in range(len(states)-1):
counts[states[i]][states[i+1]] += 1
matrix = {}
for i, from_reg in enumerate(labels):
matrix[from_reg] = {}
row_total = counts[i].sum()
for j, to_reg in enumerate(labels):
matrix[from_reg][to_reg] = round(float(counts[i][j] / row_total * 100), 1) if row_total > 0 else 0.0
return matrix
def compute_vol_risk_premium(vix_series, spx_series, windows=[20, 60, 120]):
"""
Vol Risk Premium = IV (VIX) - RV (realized vol of SPX).
Returns VRP for multiple tenors with Z-scores.
"""
if vix_series is None or spx_series is None:
return {}
results = {}
vix = vix_series.dropna()
spx = spx_series.dropna()
for w in windows:
label = f"{w}d"
if len(spx) < w + 5:
continue
rv = realized_vol(spx, window=w)
if rv is None or len(rv) < 2:
continue
# Align VIX and RV
common_idx = vix.index.intersection(rv.index)
if len(common_idx) < 20:
continue
vix_aligned = vix.loc[common_idx]
rv_aligned = rv.loc[common_idx]
vrp = vix_aligned - rv_aligned
current_vrp = float(vrp.iloc[-1])
# Z-score over available history
vrp_mean = vrp.rolling(min(60, len(vrp)), min_periods=10).mean().iloc[-1]
vrp_std = vrp.rolling(min(60, len(vrp)), min_periods=10).std().iloc[-1]
vrp_z = (current_vrp - vrp_mean) / (vrp_std + 1e-9) if vrp_std > 0 else 0.0
results[label] = {
"vix": float(vix_aligned.iloc[-1]),
"rv": float(rv_aligned.iloc[-1]),
"vrp": current_vrp,
"vrp_zscore": float(vrp_z),
"vrp_mean": float(vrp_mean),
"vrp_history": vrp,
}
return results
def compute_vix_fear_gauge(vix_val, vvix_val, vix_vvix_ratio, vrp_val):
"""
Normalized fear index combining VIX, VVIX, VIX/VVIX ratio, VRP.
0-100 scale with color zones.
"""
components = {}
# VIX component (0-40 points) — VIX typically 10-80
if vix_val is not None:
vix_score = min(40, max(0, (vix_val - 10) / 70 * 40))
else:
vix_score = 20
components["VIX"] = vix_score
# VVIX component (0-25 points) — VVIX typically 60-150
if vvix_val is not None:
vvix_score = min(25, max(0, (vvix_val - 60) / 90 * 25))
else:
vvix_score = 12.5
components["VVIX"] = vvix_score
# VIX/VVIX ratio component (0-15 points) — ratio typically 0.08-0.25
if vix_vvix_ratio is not None:
ratio_score = min(15, max(0, (vix_vvix_ratio - 0.08) / 0.17 * 15))
else:
ratio_score = 7.5
components["VIX/VVIX"] = ratio_score
# VRP component (0-20 points) — VRP typically -5 to +15
if vrp_val is not None:
vrp_score = min(20, max(0, (vrp_val + 5) / 20 * 20))
else:
vrp_score = 10
components["VRP"] = vrp_score
total = sum(components.values())
if total < 25:
zone = "Complacent"
color = "#00ff88"
elif total < 45:
zone = "Low Fear"
color = "#88cc00"
elif total < 60:
zone = "Moderate"
color = "#ffaa00"
elif total < 80:
zone = "Elevated"
color = "#ff6600"
else:
zone = "Extreme Fear"
color = "#ff4444"
return {
"value": round(total, 1),
"zone": zone,
"color": color,
"components": components,
}
def compute_max_pain(records, spot):
"""Calculate max pain strike — the strike where option writers have minimum payout."""
if not records: return None, {}
strikes = sorted(set(r.get("strike", 0) for r in records if r.get("strike", 0) > 0))
if not strikes: return None, {}
pain = {}
for strike in strikes:
total_pain = 0
for r in records:
k = r.get("strike", 0)
if k <= 0: continue
oi_c = _safe_int(r.get("oi_call"))
oi_p = _safe_int(r.get("oi_put"))
# Call pain: max(0, strike - k) * OI
total_pain += max(0, strike - k) * oi_c
# Put pain: max(0, k - strike) * OI
total_pain += max(0, k - strike) * oi_p
pain[strike] = total_pain
if not pain: return None, {}
max_pain_strike = min(pain, key=pain.get)
return max_pain_strike, pain
def compute_gamma_walls(records, spot, top_n=5):
"""Identify strikes with highest absolute GEX (gamma walls)."""
if not records: return []
walls = []
for r in records:
strike = r.get("strike", 0)
if strike <= 0: continue
gex_c = _safe_float(r.get("gamma_call")) * _safe_int(r.get("oi_call")) * spot**2 / 100
gex_p = -_safe_float(r.get("gamma_put")) * _safe_int(r.get("oi_put")) * spot**2 / 100
net_gex = gex_c + gex_p
walls.append({"strike": strike, "gex": net_gex, "abs_gex": abs(net_gex)})
walls.sort(key=lambda x: x["abs_gex"], reverse=True)
return walls[:top_n]
def compute_delta_neutral_strike(records, spot):
"""Find the strike closest to delta-neutral (call delta + put delta ~ 0)."""
if not records: return None
best_strike = None
best_diff = float('inf')
for r in records:
strike = r.get("strike", 0)
if strike <= 0: continue
dc = _safe_float(r.get("delta_call"))
dp = _safe_float(r.get("delta_put"))
diff = abs(dc + dp)
if diff < best_diff:
best_diff = diff
best_strike = strike
return best_strike
def compute_term_structure_metrics(vix_terms_dict):
"""Compute contango/backwardation, slope, and curvature of VIX term structure."""
if not vix_terms_dict or len(vix_terms_dict) < 2:
return {"regime": "Unknown", "spread": 0, "slope": 0, "curvature": 0, "roll_yield": 0}
names = list(vix_terms_dict.keys())
vals = list(vix_terms_dict.values())
spread = vals[1] - vals[0] if len(vals) >= 2 else 0
slope = spread / max(len(names)-1, 1)
# Curvature (second derivative approximation)
if len(vals) >= 3:
curvature = vals[2] - 2*vals[1] + vals[0]
else:
curvature = 0
# Roll yield: annualized return from rolling front-month to spot
if vals[0] > 0:
roll_yield = (vals[0] - vals[1]) / vals[0] * 12 * 100 # Annualized pct
else:
roll_yield = 0
if spread > 3: regime = "Strong Contango"
elif spread > 0.5: regime = "Contango"
elif spread > -0.5: regime = "Flat"
elif spread > -3: regime = "Backwardation"
else: regime = "Strong Backwardation"
return {
"regime": regime,
"spread": round(spread, 2),
"slope": round(slope, 3),
"curvature": round(curvature, 3),
"roll_yield": round(roll_yield, 2),
}
def compute_cross_asset_vol(vix_data_dict):
"""Compute cross-asset vol correlation and dispersion."""
# Build DataFrame of vol indices
dfs = {}
for name, series in vix_data_dict.items():
if series is not None and len(series) > 20:
s = series.dropna()
if len(s) > 20:
dfs[name] = s
if len(dfs) < 2:
return {"correlation": pd.DataFrame(), "dispersion": {}, "current": {}}
df = pd.DataFrame(dfs)
# Compute returns
returns = df.pct_change().dropna()
corr = returns.corr() if len(returns) > 5 else pd.DataFrame()
# Current dispersion (cross-sectional vol of vols)
current_vals = {name: float(series.iloc[-1]) for name, series in dfs.items()}
if len(current_vals) >= 2:
vol_of_vols = float(np.std(list(current_vals.values())) / np.mean(list(current_vals.values())) * 100)
else:
vol_of_vols = 0.0
return {
"correlation": corr,
"dispersion": {"vol_of_vols": vol_of_vols},
"current": current_vals,
}
# =============================================================================
# INSTITUTIONAL MATPLOTLIB STYLE
# =============================================================================
plt.rcParams.update({
'figure.facecolor': '#0a0e1a',
'axes.facecolor': '#1a2332',
'axes.edgecolor': '#2a3a5a',
'axes.labelcolor': '#8899aa',
'text.color': '#c8d6e5',
'xtick.color': '#8899aa',
'ytick.color': '#8899aa',
'grid.color': '#1e2d4a',
'grid.alpha': 0.4,
'legend.facecolor': '#111a2a',
'legend.edgecolor': '#2a3a5a',
'legend.labelcolor': '#c8d6e5',
'font.family': 'monospace',
'font.size': 9,
'axes.titlesize': 11,
'axes.titleweight': 'bold',
})
st.set_page_config(page_title="MK Quant Monitor", layout="wide", initial_sidebar_state="expanded")
st.markdown("""<style>
:root{--bg-main:#0a0e1a;--bg-panel:#111a2a;--bg-card:#1a2332;--border:#2a3a5a;--text-primary:#c8d6e5;--text-secondary:#8899aa;--cyan:#00d4ff;--green:#00ff88;--red:#ff4444;--yellow:#ffaa00;--orange:#ff6600}
body{background-color:var(--bg-main)!important;color:var(--text-primary)!important}
.reportview-container .main .block-container{padding-top:0.5rem;max-width:1600px}
.stButton>button{background-color:var(--bg-card);color:var(--text-primary);border:1px solid var(--border);border-radius:4px}
.stButton>button:hover{background-color:var(--bg-panel);border-color:var(--cyan)}
div[data-testid="stMetric"]{background:var(--bg-card);border:1px solid var(--border);border-radius:4px;padding:10px 14px}
div[data-testid="stMetric"] label{color:var(--text-secondary)!important;font-size:0.7rem!important;text-transform:uppercase;letter-spacing:0.08em;font-family:sans-serif!important}
div[data-testid="stMetric"] div[data-testid="stMetricValue"]{color:var(--text-primary)!important;font-size:1.3rem!important;font-weight:600}
div[data-testid="stMetric"] div[data-testid="stMetricDelta"]{font-size:0.8rem!important}
.stTabs [data-baseweb="tab-list"]{gap:2px;border-bottom:1px solid var(--border)}
.stTabs [data-baseweb="tab"]{background:var(--bg-card);border:1px solid var(--border);border-bottom:none;border-radius:4px 4px 0 0;color:var(--text-secondary);padding:8px 20px;font-weight:500;font-family:sans-serif;font-size:0.85rem;letter-spacing:0.02em}
.stTabs [aria-selected="true"]{background:var(--bg-panel)!important;color:var(--cyan)!important;border-color:var(--cyan)!important;border-bottom:none!important}
footer{visibility:hidden}
.wm{position:fixed;bottom:6px;right:10px;font-size:0.6rem;font-style:italic;color:#3a4a5a;opacity:0.7;z-index:999}
section[data-testid="stSidebar"]{background:var(--bg-panel)!important}
div[data-testid="stDataFrame"]{border:1px solid var(--border)!important}
.stExpander{border:1px solid var(--border)!important;border-radius:4px!important}
</style><div class="wm">Krupp Capital</div>""", unsafe_allow_html=True)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("mk_quant")
DEMO_MODE = os.environ.get("DEMO_MODE","0")=="1"
HEADERS = {"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36","Accept":"application/json"}
# =============================================================================
# DATA FETCHERS
# =============================================================================
@st.cache_data(ttl=60)
def fetch_yahoo():
import requests
out = {}
symbols = {
"SPX":"^SPX","NDX":"^NDX","DJX":"^DJX","RUT":"^RUT",
"GC1!":"GC=F","CL1!":"CL=F","FDAX1!":"FDAX.F","ES1!":"ES=F","NQ1!":"NQ=F",
"ESTX50":"^STOXX50E","NIKKEI":"^N225","DAX":"^GDAXI",
"VIX":"^VIX","VVIX":"^VVIX","VXD":"^VXD","VXN":"^VXN","RVX":"^RVX",
"OVX":"^OVX","GVZ":"^GVZ","VXEEM":"^VXEEM",
"VSTOXX":"V2TX.DE",
"VDAX":"V1X.DE",
"BVIX":"BVIX",
"EVIV":"EVIV",
"BTC":"BTC-USD","ETH":"ETH-USD","SOL":"SOL-USD",
"SPY":"SPY","QQQ":"QQQ","TLT":"TLT","HYG":"HYG","GLD":"GLD","USO":"USO",
}
for name, sym in symbols.items():
try:
r = requests.get(f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}",
params={"range":"6mo","interval":"1d"}, headers=HEADERS, timeout=10)
if r.status_code == 200:
result = r.json().get("chart",{}).get("result",[{}])[0]
ts, closes = result.get("timestamp",[]), result.get("indicators",{}).get("quote",[{}])[0].get("close",[])
if ts and closes:
out[name] = pd.DataFrame({"Close":closes}, index=pd.to_datetime(ts,unit="s")).dropna()
except Exception as e:
logger.debug(f"Yahoo {sym}: {e}")
try:
import yfinance as yf
for name, sym in symbols.items():
if name not in out:
try:
hist = yf.Ticker(sym).history(period="6mo")
if hist is not None and len(hist) > 0:
out[name] = hist[["Close"]].dropna()
except: pass
except ImportError: pass
return out
@st.cache_data(ttl=120)
def fetch_cboe_spx_chain():
result = cboe_fetch_spx_chain()
if result is not None:
spot = result["spot"]
records = result["records"]
expirations = result["expirations"]
normalized = []
for r in records:
normalized.append({
"strike": r["strike"], "expiry": r["expiry"],
"oi_call": r["open_interest"] if r["option_type"] == "C" else 0,
"oi_put": r["open_interest"] if r["option_type"] == "P" else 0,
"iv_call": r["iv"] if r["option_type"] == "C" else 0,
"iv_put": r["iv"] if r["option_type"] == "P" else 0,
"delta_call": r["delta"] if r["option_type"] == "C" else 0,
"delta_put": r["delta"] if r["option_type"] == "P" else 0,
"gamma_call": r["gamma"] if r["option_type"] == "C" else 0,
"gamma_put": r["gamma"] if r["option_type"] == "P" else 0,
"theta_call": r["theta"] if r["option_type"] == "C" else 0,
"theta_put": r["theta"] if r["option_type"] == "P" else 0,
"vega_call": r["vega"] if r["option_type"] == "C" else 0,
"vega_put": r["vega"] if r["option_type"] == "P" else 0,
"vanna_call": r.get("vanna", 0) if r["option_type"] == "C" else 0,
"vanna_put": r.get("vanna", 0) if r["option_type"] == "P" else 0,
"bid_call": r["bid"] if r["option_type"] == "C" else 0,
"bid_put": r["bid"] if r["option_type"] == "P" else 0,
"ask_call": r["ask"] if r["option_type"] == "C" else 0,
"ask_put": r["ask"] if r["option_type"] == "P" else 0,
})
merged = {}
for r in normalized:
k = r["strike"]
if k not in merged:
merged[k] = r.copy()
else:
for key in ["oi_call","oi_put","iv_call","iv_put","delta_call","delta_put",
"gamma_call","gamma_put","theta_call","theta_put",
"vega_call","vega_put","vanna_call","vanna_put",
"bid_call","bid_put","ask_call","ask_put"]:
if r[key] != 0:
merged[k][key] = r[key]
return {"spot": spot, "records": list(merged.values()), "expirations": expirations,
"source": "cboe_live", "timestamp": datetime.utcnow().isoformat()}
return None
@st.cache_data(ttl=60)
def fetch_deribit():
import requests
base = "https://www.deribit.com/api/v2/public"
result = {"perpetuals":{}, "options":{}, "orderbooks":{}, "trades":{}}
for inst in ["BTC-PERPETUAL","ETH-PERPETUAL"]:
try:
d = requests.get(f"{base}/ticker?instrument_name={inst}", timeout=10).json().get("result",{})
if d:
sym = inst.split("-")[0].lower()
result["perpetuals"][sym] = {"last":d.get("last_price"),"mark":d.get("mark_price"),"index":d.get("index_price"),"high":d["stats"].get("high"),"low":d["stats"].get("low"),"change_pct":d["stats"].get("price_change"),"volume":d["stats"].get("volume"),"volume_usd":d["stats"].get("volume_usd"),"oi":d.get("open_interest"),"funding_8h":d.get("funding_8h"),"best_bid":d.get("best_bid_price"),"best_ask":d.get("best_ask_price"),"best_bid_amt":d.get("best_bid_amount"),"best_ask_amt":d.get("best_ask_amount")}
except Exception as e:
logger.debug(f"Deribit {inst}: {e}")
for cur in ["BTC","ETH"]:
try:
insts = requests.get(f"{base}/get_instruments?currency={cur}&kind=option&expired=false", timeout=15).json().get("result",[])
result["options"][cur.lower()] = insts
except Exception as e:
logger.debug(f"Deribit options {cur}: {e}")
for inst in ["BTC-PERPETUAL","ETH-PERPETUAL"]:
try:
trades = requests.get(f"{base}/get_last_trades_by_instrument?instrument_name={inst}&count=20", timeout=10).json().get("result",{}).get("trades",[])
result["trades"][inst.split("-")[0].lower()] = trades
except Exception as e:
logger.debug(f"Deribit trades {inst}: {e}")
for inst in ["BTC-PERPETUAL","ETH-PERPETUAL"]:
try:
ob = requests.get(f"{base}/get_order_book?instrument_name={inst}&depth=10", timeout=10).json().get("result",{})
result["orderbooks"][inst.split("-")[0].lower()] = {"bids":ob.get("bids",[])[:10],"asks":ob.get("asks",[])[:10],"mark_price":ob.get("mark_price"),"index_price":ob.get("index_price")}
except Exception as e:
logger.debug(f"Deribit ob {inst}: {e}")
return result
@st.cache_data(ttl=60)
def fetch_coingecko():
import requests
try:
r = requests.get("https://api.coingecko.com/api/v3/coins/markets",
params={"vs_currency":"usd","order":"market_cap_desc","per_page":50,"page":"1","sparkline":"true","price_change_percentage":"1h,24h,7d"}, timeout=10)
if r.status_code == 200:
out = {}
for c in r.json():
sym = c["symbol"].upper()
out[sym] = {"name":c["name"],"price":c["current_price"],"mkt_cap":c["market_cap"],"vol_24h":c["total_volume"],"chg_1h":c.get("price_change_percentage_1h_in_currency"),"chg_24h":c.get("price_change_percentage_24h"),"chg_7d":c.get("price_change_percentage_7d_in_currency"),"high_24h":c.get("high_24h"),"low_24h":c.get("low_24h"),"sparkline":c.get("sparkline_in_7d",{}).get("price",[]),"rank":c.get("market_cap_rank"),"ath":c.get("ath"),"ath_change":c.get("ath_change_percentage")}
return out
except Exception as e:
logger.debug(f"CoinGecko: {e}")
return {}
@st.cache_data(ttl=60)
def fetch_fear_greed():
import requests
try:
r = requests.get("https://api.alternative.me/fng/?limit=7", timeout=10)
data = r.json()
if "data" in data and len(data["data"]) > 0:
latest = data["data"][0]
return {"value":int(latest.get("value",0)),"label":latest.get("value_classification","Unknown"),"history":[{"date":datetime.fromtimestamp(int(d["timestamp"])).strftime("%m-%d"),"value":int(d["value"])} for d in data["data"]]}
except Exception as e:
logger.debug(f"FearGreed: {e}")
return None
@st.cache_data(ttl=300)
def fetch_congress():
import requests
BASE = "https://congressinfor-production.up.railway.app"
result = {"trades":[],"health":{}}
try:
h = requests.get(f"{BASE}/health", timeout=10).json()
result["health"] = h
except: pass
try:
r = requests.get(f"{BASE}/trades/recent", params={"limit":100,"days":30}, timeout=10)
data = r.json()
result["trades"] = data.get("trades", [])
except Exception as e:
logger.debug(f"Congress: {e}")
return result
@st.cache_data(ttl=300)
def fetch_fred(series_id, limit=100):
import requests
key = os.environ.get("FRED_API_KEY","")
if not key: return None
try:
r = requests.get("https://api.stlouisfed.org/fred/series/observations",
params={"series_id":series_id,"api_key":key,"file_type":"json","sort_order":"desc","limit":limit}, timeout=10)
if r.status_code == 200:
obs = r.json().get("observations",[])
if obs:
df = pd.DataFrame(obs)
df["date"] = pd.to_datetime(df["date"])
df["value"] = pd.to_numeric(df["value"],errors="coerce")
return df.dropna(subset=["value"]).set_index("date")["value"]
except: pass
return None
# =============================================================================
# HELPERS
# =============================================================================
def _s(df):
if df is None: return None
if isinstance(df,tuple):
for x in df:
if hasattr(x,"columns"): df=x; break
if hasattr(df,"columns"):
try:
s = df["Close"] if "Close" in df.columns else df.iloc[:,0]
return pd.to_numeric(s,errors="coerce").dropna()
except: return None
return None
def fmt_pct(v):
if v is None: return "N/A"
sign = "+" if v >= 0 else ""
return f"{sign}{v:.2f}%"
def fmt_price(v):
if v is None: return "N/A"
if abs(v) >= 1000: return f"${v:,.2f}"
if abs(v) >= 1: return f"${v:,.2f}"
return f"${v:.4f}"
def sign_prefix(v):
if v is None: return ""
return "+" if v >= 0 else ""
# =============================================================================
# ASSET -> VOLA MAPPING
# =============================================================================
ASSET_VOLA_MAP = {
"SPX": {"vola":"VIX", "name":"S&P 500"},
"NDX": {"vola":"VXN", "name":"Nasdaq 100"},
"DJX": {"vola":"VXD", "name":"Dow Jones"},
"RUT": {"vola":"RVX", "name":"Russell 2000"},
"GC1!": {"vola":"GVZ", "name":"Gold"},
"CL1!": {"vola":"OVX", "name":"Crude Oil"},
"FDAX1!":{"vola":"V1X.DE", "name":"DAX"},
"ESTX50":{"vola":"V2TX.DE","name":"Euro Stoxx 50"},
"NIKKEI":{"vola":"VHSI", "name":"Nikkei 225"},
"BTC": {"vola":"BVIX", "name":"Bitcoin"},
"ETH": {"vola":"EVIV", "name":"Ethereum"},
}
# =============================================================================
# MATPLOTLIB CHARTS
# =============================================================================
def chart_gex_profile(crash_profile, spot, zero_gamma):
fig, ax = plt.subplots(figsize=(12, 5))
df = pd.DataFrame(crash_profile)
ax.fill_between(df["spot_pct"], df["gex_plus"], 0, where=(df["gex_plus"]>=0).values, alpha=0.25, color="#00ff88", label="Long Gamma")
ax.fill_between(df["spot_pct"], df["gex_plus"], 0, where=(df["gex_plus"]<0).values, alpha=0.25, color="#ff4444", label="Short Gamma")
ax.plot(df["spot_pct"], df["gex_plus"], color="#00d4ff", linewidth=1.8)
ax.axhline(0, color="#2a3a5a", linewidth=1, linestyle="-")
ax.axvline(0, color="#8899aa", linewidth=0.5, linestyle=":", alpha=0.4)
zg_pct = (zero_gamma/spot-1)*100
ax.axvline(zg_pct, color="#ff6600", linewidth=1.5, linestyle="--", label=f"Zero Gamma: {zero_gamma:,.0f}")
ax.set_xlabel("Spot Move (%)", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("GEX+ ($)", fontsize=9, fontfamily="sans-serif")
ax.set_title("GEX+ Crash Profile -- Gamma Exposure vs Spot Move", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.legend(loc="upper right", fontsize=8, framealpha=0.8)
ax.grid(True, alpha=0.3)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:+.0f}%"))
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"${x/1e9:.1f}B" if abs(x)>=1e9 else f"${x/1e6:.0f}M"))
plt.tight_layout()
return fig
def chart_oi_by_strike(records, spot):
fig, ax = plt.subplots(figsize=(14, 5))
df = pd.DataFrame(records).sort_values("strike")
strikes = df["strike"].values
width = (strikes[1]-strikes[0])*0.35 if len(strikes)>1 else 5
ax.bar(strikes - width/2, df["oi_call"].values, width=width, color="#00ff88", alpha=0.7, label="Call OI")
ax.bar(strikes + width/2, (-df["oi_put"]).values, width=width, color="#ff4444", alpha=0.7, label="Put OI")
ax.axvline(spot, color="#ffaa00", linewidth=1.5, linestyle="--", label=f"Spot: {spot:,.0f}")
ax.set_xlabel("Strike", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("Open Interest", fontsize=9, fontfamily="sans-serif")
ax.set_title("Open Interest Profile by Strike", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.legend(loc="upper right", fontsize=8, framealpha=0.8)
ax.grid(True, alpha=0.3, axis="y")
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:,.0f}"))
plt.tight_layout()
return fig
def chart_gex_by_strike(records, spot):
fig, ax = plt.subplots(figsize=(14, 5))
df = pd.DataFrame(records).sort_values("strike")
strikes = df["strike"].values
gex_call = df["gamma_call"].values * df["oi_call"].values * spot**2 / 100
gex_put = -df["gamma_put"].values * df["oi_put"].values * spot**2 / 100
net_gex = gex_call + gex_put
width = (strikes[1]-strikes[0])*0.7 if len(strikes)>1 else 5
colors = ["#00ff88" if v >= 0 else "#ff4444" for v in net_gex]
ax.bar(strikes, net_gex/1e9, width=width, color=colors, alpha=0.75)
ax.axvline(spot, color="#ffaa00", linewidth=1.5, linestyle="--", label=f"Spot: {spot:,.0f}")
ax.axhline(0, color="#2a3a5a", linewidth=1)
ax.set_xlabel("Strike", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("Net GEX ($B)", fontsize=9, fontfamily="sans-serif")
ax.set_title("Gamma Exposure (GEX) by Strike", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.legend(loc="upper right", fontsize=8, framealpha=0.8)
ax.grid(True, alpha=0.3, axis="y")
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:,.0f}"))
plt.tight_layout()
return fig
def chart_iv_skew(records, spot):
fig, ax = plt.subplots(figsize=(12, 5))
df = pd.DataFrame(records).sort_values("strike")
moneyness = (df["strike"] / spot - 1) * 100
ax.plot(moneyness, df["iv_call"].values*100, color="#00d4ff", linewidth=1.8, label="Call IV", marker="o", markersize=2.5)
ax.plot(moneyness, df["iv_put"].values*100, color="#ff6600", linewidth=1.8, label="Put IV", marker="o", markersize=2.5)
ax.fill_between(moneyness, df["iv_call"].values*100, df["iv_put"].values*100, alpha=0.08, color="#00d4ff")
ax.axvline(0, color="#ffaa00", linewidth=1.2, linestyle="--", label="ATM")
ax.set_xlabel("Moneyness (%)", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("Implied Volatility (%)", fontsize=9, fontfamily="sans-serif")
ax.set_title("Implied Volatility Skew", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.legend(loc="upper right", fontsize=8, framealpha=0.8)
ax.grid(True, alpha=0.3)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:+.0f}%"))
plt.tight_layout()
return fig
def chart_vol_smile(records, spot, expiry=None):
fig, ax = plt.subplots(figsize=(12, 5))
df = pd.DataFrame(records).sort_values("strike")
if expiry:
df = df[df.get("expiry","") == expiry] if "expiry" in df.columns else df
ax.plot(df["strike"], df["iv_call"].values*100, color="#00d4ff", linewidth=1.8, label="Call IV", marker="o", markersize=3)
ax.plot(df["strike"], df["iv_put"].values*100, color="#ff6600", linewidth=1.8, label="Put IV", marker="s", markersize=3)
ax.axvline(spot, color="#ffaa00", linewidth=1.2, linestyle="--", label=f"Spot: {spot:,.0f}")
ax.set_xlabel("Strike", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("Implied Volatility (%)", fontsize=9, fontfamily="sans-serif")
title = "Volatility Smile" + (f" -- Exp: {expiry}" if expiry else "")
ax.set_title(title, fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.legend(loc="upper right", fontsize=8, framealpha=0.8)
ax.grid(True, alpha=0.3)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:,.0f}"))
plt.tight_layout()
return fig
def chart_heatmap(heatmap_data):
fig, ax = plt.subplots(figsize=(10, 6))
values = np.array(heatmap_data["values"])
spot_range = heatmap_data["spot_range"]
iv_range = heatmap_data["iv_range"]
im = ax.imshow(values, aspect="auto", origin="lower", cmap="RdBu_r", vmin=-1, vmax=1,
extent=[iv_range[0], iv_range[1], spot_range[0], spot_range[1]])
ax.set_xlabel("IV Shock (vol pts)", fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("Spot Move (%)", fontsize=9, fontfamily="sans-serif")
ax.set_title("GEX+ Heatmap -- Spot Move vs IV Shock (Normalized)", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label("Normalized GEX+", fontsize=8, fontfamily="sans-serif")
cbar.ax.yaxis.set_tick_params(color='#8899aa')
plt.setp(cbar.ax.yaxis.get_ticklabels(), color='#8899aa')
ax.grid(True, alpha=0.15)
plt.tight_layout()
return fig
def chart_term_structure(vix_terms_dict):
fig, ax = plt.subplots(figsize=(10, 4.5))
names = list(vix_terms_dict.keys())
vals = list(vix_terms_dict.values())
x_pos = range(len(names))
if len(vals) >= 2:
spread = vals[1] - vals[0]
is_contango = spread > 0
else:
is_contango = True
spread = 0
bar_colors = []
for i, v in enumerate(vals):
if i == 0:
bar_colors.append("#00d4ff")
else:
bar_colors.append("#00ff88" if (is_contango and v >= vals[0]) or (not is_contango and v >= vals[0]) else "#ff4444")
ax.bar(x_pos, vals, color=bar_colors, alpha=0.6, width=0.5, zorder=2)
ax.plot(x_pos, vals, color="#00d4ff", linewidth=2, marker="o", markersize=6, zorder=3)
if len(vals) >= 2:
regime_label = "CONTANGO" if is_contango else "BACKWARDATION"
regime_color = "#00ff88" if is_contango else "#ff4444"
ax.annotate(f"Spread: {spread:+.2f} [{regime_label}]",
xy=(0.02, 0.95), xycoords="axes fraction",
fontsize=10, color=regime_color, ha="left", va="top",
fontweight="bold", fontfamily="sans-serif")
ax.set_xticks(list(x_pos))
ax.set_xticklabels(names, fontsize=9, fontfamily="sans-serif")
ax.set_ylabel("VIX Level", fontsize=9, fontfamily="sans-serif")
ax.set_title("VIX Term Structure", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
ax.grid(True, alpha=0.3, axis="y")
ax.set_axisbelow(True)
plt.tight_layout()
return fig
def chart_fear_greed_gauge(fg_value, fg_label):
fig, ax = plt.subplots(figsize=(6, 3.5), subplot_kw=dict(projection='polar'))
theta_start = np.pi * 0.15
theta_end = np.pi * 0.85
theta_range = theta_end - theta_start
segments = [(0,25,"#ff4444","Extreme Fear"),(25,45,"#ff6600","Fear"),(45,55,"#ffaa00","Neutral"),(55,75,"#88cc00","Greed"),(75,100,"#00ff88","Extreme Greed")]
for lo, hi, color, _ in segments:
t_lo = theta_start + theta_range * lo / 100
t_hi = theta_start + theta_range * hi / 100
t_seg = np.linspace(t_lo, t_hi, 30)
ax.fill_between(t_seg, 0.6, 1.0, color=color, alpha=0.25)
needle_theta = theta_start + theta_range * fg_value / 100
ax.annotate('', xy=(needle_theta, 0.85), xytext=(needle_theta, 0),
arrowprops=dict(arrowstyle='->', color='#c8d6e5', lw=2.5))
ax.plot(needle_theta, 0, 'o', color='#00d4ff', markersize=6, zorder=5)
ax.text(np.pi/2, 0.35, str(fg_value), ha='center', va='center', fontsize=28, fontweight='bold', color='#c8d6e5', fontfamily='monospace', transform=ax.transAxes)
ax.text(np.pi/2, 0.15, fg_label.upper(), ha='center', va='center', fontsize=9, color='#8899aa', fontfamily='sans-serif', transform=ax.transAxes)
ax.set_ylim(0, 1.1)
ax.set_xlim(theta_start - 0.05, theta_end + 0.05)
ax.set_axis_off()
ax.set_title("Fear & Greed Index", fontsize=11, fontweight="bold", pad=15, fontfamily="sans-serif", y=1.05)
plt.tight_layout()
return fig
def chart_vol_heatmap(vol_data):
fig, ax = plt.subplots(figsize=(12, 3))
names = list(vol_data.keys())
values = [vol_data[n]["value"] for n in names]
changes = [vol_data[n].get("change") for n in names]
ivr_values = [vol_data[n].get("ivr") for n in names]
n = len(names)
cell_width = 1.0
cell_height = 1.0
for i, (name, val, chg, ivr) in enumerate(zip(names, values, changes, ivr_values)):
if val < 15:
bg_color = "#0a2a1a"; text_color = "#00ff88"
elif val < 25:
bg_color = "#2a2a0a"; text_color = "#ffaa00"
elif val < 35:
bg_color = "#2a1a0a"; text_color = "#ff6600"
else:
bg_color = "#2a0a0a"; text_color = "#ff4444"
rect = Rectangle((i * cell_width, 0), cell_width * 0.95, cell_height * 0.95, facecolor=bg_color, edgecolor="#2a3a5a", linewidth=1)
ax.add_patch(rect)
ax.text(i * cell_width + cell_width/2, cell_height * 0.75, name, ha='center', va='center', fontsize=10, fontweight='bold', color='#c8d6e5', fontfamily='sans-serif')
ax.text(i * cell_width + cell_width/2, cell_height * 0.50, f"{val:.2f}", ha='center', va='center', fontsize=14, fontweight='bold', color=text_color, fontfamily='monospace')
chg_str = f"{sign_prefix(chg)}{chg:.2f}%" if chg is not None else "N/A"
chg_color = "#00ff88" if chg is not None and chg >= 0 else "#ff4444" if chg is not None else "#8899aa"
ax.text(i * cell_width + cell_width/2, cell_height * 0.30, chg_str, ha='center', va='center', fontsize=8, color=chg_color, fontfamily='monospace')
ivr_str = f"IVR: {ivr:.0f}%" if ivr is not None else ""
ax.text(i * cell_width + cell_width/2, cell_height * 0.12, ivr_str, ha='center', va='center', fontsize=7, color='#8899aa', fontfamily='sans-serif')
ax.set_xlim(0, n * cell_width)
ax.set_ylim(0, cell_height)
ax.set_aspect('equal')
ax.set_axis_off()
ax.set_title("Volatility Index Monitor", fontsize=11, fontweight="bold", pad=12, fontfamily="sans-serif")
plt.tight_layout()
return fig
def chart_order_book(orderbook, symbol):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
bids = orderbook.get("bids", [])
asks = orderbook.get("asks", [])
if bids:
bid_prices = [b[0] for b in bids]
bid_amounts = [b[1] for b in bids]
ax1.barh(range(len(bids)), bid_amounts, color="#00ff88", alpha=0.7, height=0.8)
ax1.set_yticks(range(len(bids)))
ax1.set_yticklabels([f"{p:,.0f}" for p in bid_prices], fontsize=7)
ax1.set_xlabel("Size", fontsize=8, fontfamily="sans-serif")
ax1.set_title(f"{symbol.upper()} -- Bids", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax1.invert_yaxis()
ax1.grid(True, alpha=0.2, axis="x")
if asks:
ask_prices = [a[0] for a in asks]
ask_amounts = [a[1] for a in asks]
ax2.barh(range(len(asks)), ask_amounts, color="#ff4444", alpha=0.7, height=0.8)
ax2.set_yticks(range(len(asks)))
ax2.set_yticklabels([f"{p:,.0f}" for p in ask_prices], fontsize=7)
ax2.set_xlabel("Size", fontsize=8, fontfamily="sans-serif")
ax2.set_title(f"{symbol.upper()} -- Asks", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.grid(True, alpha=0.2, axis="x")
plt.tight_layout()
return fig
def chart_sparkline(prices, symbol, current_price):
fig, ax = plt.subplots(figsize=(3, 1.2))
if prices:
ax.plot(prices, color="#00d4ff", linewidth=1.2)
ax.fill_between(range(len(prices)), prices, min(prices), alpha=0.15, color="#00d4ff")
ax.set_axis_off()
ax.set_title(f"{symbol} {fmt_price(current_price)}", fontsize=7, fontfamily="sans-serif", color="#c8d6e5", pad=4, loc="left")
plt.tight_layout(pad=0.2)
return fig
def chart_bl_forecast(fc_1d, fc_1w):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
for ax, fc, label in [(ax1, fc_1d, "1-Day Forecast"), (ax2, fc_1w, "1-Week Forecast")]:
p5, p25, median, p75, p95 = fc["p5"], fc["p25"], fc["median"], fc["p75"], fc["p95"]
ax.plot([p5, p5], [0.3, 0.7], color="#8899aa", linewidth=1)
ax.plot([p95, p95], [0.3, 0.7], color="#8899aa", linewidth=1)
ax.plot([p5, p95], [0.5, 0.5], color="#8899aa", linewidth=1, linestyle="--")
rect = Rectangle((p25, 0.3), p75-p25, 0.4, facecolor="#00d4ff", alpha=0.2, edgecolor="#00d4ff", linewidth=1.5)
ax.add_patch(rect)
ax.plot([median, median], [0.3, 0.7], color="#ffaa00", linewidth=2)
ax.text(p5, 0.15, f"{p5:,.0f}", ha='center', fontsize=7, color="#8899aa", fontfamily="monospace")
ax.text(p95, 0.15, f"{p95:,.0f}", ha='center', fontsize=7, color="#8899aa", fontfamily="monospace")
ax.text(median, 0.85, f"{median:,.0f}", ha='center', fontsize=8, color="#ffaa00", fontweight="bold", fontfamily="monospace")
ax.text(p25, 0.85, f"{p25:,.0f}", ha='center', fontsize=7, color="#8899aa", fontfamily="monospace")
ax.text(p75, 0.85, f"{p75:,.0f}", ha='center', fontsize=7, color="#8899aa", fontfamily="monospace")
ax.set_xlim(p5 * 0.998, p95 * 1.002)
ax.set_ylim(0, 1)
ax.set_yticks([])
ax.set_xlabel("Price Level", fontsize=8, fontfamily="sans-serif")
ax.set_title(f"{label} (sigma: {fc['sigma_pct']:.1f}%)", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:,.0f}"))
ax.grid(True, alpha=0.2, axis="x")
plt.tight_layout()
return fig
def chart_pc_ratios(records):
"""Put/Call OI and Volume Ratios by Expiry."""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))
df = pd.DataFrame(records)
if "expiry" in df.columns:
by_exp = df.groupby("expiry").agg({"oi_call":"sum","oi_put":"sum"}).reset_index()
by_exp["pc_oi"] = by_exp["oi_put"] / by_exp["oi_call"].replace(0,1)
by_exp = by_exp.sort_values("expiry")
colors = ["#ff4444" if v > 1 else "#00ff88" for v in by_exp["pc_oi"]]
ax1.bar(range(len(by_exp)), by_exp["pc_oi"], color=colors, alpha=0.8)
ax1.set_xticks(range(len(by_exp)))
ax1.set_xticklabels(by_exp["expiry"], rotation=45, ha="right", fontsize=8)
ax1.axhline(1, color="#ffaa00", linewidth=1, linestyle="--", label="P/C = 1")
ax1.set_ylabel("Put/Call OI Ratio", fontsize=9, fontfamily="sans-serif")
ax1.set_title("Put/Call OI Ratio by Expiry", fontsize=11, fontweight="bold", fontfamily="sans-serif")
ax1.legend(fontsize=8)
ax1.grid(True, alpha=0.3, axis="y")
plt.tight_layout()
return fig
# =============================================================================
# VOLATILITY VINTAGE CHART FUNCTIONS
# =============================================================================
def chart_vvix_gauge(vvix_val, vvix_pct):
"""VVIX gauge showing current level and percentile."""
fig = plt.figure(figsize=(10, 3.5))
# Left: polar gauge
ax1 = fig.add_subplot(121, projection='polar')
theta_start = np.pi * 0.15
theta_end = np.pi * 0.85
theta_range = theta_end - theta_start
segments = [(0,25,"#00ff88","Low Vol"),(25,50,"#88cc00","Normal"),(50,75,"#ffaa00","Elevated"),(75,100,"#ff4444","Extreme")]
for lo, hi, color, _ in segments:
t_lo = theta_start + theta_range * lo / 100
t_hi = theta_start + theta_range * hi / 100
t_seg = np.linspace(t_lo, t_hi, 30)
ax1.fill_between(t_seg, 0.6, 1.0, color=color, alpha=0.25)
needle_theta = theta_start + theta_range * vvix_pct / 100
ax1.annotate('', xy=(needle_theta, 0.85), xytext=(needle_theta, 0),
arrowprops=dict(arrowstyle='->', color='#c8d6e5', lw=2.5))
ax1.plot(needle_theta, 0, 'o', color='#00d4ff', markersize=6, zorder=5)
ax1.set_ylim(0, 1.1)
ax1.set_xlim(theta_start - 0.05, theta_end + 0.05)
ax1.set_axis_off()
ax1.set_title(f"VVIX: {vvix_val:.1f} (Pctl: {vvix_pct:.0f}%)", fontsize=10, fontweight="bold", pad=15, fontfamily="sans-serif", y=1.05)
# Right: historical percentile bar
ax2 = fig.add_subplot(122)
pct_bars = [vvix_pct, 100 - vvix_pct]
colors_bar = ["#00d4ff", "#1e2d4a"]
ax2.barh([0], [pct_bars[0]], color=colors_bar[0], height=0.4, alpha=0.8)
ax2.barh([0], [pct_bars[1]], left=[pct_bars[0]], color=colors_bar[1], height=0.4, alpha=0.8)
ax2.set_xlim(0, 100)
ax2.set_yticks([])
ax2.set_xlabel("Percentile", fontsize=9, fontfamily="sans-serif")
ax2.set_title("VVIX Historical Percentile", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.axvline(vvix_pct, color="#ffaa00", linewidth=1.5, linestyle="--")
ax2.text(vvix_pct + 2, 0, f"{vvix_pct:.0f}%", fontsize=10, color="#ffaa00", fontweight="bold", fontfamily="monospace")
ax2.grid(True, alpha=0.2, axis="x")
plt.tight_layout()
return fig
def chart_vix_vvix_ratio(vix_series, vvix_series, ratio_z=None):
"""VIX/VVIX ratio chart with mean reversion signal."""
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={'height_ratios': [2, 1]}, sharex=False)
min_len = min(len(vix_series), len(vvix_series))
if min_len < 2:
ax1.text(0.5, 0.5, "Insufficient data", ha='center', va='center', transform=ax1.transAxes, fontsize=12, color="#8899aa")
return fig
vix = vix_series.tail(60).values
vvix = vvix_series.tail(60).values
ratio = vix / vvix
days = range(len(ratio))
# Top: VIX and VVIX levels
ax1.plot(days, vix, color="#00d4ff", linewidth=1.5, label="VIX")
ax1_twin = ax1.twinx()
ax1_twin.plot(days, vvix, color="#ffaa00", linewidth=1.5, label="VVIX", linestyle="--")
ax1.set_ylabel("VIX", fontsize=9, color="#00d4ff", fontfamily="sans-serif")
ax1_twin.set_ylabel("VVIX", fontsize=9, color="#ffaa00", fontfamily="sans-serif")
ax1.set_title("VIX vs VVIX (60-day)", fontsize=11, fontweight="bold", fontfamily="sans-serif")
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax1_twin.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left", fontsize=8, framealpha=0.8)
ax1.grid(True, alpha=0.3)
# Bottom: VIX/VVIX ratio
ax2.plot(days, ratio, color="#c8d6e5", linewidth=1.5)
ax2.fill_between(days, ratio, np.mean(ratio), where=(ratio >= np.mean(ratio)), alpha=0.15, color="#00ff88")
ax2.fill_between(days, ratio, np.mean(ratio), where=(ratio < np.mean(ratio)), alpha=0.15, color="#ff4444")
ax2.axhline(np.mean(ratio), color="#ffaa00", linewidth=1, linestyle="--", label=f"Mean: {np.mean(ratio):.4f}")
if ratio_z is not None:
ax2.annotate(f"Z-Score: {ratio_z:+.2f}", xy=(0.98, 0.95), xycoords="axes fraction",
fontsize=9, color="#ffaa00", ha="right", va="top", fontfamily="monospace")
ax2.set_ylabel("VIX/VVIX Ratio", fontsize=9, fontfamily="sans-serif")
ax2.set_xlabel("Trading Days", fontsize=9, fontfamily="sans-serif")
ax2.set_title("VIX/VVIX Ratio -- Mean Reversion Signal", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.legend(fontsize=8, framealpha=0.8)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
return fig
def chart_vol_regime_dashboard(regime_data):
"""Volatility regime detection dashboard."""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
# Left: regime probability bar chart
probs = regime_data.get("probabilities", {})
if probs:
regimes = list(probs.keys())
values = list(probs.values())
colors = ["#00ff88", "#00d4ff", "#ffaa00", "#ff4444"]
bars = ax1.barh(regimes, values, color=colors[:len(regimes)], alpha=0.7, height=0.5)
for bar, val in zip(bars, values):
ax1.text(bar.get_width() + 1, bar.get_y() + bar.get_height()/2,
f"{val:.1f}%", va='center', fontsize=9, fontfamily="monospace", color="#c8d6e5")
ax1.set_xlim(0, max(values) * 1.3)
ax1.set_xlabel("Probability (%)", fontsize=9, fontfamily="sans-serif")
current = regime_data.get("regime", "Unknown")
ax1.set_title(f"Regime Probabilities [Current: {current}]", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax1.grid(True, alpha=0.2, axis="x")
# Right: regime history (last 30 days)
hist = regime_data.get("history", [])
if hist:
hdf = pd.DataFrame(hist)
regime_map = {"Low Vol": 0, "Normal": 1, "Elevated": 2, "Crisis": 3}
hdf["regime_num"] = hdf["regime"].map(regime_map)
ax2.scatter(range(len(hdf)), hdf["vix"], c=hdf["regime_num"], cmap="RdYlGn_r", s=30, alpha=0.8, zorder=3)
ax2.plot(range(len(hdf)), hdf["vix"], color="#2a3a5a", linewidth=0.5, zorder=2)
ax2.set_ylabel("VIX", fontsize=9, fontfamily="sans-serif")
ax2.set_xlabel("Days Ago", fontsize=9, fontfamily="sans-serif")
ax2.set_title("Regime History (30-day VIX)", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.grid(True, alpha=0.2)
ax2.invert_xaxis()
plt.tight_layout()
return fig
def chart_vrp_analysis(vrp_data):
"""Vol Risk Premium analysis chart."""
if not vrp_data:
fig, ax = plt.subplots(figsize=(10, 4))
ax.text(0.5, 0.5, "Insufficient data for VRP analysis", ha='center', va='center', transform=ax.transAxes, fontsize=12, color="#8899aa")
return fig
n = len(vrp_data)
fig, axes = plt.subplots(n, 1, figsize=(12, 3.5 * n), sharex=False)
if n == 1:
axes = [axes]
for ax, (label, data) in zip(axes, vrp_data.items()):
vrp_hist = data.get("vrp_history")
if vrp_hist is not None and len(vrp_hist) > 10:
v = vrp_hist.tail(60)
ax.fill_between(range(len(v)), v.values, 0, where=(v.values >= 0), alpha=0.2, color="#00ff88", label="Positive VRP (IV > RV)")
ax.fill_between(range(len(v)), v.values, 0, where=(v.values < 0), alpha=0.2, color="#ff4444", label="Negative VRP (IV < RV)")
ax.plot(range(len(v)), v.values, color="#00d4ff", linewidth=1.2)
ax.axhline(0, color="#2a3a5a", linewidth=1)
ax.axhline(data["vrp_mean"], color="#ffaa00", linewidth=1, linestyle="--", label=f"Mean: {data['vrp_mean']:.2f}")
ax.set_ylabel("VRP (vol pts)", fontsize=9, fontfamily="sans-serif")
ax.set_title(f"Vol Risk Premium ({label}) -- Current: {data['vrp']:.2f} | Z-Score: {data['vrp_zscore']:+.2f}",
fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax.legend(fontsize=7, framealpha=0.8, loc="upper right")
ax.grid(True, alpha=0.3)
else:
ax.text(0.5, 0.5, f"No history for {label}", ha='center', va='center', transform=ax.transAxes, fontsize=10, color="#8899aa")
plt.tight_layout()
return fig
def chart_vix_fear_gauge_enhanced(fear_data):
"""Enhanced VIX Fear Gauge combining VIX, VVIX, VIX/VVIX ratio, VRP."""
fig = plt.figure(figsize=(12, 4))
# Left: polar gauge
ax1 = fig.add_subplot(121, projection='polar')
val = fear_data.get("value", 50)
zone = fear_data.get("zone", "Unknown")
color = fear_data.get("color", "#8899aa")
theta_start = np.pi * 0.15
theta_end = np.pi * 0.85
theta_range = theta_end - theta_start
segments = [(0,25,"#00ff88","Complacent"),(25,45,"#88cc00","Low Fear"),(45,60,"#ffaa00","Moderate"),(60,80,"#ff6600","Elevated"),(80,100,"#ff4444","Extreme")]
for lo, hi, seg_color, _ in segments:
t_lo = theta_start + theta_range * lo / 100
t_hi = theta_start + theta_range * hi / 100
t_seg = np.linspace(t_lo, t_hi, 30)
ax1.fill_between(t_seg, 0.6, 1.0, color=seg_color, alpha=0.25)
needle_theta = theta_start + theta_range * val / 100
ax1.annotate('', xy=(needle_theta, 0.85), xytext=(needle_theta, 0),
arrowprops=dict(arrowstyle='->', color='#c8d6e5', lw=2.5))
ax1.plot(needle_theta, 0, 'o', color=color, markersize=6, zorder=5)
ax1.set_ylim(0, 1.1)
ax1.set_xlim(theta_start - 0.05, theta_end + 0.05)
ax1.set_axis_off()
ax1.set_title(f"VIX Fear Gauge: {val:.0f} [{zone}]", fontsize=10, fontweight="bold", pad=15, fontfamily="sans-serif", y=1.05)
# Right: component breakdown
ax2 = fig.add_subplot(122)
components = fear_data.get("components", {})
if components:
names = list(components.keys())
vals = list(components.values())
max_vals = [40, 25, 15, 20]
pcts = [v/m*100 if m > 0 else 0 for v, m in zip(vals, max_vals)]
colors_bar = ["#00d4ff", "#ffaa00", "#ff6600", "#00ff88"]
bars = ax2.barh(names, pcts, color=colors_bar[:len(names)], alpha=0.7, height=0.5)
for bar, v, p in zip(bars, vals, pcts):
ax2.text(bar.get_width() + 1, bar.get_y() + bar.get_height()/2,
f"{v:.1f} ({p:.0f}%)", va='center', fontsize=8, fontfamily="monospace", color="#c8d6e5")
ax2.set_xlim(0, 110)
ax2.set_xlabel("Contribution (%)", fontsize=9, fontfamily="sans-serif")
ax2.set_title("Fear Index Components", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.grid(True, alpha=0.2, axis="x")
plt.tight_layout()
return fig
def chart_gex_vex_vgr(records, spot):
"""GEX/VEX/VGR dashboard."""
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 7), sharex=True)
df = pd.DataFrame(records).sort_values("strike")
strikes = df["strike"].values
width = (strikes[1]-strikes[0])*0.7 if len(strikes)>1 else 5
gex_call = df["gamma_call"].values * df["oi_call"].values * spot**2 / 100
gex_put = -df["gamma_put"].values * df["oi_put"].values * spot**2 / 100
net_gex = gex_call + gex_put
colors_gex = ["#00ff88" if v >= 0 else "#ff4444" for v in net_gex]
ax1.bar(strikes, net_gex/1e9, width=width, color=colors_gex, alpha=0.75)
ax1.axvline(spot, color="#ffaa00", linewidth=1.5, linestyle="--", label=f"Spot: {spot:,.0f}")
ax1.axhline(0, color="#2a3a5a", linewidth=1)
ax1.set_ylabel("Net GEX ($B)", fontsize=9, fontfamily="sans-serif")
ax1.set_title("Gamma Exposure (GEX) by Strike", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax1.legend(fontsize=8, framealpha=0.8)
ax1.grid(True, alpha=0.3, axis="y")
vanna_c = df.get("vanna_call", pd.Series([0]*len(df))).values
vanna_p = df.get("vanna_put", pd.Series([0]*len(df))).values
gamma_c = df["gamma_call"].values
gamma_p = df["gamma_put"].values
oi_c = df["oi_call"].values
oi_p = df["oi_put"].values
vex = (vanna_c * oi_c + vanna_p * oi_p) * spot / 1e6
gex_abs = np.abs(gamma_c * oi_c + gamma_p * oi_p) * spot**2 / 100 / 1e6
vgr = np.where(gex_abs > 0, np.abs(vex) / gex_abs, 0)
colors_vgr = ["#ff6600" if v > 1 else "#00d4ff" for v in vgr]
ax2.bar(strikes, vgr, width=width, color=colors_vgr, alpha=0.75)
ax2.axvline(spot, color="#ffaa00", linewidth=1.5, linestyle="--", label=f"Spot: {spot:,.0f}")
ax2.axhline(1, color="#ff6600", linewidth=1, linestyle=":", label="VGR = 1 (Vanna = Gamma)")
ax2.set_xlabel("Strike", fontsize=9, fontfamily="sans-serif")
ax2.set_ylabel("VGR (Vanna/Gamma Ratio)", fontsize=9, fontfamily="sans-serif")
ax2.set_title("Vanna/Gamma Ratio (VGR) by Strike [VGR > 1 = Vanna Dominant]", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.legend(fontsize=8, framealpha=0.8)
ax2.grid(True, alpha=0.3, axis="y")
ax2.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f"{x:,.0f}"))
plt.tight_layout()
return fig
def chart_cross_asset_vol(cross_data):
"""Cross-asset vol correlation heatmap and dispersion."""
corr = cross_data.get("correlation", pd.DataFrame())
current = cross_data.get("current", {})
dispersion = cross_data.get("dispersion", {})
if corr.empty:
fig, ax = plt.subplots(figsize=(8, 6))
ax.text(0.5, 0.5, "Insufficient data for cross-asset vol analysis", ha='center', va='center', transform=ax.transAxes, fontsize=12, color="#8899aa")
return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5), gridspec_kw={'width_ratios': [2, 1]})
im = ax1.imshow(corr.values, cmap="RdBu_r", vmin=-1, vmax=1, aspect="auto")
ax1.set_xticks(range(len(corr.columns)))
ax1.set_yticks(range(len(corr.index)))
ax1.set_xticklabels(corr.columns, rotation=45, ha="right", fontsize=8, fontfamily="sans-serif")
ax1.set_yticklabels(corr.index, fontsize=8, fontfamily="sans-serif")
for i in range(len(corr.index)):
for j in range(len(corr.columns)):
ax1.text(j, i, f"{corr.values[i,j]:.2f}", ha='center', va='center', fontsize=7,
color="#c8d6e5" if abs(corr.values[i,j]) < 0.7 else "#0a0e1a", fontfamily="monospace")
ax1.set_title("Cross-Asset Vol Correlation (60-day returns)", fontsize=10, fontweight="bold", fontfamily="sans-serif")
cbar = plt.colorbar(im, ax=ax1, fraction=0.046, pad=0.04)
cbar.set_label("Correlation", fontsize=8, fontfamily="sans-serif")
if current:
names = list(current.keys())
vals = list(current.values())
colors_bar = []
for v in vals:
if v < 15: colors_bar.append("#00ff88")
elif v < 25: colors_bar.append("#ffaa00")
elif v < 35: colors_bar.append("#ff6600")
else: colors_bar.append("#ff4444")
ax2.barh(names, vals, color=colors_bar, alpha=0.7, height=0.5)
ax2.set_xlabel("Current Level", fontsize=9, fontfamily="sans-serif")
vol_of_vols = dispersion.get("vol_of_vols", 0)
ax2.set_title(f"Current Vol Levels [Dispersion: {vol_of_vols:.1f}%]", fontsize=10, fontweight="bold", fontfamily="sans-serif")
ax2.grid(True, alpha=0.2, axis="x")
plt.tight_layout()
return fig
def chart_term_structure_heatmap(term_hist_data):
"""Historical term structure heatmap (time x tenor)."""
if not term_hist_data or len(term_hist_data) < 5:
fig, ax = plt.subplots(figsize=(10, 4))
ax.text(0.5, 0.5, "Insufficient term structure history", ha='center', va='center', transform=ax.transAxes, fontsize=12, color="#8899aa")
return fig
fig, ax = plt.subplots(figsize=(12, 5))
arr = np.array(term_hist_data["values"])
tenors = term_hist_data.get("tenors", [f"T{i}" for i in range(arr.shape[1])])
im = ax.imshow(arr.T, aspect="auto", origin="lower", cmap="YlOrRd", interpolation="bilinear")
ax.set_yticks(range(len(tenors)))
ax.set_yticklabels(tenors, fontsize=8, fontfamily="sans-serif")
ax.set_xlabel("Trading Days Ago", fontsize=9, fontfamily="sans-serif")
ax.set_title("VIX Term Structure History (Time x Tenor)", fontsize=11, fontweight="bold", fontfamily="sans-serif")
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label("VIX Level", fontsize=8, fontfamily="sans-serif")
ax.grid(True, alpha=0.1)
plt.tight_layout()
return fig
def chart_regime_transition_matrix(trans_matrix):
"""Regime transition matrix heatmap."""
if not trans_matrix:
fig, ax = plt.subplots(figsize=(6, 5))
ax.text(0.5, 0.5, "No transition matrix available", ha='center', va='center', transform=ax.transAxes, fontsize=12, color="#8899aa")
return fig
labels = list(trans_matrix.keys())
n = len(labels)
arr = np.array([[trans_matrix[r].get(c, 0) for c in labels] for r in labels])
fig, ax = plt.subplots(figsize=(7, 5))
im = ax.imshow(arr, cmap="YlGnBu", aspect="auto", vmin=0, vmax=100)
ax.set_xticks(range(n))
ax.set_yticks(range(n))
ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=8, fontfamily="sans-serif")
ax.set_yticklabels(labels, fontsize=8, fontfamily="sans-serif")
for i in range(n):
for j in range(n):
ax.text(j, i, f"{arr[i,j]:.1f}%", ha='center', va='center', fontsize=9,
color="#0a0e1a" if arr[i,j] > 50 else "#c8d6e5", fontfamily="monospace", fontweight="bold")
ax.set_title("Regime Transition Matrix (%)", fontsize=11, fontweight="bold", fontfamily="sans-serif")
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label("Transition Prob (%)", fontsize=8, fontfamily="sans-serif")
plt.tight_layout()
return fig
# =============================================================================
# LAYOUT
# =============================================================================
st.markdown(f"""<div style="display:flex;justify-content:space-between;align-items:center;
padding:8px 0 12px 0;border-bottom:1px solid #2a3a5a;margin-bottom:16px">
<div><span style="color:#00d4ff;font-size:1.1rem;font-weight:700;letter-spacing:0.04em;
font-family:sans-serif">MK QUANT MONITOR</span>
<span style="color:#8899aa;font-size:0.7rem;margin-left:12px;font-family:monospace">
INSTITUTIONAL TERMINAL v7.0 -- VOLATILITY VINCE EDITION</span></div>
<div style="color:#8899aa;font-size:0.7rem;font-family:monospace">
{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC</div>
</div>""", unsafe_allow_html=True)
with st.sidebar:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.06em;font-family:sans-serif'>CONTROLS</span>", unsafe_allow_html=True)
st.checkbox("Force DEMO_MODE", value=DEMO_MODE)
st.markdown("<hr style='border-color:#2a3a5a;margin:10px 0'>", unsafe_allow_html=True)
st.markdown("<span style='color:#8899aa;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;font-family:sans-serif'>Data Sources</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>CBOE SPX options chain</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>Deribit crypto perps + options</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>CoinGecko top 50</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>Yahoo Finance + yfinance</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>Fear &amp; Greed Index</span>", unsafe_allow_html=True)
st.markdown("<span style='color:#c8d6e5;font-size:0.75rem;font-family:monospace'>CongressInvests</span>", unsafe_allow_html=True)
st.markdown("<hr style='border-color:#2a3a5a;margin:10px 0'>", unsafe_allow_html=True)
st.markdown("<span style='color:#3a4a5a;font-size:0.65rem;font-family:monospace'>No API keys in source</span>", unsafe_allow_html=True)
tabs = st.tabs(["Markets Overview", "SPX & VIX Analytics", "Volatility Vince", "MarketGuardian Pro", "Crypto Ultra", "Insider Trades", "Settings"])
# ==============================================================================
# TAB 1: MARKETS OVERVIEW
# ==============================================================================
with tabs[0]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>MARKETS OVERVIEW -- Global Indices, Commodities &amp; Volatility</span>", unsafe_allow_html=True)
data = fetch_yahoo()
fg = fetch_fear_greed()
if fg:
fg_val = fg.get("value", 0)
fg_label = fg.get("label", "Unknown")
fg_color = "#00ff88" if fg_val > 60 else "#ffaa00" if fg_val > 40 else "#ff6600" if fg_val > 20 else "#ff4444"
col_fg1, col_fg2 = st.columns([1, 2])
with col_fg1:
st.markdown(f"""<div style="background:#1a2332;border:1px solid {fg_color};border-radius:4px;padding:14px 18px">
<div style="color:#8899aa;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em;font-family:sans-serif">Fear &amp; Greed Index</div>
<div style="color:{fg_color};font-size:2.2rem;font-weight:700;font-family:monospace">{fg_val}</div>
<div style="color:{fg_color};font-size:0.85rem;font-family:sans-serif;font-weight:600">{fg_label}</div>
</div>""", unsafe_allow_html=True)
with col_fg2:
fig_fg = chart_fear_greed_gauge(fg_val, fg_label)
st.pyplot(fig_fg)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>GLOBAL INDICES</span>", unsafe_allow_html=True)
INDICES = [
{"key":"SPX","name":"S&P 500","vola":"VIX"},{"key":"NDX","name":"Nasdaq 100","vola":"VXN"},
{"key":"DJX","name":"Dow Jones","vola":"VXD"},{"key":"RUT","name":"Russell 2000","vola":"RVX"},
{"key":"FDAX1!","name":"DAX Future","vola":"EVIV"},{"key":"ESTX50","name":"Euro Stoxx 50","vola":"EVIV"},
{"key":"NIKKEI","name":"Nikkei 225","vola":"VHSI"},{"key":"ES1!","name":"S&P 500 Fut","vola":"VIX"},
{"key":"NQ1!","name":"Nasdaq 100 Fut","vola":"VXN"},
]
idx_rows = []
for idx in INDICES:
s = _s(data.get(idx["key"]))
vs = _s(data.get(idx["vola"]))
if s is not None and len(s) > 0:
price = s.iloc[-1]
chg = ((s.iloc[-1]/s.iloc[-2])-1)*100 if len(s) > 1 else None
high = s.tail(5).max(); low = s.tail(5).min()
vol_price = vs.iloc[-1] if vs is not None and len(vs) > 0 else None
vol_chg = ((vs.iloc[-1]/vs.iloc[-2])-1)*100 if vs is not None and len(vs) > 1 else None
ivr = None
if vs is not None and len(vs) > 60:
mn, mx = vs.tail(60).min(), vs.tail(60).max()
if mx > mn: ivr = (vol_price - mn) / (mx - mn) * 100
idx_rows.append({"Ticker":idx["key"],"Name":idx["name"],"Price":price,"Chg%":chg,"High":high,"Low":low,"Vola":idx["vola"],"Vola Price":vol_price,"Vola Chg%":vol_chg,"IV Rank":ivr})
if idx_rows:
mc = st.columns(4)
for i, row in enumerate(idx_rows[:4]):
with mc[i]:
chg_str = f"{row['Chg%']:+.2f}%" if row['Chg%'] is not None else "N/A"
st.metric(row["Ticker"], f"{row['Price']:,.2f}", chg_str)
vol_str = f"{row['Vola']}: {row['Vola Price']:.2f}" if row['Vola Price'] else f"{row['Vola']}: N/A"
st.caption(vol_str)
mc2 = st.columns(4)
for i, row in enumerate(idx_rows[4:8]):
with mc2[i]:
chg_str = f"{row['Chg%']:+.2f}%" if row['Chg%'] is not None else "N/A"
st.metric(row["Ticker"], f"{row['Price']:,.2f}", chg_str)
vol_str = f"{row['Vola']}: {row['Vola Price']:.2f}" if row['Vola Price'] else f"{row['Vola']}: N/A"
st.caption(vol_str)
if len(idx_rows) > 8:
mc3 = st.columns(len(idx_rows) - 8)
for i, row in enumerate(idx_rows[8:]):
with mc3[i]:
chg_str = f"{row['Chg%']:+.2f}%" if row['Chg%'] is not None else "N/A"
st.metric(row["Ticker"], f"{row['Price']:,.2f}", chg_str)
vol_str = f"{row['Vola']}: {row['Vola Price']:.2f}" if row['Vola Price'] else f"{row['Vola']}: N/A"
st.caption(vol_str)
with st.expander("Detailed Index Data", expanded=False):
display_df = pd.DataFrame(idx_rows)
for col in ["Price","High","Low"]:
display_df[col] = display_df[col].map(lambda x: f"{x:,.2f}" if x else "N/A")
display_df["Chg%"] = display_df["Chg%"].map(lambda x: f"{x:+.2f}%" if x is not None else "N/A")
display_df["Vola Price"] = display_df["Vola Price"].map(lambda x: f"{x:.2f}" if x else "N/A")
display_df["Vola Chg%"] = display_df["Vola Chg%"].map(lambda x: f"{x:+.2f}%" if x is not None else "N/A")
display_df["IV Rank"] = display_df["IV Rank"].map(lambda x: f"{x:.0f}%" if x is not None else "N/A")
st.dataframe(display_df, use_container_width=True, hide_index=True)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>COMMODITIES</span>", unsafe_allow_html=True)
COMMODITIES = [
{"key":"GC1!","name":"Gold Future","vola":"GVZ"},{"key":"SI1!","name":"Silver Future","vola":"VXSLV"},
{"key":"HG1!","name":"Copper Future","vola":None},{"key":"CL1!","name":"Crude Oil WTI","vola":"OVX"},
{"key":"BZ1!","name":"Brent Oil Future","vola":"OVX"},{"key":"NG1!","name":"Natural Gas","vola":None},
]
comm_rows = []
for comm in COMMODITIES:
s = _s(data.get(comm["key"]))
vs = _s(data.get(comm["vola"])) if comm.get("vola") else None
if s is not None and len(s) > 0:
price = s.iloc[-1]; chg = ((s.iloc[-1]/s.iloc[-2])-1)*100 if len(s) > 1 else None
high = s.tail(5).max(); low = s.tail(5).min()
vol_price = vs.iloc[-1] if vs is not None and len(vs) > 0 else None
vol_chg = ((vs.iloc[-1]/vs.iloc[-2])-1)*100 if vs is not None and len(vs) > 1 else None
comm_rows.append({"Ticker":comm["key"],"Name":comm["name"],"Price":price,"Chg%":chg,"High":high,"Low":low,"Vola":comm.get("vola","--"),"Vola Price":vol_price,"Vola Chg%":vol_chg})
if comm_rows:
mc = st.columns(len(comm_rows))
for i, row in enumerate(comm_rows):
with mc[i]:
chg_str = f"{row['Chg%']:+.2f}%" if row['Chg%'] is not None else "N/A"
st.metric(row["Ticker"], f"{row['Price']:,.2f}", chg_str)
if row['Vola Price']:
st.caption(f"{row['Vola']}: {row['Vola Price']:.2f} ({row['Vola Chg%']:+.2f}%)" if row['Vola Chg%'] else f"{row['Vola']}: {row['Vola Price']:.2f}")
else:
st.caption(f"Vola: {row['Vola']}")
with st.expander("Detailed Commodity Data", expanded=False):
display_df = pd.DataFrame(comm_rows)
for col in ["Price","High","Low"]:
display_df[col] = display_df[col].map(lambda x: f"{x:,.2f}" if x else "N/A")
display_df["Chg%"] = display_df["Chg%"].map(lambda x: f"{x:+.2f}%" if x is not None else "N/A")
display_df["Vola Price"] = display_df["Vola Price"].map(lambda x: f"{x:.2f}" if x else "N/A")
display_df["Vola Chg%"] = display_df["Vola Chg%"].map(lambda x: f"{x:+.2f}%" if x is not None else "N/A")
st.dataframe(display_df, use_container_width=True, hide_index=True)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VOLATILITY MONITOR</span>", unsafe_allow_html=True)
vol_indices = ["VIX","VVIX","VXN","VXD","RVX","OVX","GVZ","VXEEM"]
vol_data = {}
for vk in vol_indices:
s = _s(data.get(vk))
if s is not None and len(s) > 0:
val = s.iloc[-1]; chg = ((s.iloc[-1]/s.iloc[-2])-1)*100 if len(s) > 1 else None
ivr = None
if len(s) > 60:
mn, mx = s.tail(60).min(), s.tail(60).max()
if mx > mn: ivr = (val - mn) / (mx - mn) * 100
vol_data[vk] = {"value": val, "change": chg, "ivr": ivr}
if vol_data:
fig_vol = chart_vol_heatmap(vol_data)
st.pyplot(fig_vol)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VIX TERM STRUCTURE</span>", unsafe_allow_html=True)
vix_terms = {}
vix_symbols_ts = {"Spot VIX":"VIX","VIX 1M":"^VIX3M","VIX 2M":"^VIX6M"}
for name, key in vix_symbols_ts.items():
s = _s(data.get(key))
if s is not None and len(s) > 0:
vix_terms[name] = s.iloc[-1]
if len(vix_terms) >= 2:
col1, col2 = st.columns(2)
with col1:
fig_ts = chart_term_structure(vix_terms)
st.pyplot(fig_ts)
with col2:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;font-family:sans-serif'>Term Structure Levels</span>", unsafe_allow_html=True)
for name, val in vix_terms.items():
st.markdown(f"<div style='color:#c8d6e5;font-family:monospace;font-size:0.85rem;padding:2px 0'>{name}: <span style='color:#00d4ff'>{val:.2f}</span></div>", unsafe_allow_html=True)
vals = list(vix_terms.values())
if len(vals) >= 2:
spread = vals[1] - vals[0]
regime = "CONTANGO" if spread > 0 else "BACKWARDATION"
regime_color = "#00ff88" if spread > 0 else "#ff4444"
st.markdown(f"<div style='color:{regime_color};font-family:monospace;font-size:0.85rem;padding:4px 0'>Spread: <b>{spread:+.2f}</b> [{regime}]</div>", unsafe_allow_html=True)
st.caption("Contango = futures above spot (normal)" if spread > 0 else "Backwardation = futures below spot (stress signal)")
else:
st.info("VIX futures data unavailable -- Yahoo Finance may be rate-limited")
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>MACRO INDICATORS (FRED)</span>", unsafe_allow_html=True)
fred_s = {"DGS10":"10Y Treasury","T10Y2Y":"10Y-2Y Spread","DFF":"Fed Funds Rate","T5YIE":"5Y Breakeven"}
fc = st.columns(len(fred_s))
for i,(sid,label) in enumerate(fred_s.items()):
with fc[i]:
fd = fetch_fred(sid, 1)
if fd is not None and len(fd)>0:
st.metric(label, f"{fd.iloc[-1]:.2f}%")
else:
st.metric(label, "N/A", "Set FRED_API_KEY")
# ==============================================================================
# TAB 2: SPX & VIX ANALYTICS
# ==============================================================================
with tabs[1]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>SPX &amp; VIX ANALYTICS -- Options Chain Analysis</span>", unsafe_allow_html=True)
st.caption("Live CBOE data | GEX+/VEX/VGR | Zero Gamma | Crash Profile | BL Forecast | IV Skew/Smile | Heatmap")
with st.spinner("Fetching SPX options chain..."):
chain = fetch_cboe_spx_chain()
if chain and chain.get("records"):
spot = chain["spot"]
records = chain["records"]
source = chain.get("source","unknown")
ts = chain.get("timestamp","")
st.markdown(f"<div style='background:#1a2332;border-left:3px solid #00ff88;border-radius:2px;padding:8px 14px;margin:8px 0'><span style='color:#00ff88;font-family:monospace;font-size:0.8rem'>LIVE</span> <span style='color:#c8d6e5;font-family:monospace;font-size:0.8rem'>CBOE data | Spot: {spot:,.2f} | {len(records)} strikes | Source: {source} | {ts[:19]}</span></div>", unsafe_allow_html=True)
else:
# Generate realistic synthetic data based on current VIX/VVIX from Yahoo
vix_data = fetch_yahoo()
vix_s = _s(vix_data.get("VIX"))
vvix_s = _s(vix_data.get("VVIX"))
spx_s = _s(vix_data.get("SPX"))
# Use real VIX/VVIX if available, otherwise defaults
current_vix = vix_s.iloc[-1] if vix_s is not None and len(vix_s) > 0 else 18.0
current_vvix = vvix_s.iloc[-1] if vvix_s is not None and len(vvix_s) > 0 else 85.0
spot = spx_s.iloc[-1] if spx_s is not None and len(spx_s) > 0 else 5450.0
# Derive ATM IV from VIX (VIX ~= ATM IV * 100 for SPX)
atm_iv = current_vix / 100.0
rng = np.random.default_rng(int(spot) % 10000)
records = []
# Generate strikes around spot (80% to 120% of spot)
strike_range = int(spot * 0.20 / 25) * 25
strikes = np.arange(spot - strike_range, spot + strike_range + 25, 25)
# Generate multiple expiries (weekly + monthly)
from datetime import timedelta
today = datetime.utcnow().date()
expiries = []
# Weekly expiries (next 4 Fridays)
for i in range(1, 5):
d = today + timedelta(weeks=i)
# Adjust to Friday
while d.weekday() != 4:
d += timedelta(days=1)
expiries.append(d.strftime("%Y-%m-%d"))
# Monthly expiries (next 3 months)
for i in range(1, 4):
m = today.month + i
y = today.year + (m - 1) // 12
m = (m - 1) % 12 + 1
d = datetime(y, m, 1).date()
# Third Friday
while d.weekday() != 4:
d += timedelta(days=1)
d += timedelta(weeks=2)
expiries.append(d.strftime("%Y-%m-%d"))
for exp in expiries:
try:
exp_date = datetime.strptime(exp, "%Y-%m-%d").date()
dte = max(1, (exp_date - today).days)
except:
dte = 30
T = dte / 365.0
for k in strikes:
atm_dist = (k - spot) / spot
# Realistic IV skew: higher for OTM puts, lower for OTM calls
# VVIX influences the skew steepness
skew_factor = current_vvix / 85.0 # Normalize around typical VVIX
iv_c = max(0.03, atm_iv + abs(atm_dist) * 0.25 * skew_factor - atm_dist * 0.08 * skew_factor)
iv_p = max(0.03, iv_c + 0.015 + max(0.0, -atm_dist * 0.12 * skew_factor))
# Gamma: peaks ATM, decays with distance
gamma_base = 0.004 * np.exp(-60 * atm_dist**2) / np.sqrt(T)
gamma_c = max(0.000001, gamma_base * (1 + rng.normal(0, 0.05)))
gamma_p = max(0.000001, gamma_base * (1 + rng.normal(0, 0.05)))
# Delta: based on Black-Scholes approximation
if T > 0 and iv_c > 0:
d1_c = (np.log(spot/k) + (0.5 * iv_c**2) * T) / (iv_c * np.sqrt(T))
delta_c = float(np.clip(0.5 + d1_c * 0.4, 0.01, 0.99))
else:
delta_c = 0.5 if k <= spot else 0.01
delta_p = delta_c - 1.0
# Vanna: peaks at wings
vanna_c = float(gamma_c * (1 - delta_c) / max(iv_c, 0.01) * atm_dist)
vanna_p = float(gamma_p * (1 - abs(delta_p)) / max(iv_p, 0.01) * atm_dist)
# OI: higher ATM, lower wings, higher for puts (hedging demand)
oi_base = abs(rng.normal(6000, 2500))
oi_factor = np.exp(-25 * atm_dist**2) + 0.05
oi_call = int(oi_base * oi_factor * (1 + rng.normal(0, 0.1)))
oi_put = int(oi_base * oi_factor * 1.3 * (1 + rng.normal(0, 0.1)))
# Theta
theta_c = -gamma_c * spot * spot * iv_c / (2 * np.sqrt(T)) / 365 if T > 0 else 0
theta_p = -gamma_p * spot * spot * iv_p / (2 * np.sqrt(T)) / 365 if T > 0 else 0
# Vega
vega_val = spot * np.sqrt(T) * np.exp(-0.5 * (np.log(spot/k)/max(iv_c,0.01))**2) * 0.01 if T > 0 and iv_c > 0 else 0
# Bid/ask
intrinsic_c = max(0, spot - k)
time_value_c = max(0.01, iv_c * spot * np.sqrt(T) * 0.4)
mid_c = intrinsic_c + time_value_c
spread_c = max(0.25, mid_c * 0.03)
intrinsic_p = max(0, k - spot)
time_value_p = max(0.01, iv_p * spot * np.sqrt(T) * 0.4)
mid_p = intrinsic_p + time_value_p
spread_p = max(0.25, mid_p * 0.03)
records.append({
"strike": float(k),
"expiry": exp,
"oi_call": max(0, oi_call),
"oi_put": max(0, oi_put),
"iv_call": round(iv_c, 4),
"iv_put": round(iv_p, 4),
"delta_call": round(delta_c, 4),
"delta_put": round(delta_p, 4),
"gamma_call": round(gamma_c, 6),
"gamma_put": round(gamma_p, 6),
"theta_call": round(theta_c, 4),
"theta_put": round(theta_p, 4),
"vega_call": round(vega_val, 4),
"vega_put": round(vega_val, 4),
"vanna_call": round(vanna_c, 4),
"vanna_put": round(vanna_p, 4),
"bid_call": round(max(0.01, mid_c - spread_c/2), 2),
"ask_call": round(mid_c + spread_c/2, 2),
"bid_put": round(max(0.01, mid_p - spread_p/2), 2),
"ask_put": round(mid_p + spread_p/2, 2),
})
st.markdown(f"<div style='background:#1a2332;border-left:3px solid #ffaa00;border-radius:2px;padding:8px 14px;margin:8px 0'><span style='color:#ffaa00;font-family:monospace;font-size:0.8rem'>SYNTHETIC</span> <span style='color:#c8d6e5;font-family:monospace;font-size:0.8rem'>CBOE unreachable | VIX: {current_vix:.1f} | VVIX: {current_vvix:.1f} | {len(records)} records | {len(expiries)} expiries</span></div>", unsafe_allow_html=True)
strikes_list = records
gex_val = compute_gex_plus(strikes_list, spot)
vex_val = compute_vanna_exposure(strikes_list, spot)
vgr_val = abs(vex_val) / abs(gex_val) if abs(gex_val) > 1e-6 else 0.0
zg_val = find_zero_gamma(strikes_list, spot)
vix_data = fetch_yahoo()
vix_s = _s(vix_data.get("VIX"))
vix_val = vix_s.iloc[-1] if vix_s is not None and len(vix_s)>0 else None
vvix_s = _s(vix_data.get("VVIX"))
vvix_val = vvix_s.iloc[-1] if vvix_s is not None and len(vvix_s)>0 else None
# Max pain and gamma walls
max_pain_strike, _ = compute_max_pain(records, spot)
gamma_walls = compute_gamma_walls(records, spot)
delta_neutral = compute_delta_neutral_strike(records, spot)
mcols = st.columns(6)
with mcols[0]: st.metric("SPX Spot", f"{spot:,.2f}")
with mcols[1]:
if vix_val:
st.metric("VIX", f"{vix_val:.2f}", f"{((vix_val/vix_s.iloc[-2])-1)*100:+.2f}%" if len(vix_s)>1 else None)
else: st.metric("VIX", "N/A")
with mcols[2]: st.metric("VVIX", f"{vvix_val:.2f}" if vvix_val else "N/A")
with mcols[3]: st.metric("Net GEX+", f"${gex_val/1e9:.2f}B", "Long Gamma" if gex_val>0 else "Short Gamma")
with mcols[4]: st.metric("Zero Gamma", f"{zg_val:,.0f}", f"{(zg_val/spot-1)*100:+.1f}%")
with mcols[5]: st.metric("VGR", f"{vgr_val:.2f}", "Vanna Dominant" if vgr_val>1 else "Gamma Dominant")
# Key levels row
kl_cols = st.columns(4)
with kl_cols[0]:
st.metric("Max Pain", f"{max_pain_strike:,.0f}" if max_pain_strike else "N/A",
f"{(max_pain_strike/spot-1)*100:+.1f}%" if max_pain_strike else None)
with kl_cols[1]:
st.metric("Delta Neutral", f"{delta_neutral:,.0f}" if delta_neutral else "N/A",
f"{(delta_neutral/spot-1)*100:+.1f}%" if delta_neutral else None)
with kl_cols[2]:
if gamma_walls:
top_wall = gamma_walls[0]
st.metric("Top Gamma Wall", f"{top_wall['strike']:,.0f}", f"GEX: ${top_wall['gex']/1e9:.2f}B")
else:
st.metric("Top Gamma Wall", "N/A")
with kl_cols[3]:
st.metric("VEX", f"${vex_val/1e6:.1f}M")
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
c1, c2 = st.columns(2)
with c1:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>GEX+ Crash Profile</span>", unsafe_allow_html=True)
profile = compute_crash_profile(strikes_list, spot)
st.pyplot(chart_gex_profile(profile, spot, zg_val))
with c2:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>Open Interest by Strike</span>", unsafe_allow_html=True)
st.pyplot(chart_oi_by_strike(records, spot))
c1, c2 = st.columns(2)
with c1:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>Gamma Exposure by Strike</span>", unsafe_allow_html=True)
st.pyplot(chart_gex_by_strike(records, spot))
with c2:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>IV Skew</span>", unsafe_allow_html=True)
st.pyplot(chart_iv_skew(records, spot))
c1, c2 = st.columns(2)
with c1:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>Volatility Smile</span>", unsafe_allow_html=True)
st.pyplot(chart_vol_smile(records, spot))
with c2:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>GEX+ Heatmap (Spot vs IV Shock)</span>", unsafe_allow_html=True)
hm = _compute_heatmap_inline(strikes_list, spot, n_spot=30, n_iv=40)
st.pyplot(chart_heatmap(hm))
c1, c2 = st.columns(2)
with c1:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>Put/Call Ratios by Expiry</span>", unsafe_allow_html=True)
st.pyplot(chart_pc_ratios(records))
with c2:
st.markdown("<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>GEX/VEX/VGR Dashboard</span>", unsafe_allow_html=True)
st.pyplot(chart_gex_vex_vgr(records, spot))
# BL Forecast
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>BREEDEN-LITZENBERGER FORECAST</span>", unsafe_allow_html=True)
fc_bl = _compute_bl_forecast_inline(strikes_list, spot, dte=30)
fcc = st.columns(2)
for i,(period,label) in enumerate([("one_day","1-Day"),("one_week","1-Week")]):
with fcc[i]:
f = fc_bl[period]
st.markdown(f"<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>{label} Forecast</span>", unsafe_allow_html=True)
fc_cols = st.columns(4)
with fc_cols[0]: st.metric("Sigma", f"{f['sigma_pct']:.1f}%")
with fc_cols[1]: st.metric("Median", f"{f['median']:,.0f}")
with fc_cols[2]: st.metric("5th pct", f"{f['p5']:,.0f}")
with fc_cols[3]: st.metric("95th pct", f"{f['p95']:,.0f}")
st.caption(f"90% Range: {f['range_90'][0]:,.0f} -- {f['range_90'][1]:,.0f}")
st.pyplot(chart_bl_forecast(fc_bl["one_day"], fc_bl["one_week"]))
# Options Chain Table
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>OPTIONS CHAIN DATA</span>", unsafe_allow_html=True)
chain_df = pd.DataFrame(records).sort_values("strike")
display_cols = ["strike","oi_call","oi_put","iv_call","iv_put","delta_call","delta_put","gamma_call","gamma_put"]
available_cols = [c for c in display_cols if c in chain_df.columns]
st.dataframe(chain_df[available_cols].head(30), use_container_width=True, height=400)
# =============================================================================
# TAB 3: VOLATILITY VINCE DASHBOARD
# =============================================================================
with tabs[2]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VOLATILITY VINCE -- Institutional Vol Intelligence</span>", unsafe_allow_html=True)
st.caption("VVIX | VIX Term Structure | Vol Regime | VRP | Fear Gauge | Cross-Asset Vol | GEX/VEX/VGR")
vd = fetch_yahoo()
vix_s = _s(vd.get("VIX"))
vvix_s = _s(vd.get("VVIX"))
spx_s = _s(vd.get("SPX"))
vix_cur = vix_s.iloc[-1] if vix_s is not None and len(vix_s) > 0 else None
vvix_cur = vvix_s.iloc[-1] if vvix_s is not None and len(vvix_s) > 0 else None
spx_cur = spx_s.iloc[-1] if spx_s is not None and len(spx_s) > 0 else None
# ===================================================================
# ROW 1: VVIX INTELLIGENCE
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VVIX INTELLIGENCE (Volatility of Volatility)</span>", unsafe_allow_html=True)
vvix_val, vvix_pct, vvix_rank = compute_vvix_percentile(vvix_s) if vvix_s is not None else (None, None, None)
vix_vvix_ratio, ratio_z, ratio_hist = compute_vix_vvix_ratio(vix_s, vvix_s) if vix_s is not None and vvix_s is not None else (None, None, None)
mcols_vvix = st.columns(4)
with mcols_vvix[0]:
st.metric("VVIX", f"{vvix_cur:.2f}" if vvix_cur else "N/A",
f"Chg: {((vvix_s.iloc[-1]/vvix_s.iloc[-2])-1)*100:+.2f}%" if vvix_s is not None and len(vvix_s) > 1 else None)
with mcols_vvix[1]:
st.metric("VVIX Percentile", f"{vvix_pct:.0f}%" if vvix_pct else "N/A",
f"Rank: {vvix_rank:.0f}/100" if vvix_rank else None)
with mcols_vvix[2]:
st.metric("VIX/VVIX Ratio", f"{vix_vvix_ratio:.4f}" if vix_vvix_ratio else "N/A",
f"Mean-Reversion Signal" if vix_vvix_ratio and ratio_z and abs(ratio_z) > 1.5 else "Normal")
with mcols_vvix[3]:
st.metric("Ratio Z-Score", f"{ratio_z:+.2f}" if ratio_z else "N/A",
f"{'Overvalued VIX' if ratio_z and ratio_z > 1 else 'Undervalued VIX' if ratio_z and ratio_z < -1 else 'Neutral'}" if ratio_z else None)
c1, c2 = st.columns(2)
with c1:
if vvix_cur is not None and vvix_pct is not None:
st.pyplot(chart_vvix_gauge(vvix_cur, vvix_pct))
with c2:
if vix_s is not None and vvix_s is not None:
st.pyplot(chart_vix_vvix_ratio(vix_s, vvix_s, ratio_z))
# ===================================================================
# ROW 2: VIX TERM STRUCTURE (ENHANCED)
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VIX TERM STRUCTURE (ENHANCED)</span>", unsafe_allow_html=True)
vix_terms = {}
vix_ts_keys = {"Spot": "VIX", "1M": "^VIX3M", "2M": "^VIX6M", "3M": "^VIX9M"}
for name, key in vix_ts_keys.items():
s = _s(vd.get(key))
if s is not None and len(s) > 0:
vix_terms[name] = s.iloc[-1]
ts_metrics = compute_term_structure_metrics(vix_terms)
mcols_ts = st.columns(5)
with mcols_ts[0]:
regime = ts_metrics.get("regime", "N/A")
regime_color = "#00ff88" if "CONTANGO" in regime else "#ff4444"
st.markdown(f"<div style='background:#1a2332;border:1px solid {regime_color};border-radius:4px;padding:8px 12px;text-align:center'><div style='color:#8899aa;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em'>Regime</div><div style='color:{regime_color};font-size:1.4rem;font-weight:700;font-family:monospace'>{regime}</div></div>", unsafe_allow_html=True)
with mcols_ts[1]:
spread = ts_metrics.get("spread", 0)
st.metric("Front-Back Spread", f"{spread:+.2f}")
with mcols_ts[2]:
st.metric("Slope", f"{ts_metrics.get('slope', 0):.3f}",
f"{'Steep' if abs(ts_metrics.get('slope', 0)) > 0.5 else 'Flat'}" if ts_metrics.get('slope') else None)
with mcols_ts[3]:
st.metric("Curvature", f"{ts_metrics.get('curvature', 0):.3f}")
with mcols_ts[4]:
st.metric("Roll Yield", f"{ts_metrics.get('roll_yield', 0):.1f}%",
f"{'Positive' if ts_metrics.get('roll_yield', 0) > 0 else 'Negative'}")
if len(vix_terms) >= 2:
st.pyplot(chart_term_structure(vix_terms))
# VIX Term Structure Historical Heatmap
if len(vix_ts_keys) >= 2:
vix_ts_hist = {}
for name, key in vix_ts_keys.items():
s = _s(vd.get(key))
if s is not None and len(s) > 30:
vix_ts_hist[name] = s.tail(60)
if len(vix_ts_hist) >= 2:
st.pyplot(chart_term_structure_heatmap(vix_ts_hist))
# ===================================================================
# ROW 3: VOLATILITY REGIME DETECTION
# ===================================================================
st.markdown("<div style='height='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VOLATILITY REGIME DETECTION</span>", unsafe_allow_html=True)
regime_data = compute_vol_regime_detection(vix_s, vvix_s, spx_s)
probs = regime_data.get("probabilities", {})
current_regime = regime_data.get("regime", "N/A")
regime_colors = {"Low Vol": "#00ff88", "Normal": "#88cc00", "Elevated": "#ffaa00", "Crisis": "#ff4444"}
rc = regime_colors.get(current_regime, "#8899aa")
mcols_reg = st.columns(5)
with mcols_reg[0]:
st.markdown(f"<div style='background:#1a2332;border:2px solid {rc};border-radius:4px;padding:8px 12px;text-align:center'><div style='color:#8899aa;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em'>Current Regime</div><div style='color:{rc};font-size:1.2rem;font-weight:700'>{current_regime}</div></div>", unsafe_allow_html=True)
with mcols_reg[1]:
st.metric("Low Vol Prob", f"{probs.get('Low Vol', 0):.1f}%")
with mcols_reg[2]:
st.metric("Normal Prob", f"{probs.get('Normal', 0):.1f}%")
with mcols_reg[3]:
st.metric("Elevated Prob", f"{probs.get('Elevated', 0):.1f}%")
with mcols_reg[4]:
st.metric("Crisis Prob", f"{probs.get('Crisis', 0):.1f}%")
c1, c2 = st.columns(2)
with c1:
st.pyplot(chart_vol_regime_dashboard(regime_data))
with c2:
trans_matrix = regime_data.get("transition_matrix", {})
if trans_matrix:
st.pyplot(chart_regime_transition_matrix(trans_matrix))
# ===================================================================
# ROW 4: VOLATILITY RISK PREMIUM
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VOLATILITY RISK PREMIUM (IV vs RV)</span>", unsafe_allow_html=True)
vrp_data = compute_vol_risk_premium(vix_s, spx_s, windows=[20, 60, 120])
if vrp_data:
mcols_vrp = st.columns(len(vrp_data))
for i, (label, vrp) in enumerate(vrp_data.items()):
with mcols_vrp[i]:
vrp_val = vrp['vrp']
z = vrp['vrp_zscore']
color = "#00ff88" if vrp_val > 0 else "#ff4444"
st.markdown(f"<div style='background:#1a2332;border:1px solid {color};border-radius:4px;padding:8px 12px'><div style='color:#8899aa;font-size:0.65rem;text-transform:uppercase'>VRP ({label})</div><div style='color:{color};font-size:1.3rem;font-weight:700;font-family:monospace'>{vrp_val:+.2f}</div><div style='color:#8899aa;font-size:0.7rem'>Z-Score: {z:+.2f}</div><div style='color:#8899aa;font-size:0.65rem'>VIX: {vrp['vix']:.2f} | RV: {vrp['rv']:.2f}</div></div>", unsafe_allow_html=True)
st.pyplot(chart_vrp_analysis(vrp_data))
else:
st.info("Insufficient data for VRP calculation")
# ===================================================================
# ROW 5: VIX FEAR GAUGE (COMPOSITE)
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>VIX FEAR GAUGE (COMPOSITE INDEX)</span>", unsafe_allow_html=True)
current_vrp_val = list(vrp_data.values())[0]["vrp"] if vrp_data else None
fear_data = compute_vix_fear_gauge(vix_cur, vvix_cur, vix_vvix_ratio, current_vrp_val)
fi = fear_data.get('value', 0)
fz = fear_data.get('zone', 'Neutral')
fc = "#ff4444" if fi > 75 else "#ffaa00" if fi > 50 else "#88cc00" if fi > 25 else "#00ff88"
components = fear_data.get('components', {})
mcols_fear = st.columns(4)
with mcols_fear[0]:
st.markdown(f"<div style='background:#1a2332;border:2px solid {fc};border-radius:4px;padding:8px 12px;text-align:center'><div style='color:#8899aa;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.08em'>Fear Index</div><div style='color:{fc};font-size:1.8rem;font-weight:700;font-family:monospace'>{fi:.0f}</div><div style='color:{fc};font-size:0.8rem;font-weight:600'>{fz}</div><div style='color:#8899aa;font-size:0.6rem'>/ 100</div></div>", unsafe_allow_html=True)
with mcols_fear[1]:
st.metric("VIX Component", f"{components.get('VIX', 0):.1f}/40",
f"Percentile: {fear_data.get('components', {}).get('VIX_pct', 0):.0f}%" if fear_data.get('components', {}).get('VIX_pct') else None)
with mcols_fear[2]:
st.metric("VVIX Component", f"{components.get('VVIX', 0):.1f}/25",
f"Percentile: {fear_data.get('components', {}).get('VVIX_pct', 0):.0f}%" if fear_data.get('components', {}).get('VVIX_pct') else None)
with mcols_fear[3]:
st.metric("VRP Component", f"{components.get('VRP', 0):.1f}/20",
f"Z-Score: {fear_data.get('components', {}).get('VRP_z', 0):+.2f}" if fear_data.get('components', {}).get('VRP_z') is not None else None)
st.pyplot(chart_vix_fear_gauge_enhanced(fear_data))
# ===================================================================
# ROW 6: CROSS-ASSET VOLATILITY MONITOR
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>CROSS-ASSET VOLATILITY MONITOR</span>", unsafe_allow_html=True)
vol_names = ["VIX", "VVIX", "VXN", "VXD", "RVX", "OVX", "GVZ", "VXEEM"]
vol_series_dict = {}
for vn in vol_names:
s = _s(vd.get(vn))
if s is not None and len(s) > 20:
vol_series_dict[vn] = s
cross_data = compute_cross_asset_vol(vol_series_dict)
if cross_data.get("current"):
n_cols = min(len(cross_data["current"]), 8)
mcols_cross = st.columns(n_cols)
for i, (name, val) in enumerate(list(cross_data["current"].items())[:n_cols]):
if i < n_cols:
with mcols_cross[i]:
chg_str = ""
if name in vol_series_dict and len(vol_series_dict[name]) > 1:
chg = ((vol_series_dict[name].iloc[-1] / vol_series_dict[name].iloc[-2]) - 1) * 100
chg_str = f"{chg:+.2f}%"
st.metric(name, f"{val:.2f}", chg_str)
disp = cross_data.get("dispersion", {}).get("vol_of_vols", 0)
st.caption(f"Vol of Vols (cross-asset dispersion): {disp:.1f}% | Higher = more disagreement between asset vols")
st.pyplot(chart_cross_asset_vol(cross_data))
# ===================================================================
# ROW 7: GEX/VEX/VGR DASHBOARD
# ===================================================================
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>GEX/VEX/VGR EXPOSURE DASHBOARD</span>", unsafe_allow_html=True)
# Use the SPX chain from Tab 2 if available, otherwise use synthetic
try:
chain_vv = fetch_cboe_spx_chain()
if chain_vv and chain_vv.get("records"):
spot_vv = chain_vv["spot"]
records_vv = chain_vv["records"]
else:
raise Exception("No CBOE data")
except:
# Use synthetic based on current VIX
spot_vv = spx_cur if spx_cur else 5450.0
vix_for_synth = vix_cur if vix_cur else 18.0
vvix_for_synth = vvix_cur if vvix_cur else 85.0
rng = np.random.default_rng(42)
records_vv = []
strikes = np.arange(spot_vv * 0.85, spot_vv * 1.15 + 25, 25)
atm_iv = vix_for_synth / 100.0
skew_factor = vvix_for_synth / 85.0
T = 30.0 / 365.0 # Default 30 DTE for synthetic
for k in strikes:
atm_dist = (k - spot_vv) / spot_vv
iv_c = max(0.03, atm_iv + abs(atm_dist) * 0.25 * skew_factor - atm_dist * 0.08 * skew_factor)
iv_p = max(0.03, iv_c + 0.015 + max(0.0, -atm_dist * 0.12 * skew_factor))
gamma = 0.004 * np.exp(-60 * atm_dist**2)
if T > 0 and iv_c > 0:
d1 = (np.log(spot_vv/k) + (0.5 * iv_c**2) * 30/365) / (iv_c * np.sqrt(30/365))
dc = float(np.clip(0.5 + d1 * 0.4, 0.01, 0.99))
else:
dc = 0.5 if k <= spot_vv else 0.01
oi_c = int(abs(rng.normal(6000, 2500)) * (np.exp(-25 * atm_dist**2) + 0.05))
oi_p = int(abs(rng.normal(7000, 3000)) * (np.exp(-25 * atm_dist**2) + 0.05) * 1.3)
records_vv.append({"strike": float(k), "oi_call": oi_c, "oi_put": oi_p,
"iv_c": round(iv_c, 4), "iv_p": round(iv_p, 4),
"gamma_c": round(gamma, 6), "gamma_p": round(gamma, 6),
"delta_call": round(dc, 4), "delta_put": round(dc - 1.0, 4),
"vanna_c": round(gamma * (1-dc) / max(iv_c, 0.01) * atm_dist, 4),
"vanna_p": round(gamma * (1-abs(dc-1)) / max(iv_p, 0.01) * atm_dist, 4)})
spot_vv = spot_vv # Use the synthetic spot
gex_vv = compute_gex_plus(records_vv, spot_vv)
vex_vv = compute_vanna_exposure(records_vv, spot_vv)
vgr_vv = abs(vex_vv) / abs(gex_vv) if abs(gex_vv) > 1e-6 else 0.0
zg_vv = find_zero_gamma(records_vv, spot_vv)
max_pain_vv, _ = compute_max_pain(records_vv, spot_vv)
gamma_walls_vv = compute_gamma_walls(records_vv, spot_vv)
dn_vv = compute_delta_neutral_strike(records_vv, spot_vv)
mcols_gex = st.columns(6)
with mcols_gex[0]: st.metric("Net GEX+", f"${gex_vv/1e9:.2f}B", "Long Gamma" if gex_vv > 0 else "Short Gamma")
with mcols_gex[1]: st.metric("VEX", f"${vex_vv/1e6:.1f}M")
with mcols_gex[2]: st.metric("VGR", f"{vgr_vv:.3f}", "Vanna Dominant" if vgr_vv > 1 else "Gamma Dominant")
with mcols_gex[3]: st.metric("Zero Gamma", f"{zg_vv:,.0f}", f"{(zg_vv/spot_vv-1)*100:+.1f}%")
with mcols_gex[4]: st.metric("Max Pain", f"{max_pain_vv:,.0f}" if max_pain_vv else "N/A", f"{(max_pain_vv/spot_vv-1)*100:+.1f}%" if max_pain_vv else None)
with mcols_gex[5]: st.metric("Delta Neutral", f"{dn_vv:,.0f}" if dn_vv else "N/A", f"{(dn_vv/spot_vv-1)*100:+.1f}%" if dn_vv else None)
if gamma_walls_vv:
wall_str = " | ".join([f"{w['strike']:,.0f} (${w['gex']/1e9:.1f}B)" for w in gamma_walls_vv[:4]])
st.caption(f"Top Gamma Walls: {wall_str}")
c1, c2 = st.columns(2)
with c1:
st.pyplot(chart_gex_vex_vgr(records_vv, spot_vv))
with c2:
profile = compute_crash_profile(records_vv, spot_vv)
st.pyplot(chart_gex_profile(profile, spot_vv, zg_vv))
# ==============================================================================
# TAB 4: MARKETGUARDIAN PRO
# ==============================================================================
with tabs[3]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>MARKETGUARDIAN PRO -- Market Stress Early Warning System</span>", unsafe_allow_html=True)
try:
html_path = pathlib.Path(__file__).parent / "marketguardian_pro.html"
if html_path.exists():
with open(html_path, "r", encoding="utf-8") as f:
html_content = f.read()
vd_mg = fetch_yahoo()
vs_mg = _s(vd_mg.get("VIX"))
vvs_mg = _s(vd_mg.get("VVIX"))
vix_val_mg = f"{vs_mg.iloc[-1]:.1f}" if vs_mg is not None and len(vs_mg)>0 else "N/A"
vvix_val_mg = f"{vvs_mg.iloc[-1]:.1f}" if vvs_mg is not None and len(vvs_mg)>0 else "N/A"
now_str = datetime.utcnow().strftime("%d.%m.%Y, %H:%M:%S")
html_content = html_content.replace(
'Letzte Aktualisierung: <span id="last-update">25.10.2025, 21:42:24</span>',
f'Letzte Aktualisierung: <span id="last-update">{now_str}</span>')
for old in ['>18.5 <span class="status-indicator status-green"></span>']:
html_content = html_content.replace(old, f'>{vix_val_mg} <span class="status-indicator status-green"></span>', 1)
for old in ['>95.3 <span class="status-indicator status-orange"></span>']:
html_content = html_content.replace(old, f'>{vvix_val_mg} <span class="status-indicator status-orange"></span>', 1)
st.components.v1.html(html_content, height=900, scrolling=True)
else:
st.error("marketguardian_pro.html not found")
except Exception as e:
st.error(f"MarketGuardian Pro error: {e}")
# ==============================================================================
# TAB 5: CRYPTO ULTRA
# ==============================================================================
with tabs[4]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>CRYPTO ULTRA -- Deribit + CoinGecko</span>", unsafe_allow_html=True)
with st.spinner("Loading crypto data..."):
deribit = fetch_deribit()
cg = fetch_coingecko()
fg_c = fetch_fear_greed()
if fg_c:
fg_val_c = fg_c.get("value",0)
fg_label_c = fg_c.get("label","Unknown")
fg_color_c = "#00ff88" if fg_val_c>60 else "#ffaa00" if fg_val_c>40 else "#ff6600" if fg_val_c>20 else "#ff4444"
st.markdown(f"""<div style="background:#1a2332;border:1px solid {fg_color_c};border-radius:4px;padding:10px 16px;margin:8px 0">
<span style="color:{fg_color_c};font-size:1.1rem;font-weight:700;font-family:monospace">{fg_val_c}</span>
<span style="color:{fg_color_c};font-size:0.8rem;margin-left:8px;font-family:sans-serif">{fg_label_c}</span>
</div>""", unsafe_allow_html=True)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>PERPETUAL FUTURES (DERIBIT)</span>", unsafe_allow_html=True)
pc = st.columns(4)
for idx, sym in enumerate(["btc","eth"]):
if sym in deribit["perpetuals"]:
p = deribit["perpetuals"][sym]
with pc[idx*2]:
st.metric(f"{sym.upper()}-PERP", fmt_price(p.get("last")), f"{p.get('change_pct',0):+.2f}%")
with pc[idx*2+1]:
st.markdown(f"<span style='color:#8899aa;font-size:0.75rem;font-family:monospace'>Mark: {fmt_price(p.get('mark'))} | OI: {p.get('oi',0):,.0f}</span>", unsafe_allow_html=True)
st.markdown(f"<span style='color:#8899aa;font-size:0.75rem;font-family:monospace'>Funding: {p.get('funding_8h',0):.8f}</span>", unsafe_allow_html=True)
for sym in ["btc","eth"]:
if sym in deribit.get("orderbooks",{}):
st.markdown(f"<span style='color:#8899aa;font-size:0.75rem;font-family:sans-serif'>{sym.upper()} Order Book</span>", unsafe_allow_html=True)
st.pyplot(chart_order_book(deribit["orderbooks"][sym], sym))
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>COINGECKO TOP 20</span>", unsafe_allow_html=True)
if cg:
cg_data = []
for sym_c, c in list(cg.items())[:20]:
cg_data.append({
"Rank":c.get("rank",""),"Symbol":sym_c,"Name":c.get("name",""),
"Price":fmt_price(c.get("price")),"1h":fmt_pct(c.get("chg_1h")),
"24h":fmt_pct(c.get("chg_24h")),"7d":fmt_pct(c.get("chg_7d")),
"Mkt Cap":f"${c.get('mkt_cap',0)/1e9:.1f}B" if c.get('mkt_cap') else "N/A"
})
st.dataframe(pd.DataFrame(cg_data), use_container_width=True, hide_index=True, height=400)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>7-DAY SPARKLINES</span>", unsafe_allow_html=True)
spark_cols = st.columns(5)
for i, sym_s in enumerate(["BTC","ETH","SOL","BNB","XRP"]):
if sym_s in cg and cg[sym_s].get("sparkline"):
with spark_cols[i]:
prices_sp = cg[sym_s]["sparkline"]
if prices_sp:
st.markdown(f"<span style='color:#8899aa;font-size:0.7rem;font-family:sans-serif'>{sym_s} -- {fmt_price(cg[sym_s]['price'])}</span>", unsafe_allow_html=True)
st.pyplot(chart_sparkline(prices_sp, sym_s, cg[sym_s]["price"]))
# ==============================================================================
# TAB 6: INSIDER TRADES
# ==============================================================================
with tabs[5]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>CONGRESSIONAL INSIDER TRADES -- CongressInvests</span>", unsafe_allow_html=True)
with st.spinner("Loading..."):
congress = fetch_congress()
h = congress.get("health",{})
if h:
st.caption(f"API: {h.get('status','?')} | Tickers: {h.get('tickers',0)} | Updated: {h.get('last_updated','?')}")
trades = congress.get("trades",[])
if trades:
st.markdown(f"<span style='color:#c8d6e5;font-family:monospace;font-size:0.85rem'>{len(trades)} recent trades</span>", unsafe_allow_html=True)
sc = st.columns(4)
with sc[0]: st.metric("Total", len(trades))
with sc[1]: st.metric("Buyers", sum(1 for t in trades if t.get("transaction_type","").lower() in ["buy","purchase"]))
with sc[2]: st.metric("Sellers", sum(1 for t in trades if t.get("transaction_type","").lower()=="sell"))
with sc[3]: st.metric("Unique Tickers", len(set(t.get("ticker","") for t in trades if t.get("ticker"))))
td = [{"Date":t.get("transaction_date",""),"Politician":t.get("member",""),"Party":t.get("party",""),"Chamber":t.get("chamber",""),"Ticker":t.get("ticker",""),"Type":t.get("transaction_type",""),"Amount":t.get("amount","")} for t in trades]
st.dataframe(pd.DataFrame(td), use_container_width=True, height=500)
else:
st.info("CongressInvests data unavailable")
# ==============================================================================
# TAB 7: SETTINGS
# ==============================================================================
with tabs[6]:
st.markdown("<span style='color:#00d4ff;font-size:0.85rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>SETTINGS &amp; API REFERENCE</span>", unsafe_allow_html=True)
demo_str = '1' if DEMO_MODE else '0'
fred_str = '***' if os.environ.get('FRED_API_KEY') else 'NOT SET'
st.code(f"DEMO_MODE={demo_str}\nFRED_API_KEY={fred_str}")
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>API KEY REFERENCE</span>", unsafe_allow_html=True)
st.markdown("| Key | Provider | Required |\n|-----|----------|----------|\n| `FRED_API_KEY` | FRED (Fed) | Optional |\n| `ALPHA_VANTAGE_API_KEY` | Alpha Vantage | Optional |\n| `COINGECKO_API_KEY` | CoinGecko | Optional |\n| `DERIBIT_API_KEY` | Deribit | Not needed |\n| `BINGX_API_KEY` | BingX | Not needed |")
st.markdown("<div style='height:1px;background:#2a3a5a;margin:12px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#00d4ff;font-size:0.8rem;font-weight:700;letter-spacing:0.04em;font-family:sans-serif'>ASSET VOLATILITY MAPPING</span>", unsafe_allow_html=True)
map_rows = []
for asset, info in ASSET_VOLA_MAP.items():
map_rows.append({"Asset": asset, "Name": info["name"], "Vola Index": info["vola"]})
st.dataframe(pd.DataFrame(map_rows), use_container_width=True, hide_index=True)
st.markdown("<div style='height:1px;background:#2a3a5a;margin:16px 0 8px 0'></div>", unsafe_allow_html=True)
st.markdown("<span style='color:#3a4a5a;font-size:0.65rem;font-family:monospace'>KRUPP CAPITAL | Quantitative Desk | Precision in Chaos, Alpha in Variance</span>", unsafe_allow_html=True)