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="page-body">
<div class="container-xl"> <div class="container-xl">
<!-- Toolbar --> <!-- Top section: toolbar + skew badges on the left, HV/IV card (rowspan-2 style) on the right -->
<div class="card mb-4" style="background:#161824; border:1px solid #2d3045;"> <div class="row g-3 mb-4 align-items-stretch">
<div class="card-body"> <div class="col-12 col-lg">
<div class="row g-3 align-items-end"> <!-- Toolbar -->
<div class="col-12 col-sm-auto"> <div class="card mb-3" style="background:#161824; border:1px solid #2d3045;">
<label class="form-label text-secondary" for="symbolInput">Symbol</label> <div class="card-body">
<div class="input-group"> <div class="row g-3 align-items-end">
<input <div class="col-12 col-sm-auto">
id="symbolInput" <label class="form-label text-secondary" for="symbolInput">Symbol</label>
type="text" <div class="input-group">
class="form-control" <input
style="background:#1e2030; border-color:#2d3045; color:#fff; width:100px; text-transform:uppercase;" id="symbolInput"
placeholder="SPY" type="text"
x-model="symbol" class="form-control"
@keydown.enter="fetchExpirations()" style="background:#1e2030; border-color:#2d3045; color:#fff; width:100px; text-transform:uppercase;"
@input="symbol = symbol.toUpperCase()" placeholder="SPY"
:disabled="loading" x-model="symbol"
aria-label="Ticker symbol" @keydown.enter="fetchExpirations()"
> @input="symbol = symbol.toUpperCase()"
<button :disabled="loading"
class="btn btn-secondary" aria-label="Ticker symbol"
@click="fetchExpirations()" >
:disabled="lookingUp || !symbol" <button
aria-label="Look up expirations for symbol" class="btn btn-secondary"
> @click="fetchExpirations()"
<span x-show="lookingUp" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> :disabled="lookingUp || !symbol"
<span x-text="lookingUp ? '…' : 'Lookup'"></span> aria-label="Look up expirations for symbol"
</button> >
</div> <span x-show="lookingUp" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
</div> <span x-text="lookingUp ? '…' : 'Lookup'"></span>
</button>
<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>
</div> </div>
</template> </div>
<div class="hviv-foot" x-text="hvIvFooter"></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> </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>
</div>
<!-- Skew metric badges --> <!-- HV vs IV card — spans full height of left column (toolbar + skew badges) -->
<div class="mb-4" x-show="hasData" x-cloak> <div class="col-12 col-lg-auto" x-show="hasData" x-cloak>
<div class="d-flex flex-wrap gap-3"> <div class="hviv-card h-100 d-flex flex-column">
<div class="hviv-head">
<div class="stat-inline"> <span class="hviv-title">HV vs IV</span>
<span class="stat-label">ATM IV</span> <span class="hviv-badge" :class="hvIvVerdict.cls" x-text="hvIvVerdict.label"></span>
<span class="stat-value" x-text="formatPct(currentMetrics.atmIV)"></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>
<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>