Vol Surface: HV-vs-IV card as rowspan-2 to the right of toolbar

Wrap the toolbar card and the skew-metric badges (ATM IV / RR25 /
Fly25) in a left column, and place the HV-vs-IV card in a sibling
right column with h-100 d-flex flex-column so it stretches to the
full height of both stacked items — effectively a rowspan=2 layout.

On screens narrower than lg the right column drops below as a
single full-width card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ojy
2026-05-13 08:00:32 +00:00
parent f8aa3cdaae
commit d89ad179f3

View File

@@ -353,135 +353,141 @@
<div class="page-body">
<div class="container-xl">
<!-- Toolbar -->
<div class="card mb-4" style="background:#161824; border:1px solid #2d3045;">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-sm-auto">
<label class="form-label text-secondary" for="symbolInput">Symbol</label>
<div class="input-group">
<input
id="symbolInput"
type="text"
class="form-control"
style="background:#1e2030; border-color:#2d3045; color:#fff; width:100px; text-transform:uppercase;"
placeholder="SPY"
x-model="symbol"
@keydown.enter="fetchExpirations()"
@input="symbol = symbol.toUpperCase()"
:disabled="loading"
aria-label="Ticker symbol"
>
<button
class="btn btn-secondary"
@click="fetchExpirations()"
:disabled="lookingUp || !symbol"
aria-label="Look up expirations for symbol"
>
<span x-show="lookingUp" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
<span x-text="lookingUp ? '…' : 'Lookup'"></span>
</button>
</div>
</div>
<div class="col-12 col-sm-auto">
<label class="form-label text-secondary" for="expirySelect">Expiry</label>
<select
id="expirySelect"
class="form-select"
style="background:#1e2030; border-color:#2d3045; color:#fff; min-width:160px;"
x-model="expiry"
:disabled="loading || expirations.length === 0"
aria-label="Select expiry date"
>
<option value="" disabled>Select expiry…</option>
<template x-for="exp in expirations" :key="exp">
<option :value="exp" x-text="exp"></option>
</template>
</select>
</div>
<div class="col-12 col-sm-auto">
<button
class="btn btn-primary"
@click="loadSurface()"
:disabled="loading || !symbol || !expiry"
aria-label="Load volatility surface"
>
<span x-show="loading" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span x-text="loading ? 'Loading…' : 'Load Surface'"></span>
</button>
</div>
<div class="col-12 col-sm-auto" x-show="errorMsg" x-cloak>
<div class="alert alert-danger mb-0 py-2 px-3" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" class="me-1">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span x-text="errorMsg"></span>
</div>
</div>
<div class="col-12 col-sm-auto ms-sm-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">&nbsp;</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>
<!-- Top section: toolbar + skew badges on the left, HV/IV card (rowspan-2 style) on the right -->
<div class="row g-3 mb-4 align-items-stretch">
<div class="col-12 col-lg">
<!-- Toolbar -->
<div class="card mb-3" style="background:#161824; border:1px solid #2d3045;">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-sm-auto">
<label class="form-label text-secondary" for="symbolInput">Symbol</label>
<div class="input-group">
<input
id="symbolInput"
type="text"
class="form-control"
style="background:#1e2030; border-color:#2d3045; color:#fff; width:100px; text-transform:uppercase;"
placeholder="SPY"
x-model="symbol"
@keydown.enter="fetchExpirations()"
@input="symbol = symbol.toUpperCase()"
:disabled="loading"
aria-label="Ticker symbol"
>
<button
class="btn btn-secondary"
@click="fetchExpirations()"
:disabled="lookingUp || !symbol"
aria-label="Look up expirations for symbol"
>
<span x-show="lookingUp" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
<span x-text="lookingUp ? '…' : 'Lookup'"></span>
</button>
</div>
</template>
<div class="hviv-foot" x-text="hvIvFooter"></div>
</div>
<div class="col-12 col-sm-auto">
<label class="form-label text-secondary" for="expirySelect">Expiry</label>
<select
id="expirySelect"
class="form-select"
style="background:#1e2030; border-color:#2d3045; color:#fff; min-width:160px;"
x-model="expiry"
:disabled="loading || expirations.length === 0"
aria-label="Select expiry date"
>
<option value="" disabled>Select expiry…</option>
<template x-for="exp in expirations" :key="exp">
<option :value="exp" x-text="exp"></option>
</template>
</select>
</div>
<div class="col-12 col-sm-auto">
<button
class="btn btn-primary"
@click="loadSurface()"
:disabled="loading || !symbol || !expiry"
aria-label="Load volatility surface"
>
<span x-show="loading" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span x-text="loading ? 'Loading…' : 'Load Surface'"></span>
</button>
</div>
<div class="col-12 col-sm-auto ms-sm-auto" x-show="errorMsg" x-cloak>
<div class="alert alert-danger mb-0 py-2 px-3" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" class="me-1">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span x-text="errorMsg"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Skew metric badges -->
<div x-show="hasData" x-cloak>
<div class="d-flex flex-wrap gap-3">
<div class="stat-inline">
<span class="stat-label">ATM IV</span>
<span class="stat-value" x-text="formatPct(currentMetrics.atmIV)"></span>
</div>
<div class="stat-inline">
<span class="stat-label">RR25</span>
<span
class="stat-value"
:class="{
'positive': currentMetrics.rr25 > 0.005,
'negative': currentMetrics.rr25 < -0.005
}"
x-text="formatPctSigned(currentMetrics.rr25)"
></span>
</div>
<div class="stat-inline">
<span class="stat-label">Fly25</span>
<span
class="stat-value"
:class="{ 'amber': Math.abs(currentMetrics.fly25) > 0.002 }"
x-text="formatPctSigned(currentMetrics.fly25)"
></span>
</div>
</div>
</div>
</div>
</div>
<!-- Skew metric badges -->
<div class="mb-4" x-show="hasData" x-cloak>
<div class="d-flex flex-wrap gap-3">
<div class="stat-inline">
<span class="stat-label">ATM IV</span>
<span class="stat-value" x-text="formatPct(currentMetrics.atmIV)"></span>
<!-- HV vs IV card — spans full height of left column (toolbar + skew badges) -->
<div class="col-12 col-lg-auto" x-show="hasData" x-cloak>
<div class="hviv-card h-100 d-flex flex-column">
<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">&nbsp;</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 mt-auto" x-text="hvIvFooter"></div>
</div>
<div class="stat-inline">
<span class="stat-label">RR25</span>
<span
class="stat-value"
:class="{
'positive': currentMetrics.rr25 > 0.005,
'negative': currentMetrics.rr25 < -0.005
}"
x-text="formatPctSigned(currentMetrics.rr25)"
></span>
</div>
<div class="stat-inline">
<span class="stat-label">Fly25</span>
<span
class="stat-value"
:class="{ 'amber': Math.abs(currentMetrics.fly25) > 0.002 }"
x-text="formatPctSigned(currentMetrics.fly25)"
></span>
</div>
</div>
</div>