Scanner: spike = current ATM IV >= +30% above recent baseline
Replaces the IV/HV >= 1.5 heuristic with a baseline-relative jump: a symbol is flagged SPIKE when current ATM IV is at least 30% above its recent baseline (e.g., 10% -> 13%+). Baseline prefers the 30-day average of saved snapshots; falls back to HV30 when no scan history exists. Each scan persists a snapshot so the baseline self-improves over time. BIG MOVE (|chg%| >= 3%) is now shown as a separate badge instead of a spike requirement. - snapshots.ts: add getAverageAtmIv(symbol, days) - datafetch.ts: save snapshot per scan; compute baselineIv, baselineSrc, baselineN, ivJumpPct; spike from jump threshold - options.ts: /api/scan returns new baseline + jump fields - scanner.html: header copy, table columns (Baseline IV, IV Δ%), default sort by ivJumpPct desc, separate SPIKE/BIG MOVE badges Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,8 +66,11 @@
|
||||
<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>.
|
||||
A <strong>spike</strong> is flagged when current ATM IV is at least <span class="mono">+30%</span>
|
||||
above the symbol's recent baseline — the average ATM IV of the last 30 days from scan history,
|
||||
falling back to 30-day realized vol when no history exists. Each scan saves a snapshot so the
|
||||
baseline gets better the more you run it. The yellow <strong>BIG MOVE</strong> badge is a
|
||||
separate flag for |today Δ| ≥ 3%.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,9 +129,10 @@
|
||||
<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('baselineIv')" class="text-end">Baseline IV</th>
|
||||
<th @click="setSort('ivJumpPct')" class="text-end">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 class="text-center">Flags</th>
|
||||
<th>Expiry</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -143,9 +147,16 @@
|
||||
<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">
|
||||
<span :class="ivClass(r.baselineIv)" x-text="r.baselineIv ? (r.baselineIv*100).toFixed(1) + '%' : '—'"></span>
|
||||
<span class="text-secondary small ms-1" :title="r.baselineSrc === 'history' ? ('avg of ' + r.baselineN + ' scan snapshots in last 30d') : (r.baselineSrc === 'hv30' ? '30-day realized vol (no scan history yet)' : 'no baseline')" x-text="r.baselineSrc === 'history' ? ('n=' + r.baselineN) : (r.baselineSrc === 'hv30' ? 'hv' : '')"></span>
|
||||
</td>
|
||||
<td class="text-end mono fw-bold" :style="r.ivJumpPct >= 0.30 ? 'color:#ffd43b' : (r.ivJumpPct >= 0 ? 'color:#d0d5e0' : 'color:#8b95a7')" x-text="r.baselineIv ? ((r.ivJumpPct >= 0 ? '+' : '') + (r.ivJumpPct * 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="text-center">
|
||||
<span x-show="r.spike" class="badge me-1" style="background:#ffd43b;color:#1a1c2e;font-weight:700;">SPIKE</span>
|
||||
<span x-show="r.bigMove" class="badge" style="background:#4d9ef7;color:#fff;font-weight:700;">BIG MOVE</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>
|
||||
@@ -185,7 +196,7 @@
|
||||
watchlist: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
sortBy: 'ivHv',
|
||||
sortBy: 'ivJumpPct',
|
||||
sortDesc: true,
|
||||
|
||||
async init() {
|
||||
@@ -246,8 +257,8 @@
|
||||
|
||||
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) + '×';
|
||||
const r = [...this.results].sort((a, b) => b.ivJumpPct - a.ivJumpPct)[0];
|
||||
return r.symbol + ' ' + (r.ivJumpPct >= 0 ? '+' : '') + (r.ivJumpPct*100).toFixed(0) + '%';
|
||||
},
|
||||
|
||||
ivClass(iv) {
|
||||
|
||||
Reference in New Issue
Block a user