Vol Surface: top-right HV-vs-IV comparison card
Adds a compact card in the page header that shows ATM IV alongside realized vol over 20/30/60-day windows, the IV-minus-HV spread in vol points, and a RICH/FAIR/CHEAP verdict (driven by IV/HV30 ratio: >=1.20x = RICH, <=0.80x = CHEAP, otherwise FAIR). Lets you eyeball whether options are priced rich relative to recent realized vol the moment the surface loads. - datafetch.ts: extract annualizedVolWindow helper; new fetchHistoricalVolWindows() returns hv20/hv30/hv60 from one ~90-day Yahoo historical pull - options.ts: /api/analytics includes hvWindows in response - surface.html: top-right hviv-card with per-window rows + footer showing IV/HV ratio and sample size Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,73 @@
|
||||
color: #ffd43b;
|
||||
}
|
||||
|
||||
/* HV vs IV comparison card (top-right of page header) */
|
||||
.hviv-card {
|
||||
background: #161824;
|
||||
border: 1px solid #2d3045;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
min-width: 280px;
|
||||
}
|
||||
.hviv-card .hviv-head {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
border-bottom: 1px solid #2d3045;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.hviv-card .hviv-title {
|
||||
color: #8b95a7;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.hviv-card .hviv-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.hviv-card .hviv-badge.rich { background:#ff6b6b; color:#1a1c2e; }
|
||||
.hviv-card .hviv-badge.cheap { background:#51cf66; color:#1a1c2e; }
|
||||
.hviv-card .hviv-badge.fair { background:#374151; color:#cbd3df; }
|
||||
.hviv-card .hviv-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr auto;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
padding: 0.15rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.hviv-card .hviv-row .label {
|
||||
color: #8b95a7;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.hviv-card .hviv-row .val {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono','Fira Code',monospace;
|
||||
}
|
||||
.hviv-card .hviv-row .spread {
|
||||
font-family: 'JetBrains Mono','Fira Code',monospace;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hviv-card .hviv-row .spread.positive { color: #ff6b6b; }
|
||||
.hviv-card .hviv-row .spread.negative { color: #51cf66; }
|
||||
.hviv-card .hviv-row.atm .val { color: #ffd43b; }
|
||||
.hviv-card .hviv-foot {
|
||||
color: #6c757d;
|
||||
font-size: 0.7rem;
|
||||
margin-top: 0.25rem;
|
||||
border-top: 1px dashed #2d3045;
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -279,6 +346,29 @@
|
||||
<h2 class="page-title">Volatility Surface</h2>
|
||||
<div class="text-secondary mt-1">IV skew analysis and term structure</div>
|
||||
</div>
|
||||
<div class="col-auto" x-show="hasData" x-cloak>
|
||||
<div class="hviv-card">
|
||||
<div class="hviv-head">
|
||||
<span class="hviv-title">HV vs IV</span>
|
||||
<span class="hviv-badge" :class="hvIvVerdict.cls" x-text="hvIvVerdict.label"></span>
|
||||
</div>
|
||||
<div class="hviv-row atm">
|
||||
<span class="label">ATM IV</span>
|
||||
<span class="val" x-text="formatPct(currentMetrics.atmIV)"></span>
|
||||
<span class="spread"> </span>
|
||||
</div>
|
||||
<template x-for="w in hvRows" :key="w.key">
|
||||
<div class="hviv-row">
|
||||
<span class="label" x-text="w.label"></span>
|
||||
<span class="val" x-text="w.value > 0 ? formatPct(w.value) : '—'"></span>
|
||||
<span class="spread"
|
||||
:class="w.value > 0 ? (w.spread >= 0 ? 'positive' : 'negative') : ''"
|
||||
x-text="w.value > 0 ? ((w.spread >= 0 ? '+' : '') + (w.spread * 100).toFixed(1) + ' pts') : ''"></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="hviv-foot" x-text="hvIvFooter"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -655,6 +745,7 @@
|
||||
errorMsg: '',
|
||||
skewTable: [],
|
||||
currentMetrics: { atmIV: 0, rr25: 0, fly25: 0 },
|
||||
hvWindows: { hv20: 0, hv30: 0, hv60: 0, samples: { hv20: 0, hv30: 0, hv60: 0 } },
|
||||
greeks: { atmCall: null, atmPut: null, itmCall: null, itmPut: null },
|
||||
skewChartInstance: null,
|
||||
termChartInstance: null,
|
||||
@@ -753,6 +844,7 @@
|
||||
});
|
||||
|
||||
this.greeks = data.greeks || { atmCall: null, atmPut: null, itmCall: null, itmPut: null };
|
||||
this.hvWindows = data.hvWindows || { hv20: 0, hv30: 0, hv60: 0, samples: { hv20: 0, hv30: 0, hv60: 0 } };
|
||||
this.hasData = true;
|
||||
|
||||
// Wait a tick for x-show to render the divs
|
||||
@@ -916,6 +1008,36 @@
|
||||
return v >= 0 ? `+${pct}%` : `${pct}%`;
|
||||
},
|
||||
|
||||
// HV vs IV comparison — rows for HV20/30/60 with IV-minus-HV spread (in vol points)
|
||||
get hvRows() {
|
||||
const iv = this.currentMetrics.atmIV || 0;
|
||||
const w = this.hvWindows || {};
|
||||
return [
|
||||
{ key: 'hv20', label: 'HV20', value: w.hv20 || 0, spread: iv - (w.hv20 || 0) },
|
||||
{ key: 'hv30', label: 'HV30', value: w.hv30 || 0, spread: iv - (w.hv30 || 0) },
|
||||
{ key: 'hv60', label: 'HV60', value: w.hv60 || 0, spread: iv - (w.hv60 || 0) },
|
||||
];
|
||||
},
|
||||
|
||||
// Verdict: ATM IV vs HV30 — IV >20% above HV30 = RICH, >20% below = CHEAP, else FAIR.
|
||||
get hvIvVerdict() {
|
||||
const iv = this.currentMetrics.atmIV || 0;
|
||||
const hv = this.hvWindows?.hv30 || 0;
|
||||
if (!iv || !hv) return { label: 'N/A', cls: 'fair' };
|
||||
const ratio = iv / hv;
|
||||
if (ratio >= 1.20) return { label: 'RICH', cls: 'rich' };
|
||||
if (ratio <= 0.80) return { label: 'CHEAP', cls: 'cheap' };
|
||||
return { label: 'FAIR', cls: 'fair' };
|
||||
},
|
||||
|
||||
get hvIvFooter() {
|
||||
const iv = this.currentMetrics.atmIV || 0;
|
||||
const hv = this.hvWindows?.hv30 || 0;
|
||||
if (!iv || !hv) return 'IV/HV — n/a';
|
||||
const ratio = (iv / hv).toFixed(2);
|
||||
return `IV/HV ratio ${ratio}× · HV30 over ${this.hvWindows?.samples?.hv30 ?? 0}d`;
|
||||
},
|
||||
|
||||
// Demo data for development / when API is unavailable
|
||||
_demoExpirations() {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user