Files
options-pricer/frontend/scanner.html
ojy 2cd3d6ece8 IV Spike Scanner
Backend: GET /api/scan?symbols=SYM1,SYM2,... — for each symbol fetches
the front-expiry options chain plus 30-day realized vol and returns
{ spot, change, changePct, atmIv, hv30, ivHv, spike, expiry }. Spike flag
is on when IV/HV ≥ 1.5 or |today's % change| ≥ 3. Defaults to ~15 popular
tickers when no list is given; cap of 30 symbols/scan.

Frontend: new scanner.html page — symbol input (with "Use defaults" / "Use
watchlist" shortcuts), summary cards (count · spikes · biggest mover ·
highest IV/HV), sortable results table with spike rows highlighted, and
shortcut buttons to open each symbol on Chain or Surface.

Scanner added to all sidebars between Vol Surface and Strategy P/L.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:32:14 +00:00

269 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IV Spike Scanner — Options Pricer</title>
<link rel="stylesheet" href="/assets/tabler.min.css" />
<link rel="stylesheet" href="/assets/tabler-vendors.min.css" />
<style>
.mono { font-family:'JetBrains Mono','Fira Code',monospace; }
[x-cloak] { display:none !important; }
.scan-table th { background:#1a1c2e; color:#8b95a7; font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid #2d3045; cursor:pointer; user-select:none; }
.scan-table th:hover { color:#cbd3df; }
.scan-table td { border-bottom:1px solid #1e2030; color:#d0d5e0; font-size:.88rem; vertical-align:middle; }
.scan-table tbody tr.spike { background: rgba(255, 212, 59, 0.06); }
.scan-table tbody tr:hover td { background: rgba(255,255,255,0.03); }
.iv-cell-low { color: #51cf66; }
.iv-cell-mid { color: #ffd43b; }
.iv-cell-high { color: #ff8c42; }
.iv-cell-vhigh{ color: #ff6b6b; }
</style>
</head>
<body class="antialiased">
<div class="wrapper" x-data="scannerApp()" x-init="init()">
<!-- Sidebar -->
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark">
<a href="index.html" class="d-flex align-items-center gap-2 text-decoration-none">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-primary" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="4" y="8" width="4" height="8" rx="1"/>
<line x1="6" y1="4" x2="6" y2="8"/>
<line x1="6" y1="16" x2="6" y2="20"/>
<rect x="16" y="6" width="4" height="10" rx="1"/>
<line x1="18" y1="2" x2="18" y2="6"/>
<line x1="18" y1="16" x2="18" y2="22"/>
</svg>
<span class="fw-bold">Options Pricer</span>
</a>
</h1>
<div class="collapse navbar-collapse" id="sidebar-menu">
<ul class="navbar-nav pt-lg-3">
<li class="nav-item"><a class="nav-link" href="index.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="6" height="5" rx="2"/><rect x="4" y="13" width="6" height="7" rx="2"/><rect x="14" y="4" width="6" height="11" rx="2"/><rect x="14" y="19" width="6" height="1" rx=".5"/></svg></span><span class="nav-link-title">Dashboard</span></a></li>
<li class="nav-item"><a class="nav-link" href="chain.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10l18 0"/><path d="M10 5v14"/></svg></span><span class="nav-link-title">Options Chain</span></a></li>
<li class="nav-item"><a class="nav-link" href="surface.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12c1.333 -4.667 2.667 -7 4 -7s2.667 2.333 4 7s2.667 7 4 7s2.667 -2.333 4 -7"/></svg></span><span class="nav-link-title">Vol Surface</span></a></li>
<li class="nav-item active"><a class="nav-link" href="scanner.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><path d="M11 8v6"/><path d="M8 11h6"/></svg></span><span class="nav-link-title">Scanner</span></a></li>
<li class="nav-item"><a class="nav-link" href="strategy.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19l4 -6l4 2l4 -8l4 5"/><path d="M4 4v16h16"/></svg></span><span class="nav-link-title">Strategy P/L</span></a></li>
<li class="nav-item"><a class="nav-link" href="positions.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7v-2a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v2"/><line x1="12" y1="12" x2="12" y2="12.01"/><path d="M3 13a20 20 0 0 0 18 0"/></svg></span><span class="nav-link-title">Positions</span></a></li>
<li class="nav-item"><a class="nav-link" href="tracker.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="9"/><path d="M15 12l-3 -3"/></svg></span><span class="nav-link-title">Tracker</span></a></li>
<li class="nav-item"><a class="nav-link" href="settings.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/></svg></span><span class="nav-link-title">Settings</span></a></li>
</ul>
</div>
</div>
</aside>
<!-- Page -->
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">IV Spike Scanner</h2>
<div class="text-secondary mt-1">
Find symbols where IV / HV ratio is unusually high or price is moving big today.
A <strong>spike</strong> is flagged when <span class="mono">IV / HV ≥ 1.5</span> or <span class="mono">|today Δ| ≥ 3%</span>.
</div>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<!-- Toolbar -->
<div class="card mb-3" style="background:#161824; border:1px solid #2d3045;">
<div class="card-body py-3">
<div class="row g-2 align-items-end">
<div class="col">
<label class="form-label small text-secondary" for="symInput">Symbols (comma-separated)</label>
<input id="symInput" type="text" class="form-control text-uppercase mono" x-model="symbolsRaw" placeholder="SPY,QQQ,AAPL,TSLA,..." @keydown.enter="scan()">
</div>
<div class="col-auto">
<button class="btn btn-outline-secondary" @click="useDefaults()" :disabled="loading">Use defaults</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-info" @click="useWatchlist()" :disabled="loading || watchlist.length === 0">
Use watchlist (<span x-text="watchlist.length"></span>)
</button>
</div>
<div class="col-auto">
<button class="btn btn-primary" @click="scan()" :disabled="loading">
<span x-show="loading" class="spinner-border spinner-border-sm me-1"></span>
<span x-text="loading ? 'Scanning…' : 'Scan'"></span>
</button>
</div>
</div>
<div class="text-secondary small mt-2" x-show="error" x-cloak x-text="error" style="color:#ff6b6b !important;"></div>
</div>
</div>
<!-- Spike summary cards -->
<div class="row g-2 mb-3" x-show="results.length > 0" x-cloak>
<div class="col-6 col-md-3"><div class="card" style="background:#1e2030;border:1px solid #2d3045;padding:.75rem 1rem;"><div class="text-secondary small">Scanned</div><div class="fs-4 fw-bold" x-text="results.length"></div></div></div>
<div class="col-6 col-md-3"><div class="card" style="background:#1e2030;border:1px solid #ffd43b66;padding:.75rem 1rem;"><div class="small" style="color:#ffd43b;">Spikes</div><div class="fs-4 fw-bold" style="color:#ffd43b;" x-text="results.filter(r => r.spike).length"></div></div></div>
<div class="col-6 col-md-3"><div class="card" style="background:#1e2030;border:1px solid #2d3045;padding:.75rem 1rem;"><div class="text-secondary small">Biggest mover</div><div class="fs-5 fw-bold mono" x-text="biggestMover"></div></div></div>
<div class="col-6 col-md-3"><div class="card" style="background:#1e2030;border:1px solid #2d3045;padding:.75rem 1rem;"><div class="text-secondary small">Highest IV/HV</div><div class="fs-5 fw-bold mono" x-text="highestRatio"></div></div></div>
</div>
<!-- Results table -->
<div class="card" x-show="results.length > 0" x-cloak style="background:#161824; border:1px solid #2d3045;">
<div class="card-header d-flex align-items-center justify-content-between" style="border-bottom:1px solid #2d3045;">
<h3 class="card-title text-white mb-0">Results <span class="text-secondary small fw-normal ms-2" x-text="'sorted by ' + sortBy + (sortDesc ? ' ↓' : ' ↑')"></span></h3>
<span class="text-secondary small">Click a column header to sort · click a symbol to open it</span>
</div>
<div class="table-responsive">
<table class="table table-sm scan-table mb-0">
<thead>
<tr>
<th @click="setSort('symbol')">Symbol</th>
<th @click="setSort('spot')" class="text-end">Spot</th>
<th @click="setSort('changePct')" class="text-end">Δ Today</th>
<th @click="setSort('atmIv')" class="text-end">ATM IV</th>
<th @click="setSort('hv30')" class="text-end">HV30</th>
<th @click="setSort('ivHv')" class="text-end">IV / HV</th>
<th class="text-center">Spike</th>
<th>Expiry</th>
<th></th>
</tr>
</thead>
<tbody>
<template x-for="r in sorted" :key="r.symbol">
<tr :class="r.spike ? 'spike' : ''">
<td class="mono fw-bold">
<span x-text="r.symbol"></span>
<span x-show="r.error" class="text-danger small ms-1" :title="r.error"></span>
</td>
<td class="text-end mono" x-text="r.spot ? '$' + r.spot.toFixed(2) : '—'"></td>
<td class="text-end mono fw-semibold" :class="r.changePct >= 0 ? 'text-success' : 'text-danger'" x-text="r.spot ? (r.changePct >= 0 ? '+' : '') + r.changePct.toFixed(2) + '%' : '—'"></td>
<td class="text-end mono" :class="ivClass(r.atmIv)" x-text="r.atmIv ? (r.atmIv*100).toFixed(1) + '%' : '—'"></td>
<td class="text-end mono" :class="ivClass(r.hv30)" x-text="r.hv30 ? (r.hv30*100).toFixed(1) + '%' : '—'"></td>
<td class="text-end mono fw-bold" :style="r.ivHv >= 1.5 ? 'color:#ffd43b' : 'color:#d0d5e0'" x-text="r.ivHv ? r.ivHv.toFixed(2) : '—'"></td>
<td class="text-center"><span x-show="r.spike" class="badge" style="background:#ffd43b;color:#1a1c2e;font-weight:700;">SPIKE</span></td>
<td class="mono small text-secondary" x-text="r.expiry"></td>
<td>
<a class="btn btn-sm btn-outline-secondary me-1" :href="'chain.html'" @click.prevent="goChain(r.symbol)" title="Open in Options Chain">Chain</a>
<a class="btn btn-sm btn-outline-primary" :href="'surface.html'" @click.prevent="goSurface(r.symbol)" title="Open in Vol Surface">Surface</a>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Empty state -->
<div x-show="!loading && results.length === 0" x-cloak class="card text-center py-5" style="background:#161824;border:1px solid #2d3045;">
<div class="card-body">
<h3 class="text-secondary">No scan run yet</h3>
<p class="text-muted">Hit <strong>Scan</strong> with the defaults, your Tracker watchlist, or a custom list.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/assets/tabler.min.js" defer></script>
<script src="/assets/viewstate-store.js"></script>
<script src="/assets/alpine.min.js" defer></script>
<script>
const DEFAULT_SYMBOLS = ["SPY","QQQ","IWM","DIA","AAPL","MSFT","NVDA","GOOGL","META","AMZN","TSLA","AMD","NFLX","COIN","INTC"];
function scannerApp() {
return {
symbolsRaw: DEFAULT_SYMBOLS.join(','),
results: [],
watchlist: [],
loading: false,
error: '',
sortBy: 'ivHv',
sortDesc: true,
async init() {
try { this.watchlist = JSON.parse(localStorage.getItem('optionsPricer:watchlist') || '[]'); } catch {}
const vs = ViewState.load('scanner');
if (vs) {
this.symbolsRaw = vs.symbolsRaw ?? this.symbolsRaw;
this.results = vs.results ?? [];
this.sortBy = vs.sortBy ?? this.sortBy;
this.sortDesc = vs.sortDesc !== undefined ? vs.sortDesc : true;
}
},
useDefaults() { this.symbolsRaw = DEFAULT_SYMBOLS.join(','); },
useWatchlist() { if (this.watchlist.length) this.symbolsRaw = this.watchlist.join(','); },
async scan() {
const syms = this.symbolsRaw.split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
if (syms.length === 0) { this.error = 'Add at least one symbol'; return; }
this.loading = true; this.error = '';
try {
const r = await fetch('/api/scan?symbols=' + encodeURIComponent(syms.join(',')));
const d = await r.json();
if (!r.ok || !d.ok) throw new Error(d.error || ('HTTP ' + r.status));
this.results = d.data.results || [];
this._persist();
} catch (e) {
this.error = 'Scan failed: ' + e.message;
} finally {
this.loading = false;
}
},
_persist() {
ViewState.save('scanner', { symbolsRaw: this.symbolsRaw, results: this.results, sortBy: this.sortBy, sortDesc: this.sortDesc });
},
setSort(col) {
if (this.sortBy === col) this.sortDesc = !this.sortDesc;
else { this.sortBy = col; this.sortDesc = true; }
this._persist();
},
get sorted() {
const dir = this.sortDesc ? -1 : 1;
return [...this.results].sort((a, b) => {
const av = a[this.sortBy], bv = b[this.sortBy];
if (typeof av === 'string') return av.localeCompare(bv) * dir;
return ((av ?? 0) - (bv ?? 0)) * dir;
});
},
get biggestMover() {
if (this.results.length === 0) return '—';
const r = [...this.results].sort((a, b) => Math.abs(b.changePct) - Math.abs(a.changePct))[0];
return r.symbol + ' ' + (r.changePct >= 0 ? '+' : '') + r.changePct.toFixed(2) + '%';
},
get highestRatio() {
if (this.results.length === 0) return '—';
const r = [...this.results].sort((a, b) => b.ivHv - a.ivHv)[0];
return r.symbol + ' ' + r.ivHv.toFixed(2) + '×';
},
ivClass(iv) {
if (!iv) return '';
const pct = iv * 100;
if (pct < 20) return 'iv-cell-low';
if (pct < 40) return 'iv-cell-mid';
if (pct < 80) return 'iv-cell-high';
return 'iv-cell-vhigh';
},
goChain(sym) { try { const v = ViewState.load('chain') || {}; v.symbol = sym; ViewState.save('chain', v); } catch {} window.location.href = '/chain.html'; },
goSurface(sym){ try { const v = ViewState.load('surface') || {}; v.symbol = sym; ViewState.save('surface', v); } catch {} window.location.href = '/surface.html'; },
};
}
</script>
</body>
</html>