New /movers page surfaces Yahoo Finance's predefined screeners (day_gainers, day_losers, most_actives, most_shorted_stocks) filtered to common equities with market cap >= $2B, so every listed name has a deep options chain. Per-row actions jump straight into Chain / Vol Surface / IV Spike Scanner, or pin the symbol to the Tracker watchlist. - datafetch.ts: fetchMovers(category, count) using yf.screener, post-filtered to quoteType=EQUITY and marketCap >= $2B - options.ts: GET /api/movers?category=&count= - movers.html: Tabler page with 4-tab segmented control, sortable table, summary cards, volume-vs-avg ratio highlighting hot names - All page sidebars: insert "Movers" link between Vol Surface and Scanner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
360 lines
22 KiB
HTML
360 lines
22 KiB
HTML
<!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>Top Movers — 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; }
|
|
.mv-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; }
|
|
.mv-table th:hover { color:#cbd3df; }
|
|
.mv-table td { border-bottom:1px solid #1e2030; color:#d0d5e0; font-size:.88rem; vertical-align:middle; }
|
|
.mv-table tbody tr:hover td { background: rgba(255,255,255,0.03); }
|
|
.mv-table tbody tr.active-row { background: rgba(0, 173, 181, 0.06); }
|
|
.cat-btn { font-weight:600; }
|
|
.cat-btn.active { background:#00adb5 !important; color:#0b1020 !important; border-color:#00adb5 !important; }
|
|
.cap-mega { color:#9d8cff; }
|
|
.cap-large { color:#4d9ef7; }
|
|
.cap-mid { color:#51cf66; }
|
|
.vol-hot { color:#ffd43b; font-weight:600; }
|
|
.truncate-name { max-width: 260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:middle; }
|
|
</style>
|
|
</head>
|
|
<body class="antialiased">
|
|
<div class="wrapper" x-data="moversApp()" 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="movers.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 17l6 -6l4 4l8 -8"/><path d="M14 7l7 0l0 7"/></svg></span><span class="nav-link-title">Movers</span></a></li>
|
|
<li class="nav-item"><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">Top Movers</h2>
|
|
<div class="text-secondary mt-1">
|
|
Yahoo Finance's live screener for the biggest market moves today, filtered to
|
|
<strong>mid-cap and above</strong> (market cap ≥ <span class="mono">$2B</span>) — small-caps
|
|
are excluded so every name listed has a deep, tradable options chain. Click any symbol to jump
|
|
straight into the chain, vol surface, or IV-spike scanner.
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button class="btn btn-primary" @click="refresh()" :disabled="loading">
|
|
<span x-show="loading" class="spinner-border spinner-border-sm me-1"></span>
|
|
<span x-text="loading ? 'Loading…' : 'Refresh'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-body">
|
|
<div class="container-xl">
|
|
|
|
<!-- Category tabs -->
|
|
<div class="card mb-3" style="background:#161824; border:1px solid #2d3045;">
|
|
<div class="card-body py-3">
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<template x-for="c in categories" :key="c.id">
|
|
<button
|
|
class="btn btn-outline-secondary cat-btn"
|
|
:class="category === c.id ? 'active' : ''"
|
|
@click="setCategory(c.id)"
|
|
:disabled="loading"
|
|
>
|
|
<span x-text="c.label"></span>
|
|
</button>
|
|
</template>
|
|
<div class="ms-auto small text-secondary">
|
|
<span x-show="lastUpdated" x-cloak>Updated <span class="mono" x-text="lastUpdatedDisplay"></span></span>
|
|
</div>
|
|
</div>
|
|
<div class="text-secondary small mt-2" x-show="error" x-cloak x-text="error" style="color:#ff6b6b !important;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary cards -->
|
|
<div class="row g-2 mb-3" x-show="rows.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">Showing</div><div class="fs-4 fw-bold" x-text="rows.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">Top mover</div><div class="fs-5 fw-bold mono" x-text="topMoverDisplay"></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">Most volume</div><div class="fs-5 fw-bold mono" x-text="topVolumeDisplay"></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">Largest cap</div><div class="fs-5 fw-bold mono" x-text="topCapDisplay"></div></div></div>
|
|
</div>
|
|
|
|
<!-- Results table -->
|
|
<div class="card" x-show="rows.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">
|
|
<span x-text="currentCategoryLabel"></span>
|
|
<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 mv-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th @click="setSort('symbol')">Symbol</th>
|
|
<th>Name</th>
|
|
<th @click="setSort('price')" class="text-end">Price</th>
|
|
<th @click="setSort('changePct')" class="text-end">Δ %</th>
|
|
<th @click="setSort('change')" class="text-end">Δ $</th>
|
|
<th @click="setSort('volume')" class="text-end">Volume</th>
|
|
<th @click="setSort('volRatio')" class="text-end">Vol / Avg</th>
|
|
<th @click="setSort('marketCap')" class="text-end">Mkt Cap</th>
|
|
<th>Exchange</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="r in sorted" :key="r.symbol">
|
|
<tr>
|
|
<td class="mono fw-bold">
|
|
<span x-text="r.symbol"></span>
|
|
</td>
|
|
<td class="text-secondary small">
|
|
<span class="truncate-name" :title="r.name" x-text="r.name || '—'"></span>
|
|
</td>
|
|
<td class="text-end mono" x-text="r.price ? '$' + r.price.toFixed(2) : '—'"></td>
|
|
<td class="text-end mono fw-semibold" :class="r.changePct >= 0 ? 'text-success' : 'text-danger'"
|
|
x-text="(r.changePct >= 0 ? '+' : '') + r.changePct.toFixed(2) + '%'"></td>
|
|
<td class="text-end mono" :class="r.change >= 0 ? 'text-success' : 'text-danger'"
|
|
x-text="(r.change >= 0 ? '+' : '') + r.change.toFixed(2)"></td>
|
|
<td class="text-end mono" x-text="fmtVol(r.volume)"></td>
|
|
<td class="text-end mono" :class="volRatio(r) >= 2 ? 'vol-hot' : ''"
|
|
x-text="r.avgVolume ? (volRatio(r)).toFixed(2) + 'x' : '—'"></td>
|
|
<td class="text-end mono" :class="capClass(r.marketCap)" x-text="fmtCap(r.marketCap)"></td>
|
|
<td class="text-secondary small" x-text="shortExchange(r.exchange)"></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-secondary me-1" @click="goChain(r.symbol)" title="Open in Options Chain">Chain</button>
|
|
<button class="btn btn-sm btn-outline-primary me-1" @click="goSurface(r.symbol)" title="Open in Vol Surface">Surface</button>
|
|
<button class="btn btn-sm btn-outline-warning me-1" @click="goScanner(r.symbol)" title="Run IV Spike Scanner">IV</button>
|
|
<button class="btn btn-sm btn-outline-success" @click="addToWatchlist(r.symbol)" :title="watchlist.includes(r.symbol) ? 'Already in watchlist' : 'Add to Tracker watchlist'"
|
|
:disabled="watchlist.includes(r.symbol)">
|
|
<span x-text="watchlist.includes(r.symbol) ? '★' : '☆'"></span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div x-show="!loading && rows.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 movers loaded yet</h3>
|
|
<p class="text-muted">Hit <strong>Refresh</strong> to pull the latest market data.</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>
|
|
function moversApp() {
|
|
return {
|
|
categories: [
|
|
{ id: 'day_gainers', label: 'Gainers' },
|
|
{ id: 'day_losers', label: 'Losers' },
|
|
{ id: 'most_actives', label: 'Most Active' },
|
|
{ id: 'most_shorted_stocks', label: 'Most Shorted' },
|
|
],
|
|
category: 'day_gainers',
|
|
rows: [],
|
|
watchlist: [],
|
|
loading: false,
|
|
error: '',
|
|
sortBy: 'changePct',
|
|
sortDesc: true,
|
|
lastUpdated: null,
|
|
|
|
async init() {
|
|
try { this.watchlist = JSON.parse(localStorage.getItem('optionsPricer:watchlist') || '[]'); } catch {}
|
|
const vs = ViewState.load('movers');
|
|
if (vs) {
|
|
this.category = vs.category ?? this.category;
|
|
this.rows = vs.rows ?? [];
|
|
this.sortBy = vs.sortBy ?? this.sortBy;
|
|
this.sortDesc = vs.sortDesc !== undefined ? vs.sortDesc : true;
|
|
this.lastUpdated = vs.lastUpdated ?? null;
|
|
}
|
|
// Auto-refresh on first open or if data is older than 5 min
|
|
const stale = !this.lastUpdated || (Date.now() - this.lastUpdated > 5 * 60 * 1000);
|
|
if (this.rows.length === 0 || stale) await this.refresh();
|
|
},
|
|
|
|
setCategory(id) {
|
|
if (this.category === id) return;
|
|
this.category = id;
|
|
// Default sort per-category: by changePct for gainers/losers, by volume for actives/shorted
|
|
if (id === 'most_actives' || id === 'most_shorted_stocks') {
|
|
this.sortBy = 'volume'; this.sortDesc = true;
|
|
} else {
|
|
this.sortBy = 'changePct';
|
|
this.sortDesc = (id === 'day_gainers');
|
|
}
|
|
this.refresh();
|
|
},
|
|
|
|
async refresh() {
|
|
this.loading = true; this.error = '';
|
|
try {
|
|
const r = await fetch('/api/movers?category=' + encodeURIComponent(this.category) + '&count=50');
|
|
const d = await r.json();
|
|
if (!r.ok || !d.ok) throw new Error(d.error || ('HTTP ' + r.status));
|
|
this.rows = d.data.results || [];
|
|
this.lastUpdated = Date.now();
|
|
this._persist();
|
|
} catch (e) {
|
|
this.error = 'Failed to load: ' + e.message;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
_persist() {
|
|
ViewState.save('movers', {
|
|
category: this.category, rows: this.rows,
|
|
sortBy: this.sortBy, sortDesc: this.sortDesc,
|
|
lastUpdated: this.lastUpdated,
|
|
});
|
|
},
|
|
|
|
setSort(col) {
|
|
if (this.sortBy === col) this.sortDesc = !this.sortDesc;
|
|
else { this.sortBy = col; this.sortDesc = true; }
|
|
this._persist();
|
|
},
|
|
|
|
volRatio(r) { return r.avgVolume > 0 ? r.volume / r.avgVolume : 0; },
|
|
|
|
get sorted() {
|
|
const dir = this.sortDesc ? -1 : 1;
|
|
return [...this.rows].sort((a, b) => {
|
|
let av, bv;
|
|
if (this.sortBy === 'volRatio') { av = this.volRatio(a); bv = this.volRatio(b); }
|
|
else { av = a[this.sortBy]; bv = b[this.sortBy]; }
|
|
if (typeof av === 'string') return av.localeCompare(bv) * dir;
|
|
return ((av ?? 0) - (bv ?? 0)) * dir;
|
|
});
|
|
},
|
|
|
|
get currentCategoryLabel() {
|
|
return this.categories.find(c => c.id === this.category)?.label ?? '';
|
|
},
|
|
|
|
get topMoverDisplay() {
|
|
if (this.rows.length === 0) return '—';
|
|
const r = [...this.rows].sort((a, b) => Math.abs(b.changePct) - Math.abs(a.changePct))[0];
|
|
return r.symbol + ' ' + (r.changePct >= 0 ? '+' : '') + r.changePct.toFixed(2) + '%';
|
|
},
|
|
|
|
get topVolumeDisplay() {
|
|
if (this.rows.length === 0) return '—';
|
|
const r = [...this.rows].sort((a, b) => b.volume - a.volume)[0];
|
|
return r.symbol + ' ' + this.fmtVol(r.volume);
|
|
},
|
|
|
|
get topCapDisplay() {
|
|
if (this.rows.length === 0) return '—';
|
|
const r = [...this.rows].sort((a, b) => b.marketCap - a.marketCap)[0];
|
|
return r.symbol + ' ' + this.fmtCap(r.marketCap);
|
|
},
|
|
|
|
get lastUpdatedDisplay() {
|
|
if (!this.lastUpdated) return '';
|
|
const d = new Date(this.lastUpdated);
|
|
return d.toLocaleTimeString();
|
|
},
|
|
|
|
fmtVol(v) {
|
|
if (!v) return '—';
|
|
if (v >= 1e9) return (v / 1e9).toFixed(2) + 'B';
|
|
if (v >= 1e6) return (v / 1e6).toFixed(2) + 'M';
|
|
if (v >= 1e3) return (v / 1e3).toFixed(1) + 'K';
|
|
return v.toString();
|
|
},
|
|
|
|
fmtCap(v) {
|
|
if (!v) return '—';
|
|
if (v >= 1e12) return '$' + (v / 1e12).toFixed(2) + 'T';
|
|
if (v >= 1e9) return '$' + (v / 1e9).toFixed(2) + 'B';
|
|
if (v >= 1e6) return '$' + (v / 1e6).toFixed(2) + 'M';
|
|
return '$' + v.toFixed(0);
|
|
},
|
|
|
|
capClass(v) {
|
|
if (v >= 200e9) return 'cap-mega';
|
|
if (v >= 10e9) return 'cap-large';
|
|
return 'cap-mid';
|
|
},
|
|
|
|
shortExchange(ex) {
|
|
if (!ex) return '';
|
|
return ex
|
|
.replace(/^Nasdaq.*$/, 'Nasdaq')
|
|
.replace(/^NYSE.*$/, 'NYSE')
|
|
.replace(/^NasdaqGS$/, 'Nasdaq');
|
|
},
|
|
|
|
addToWatchlist(sym) {
|
|
if (this.watchlist.includes(sym)) return;
|
|
this.watchlist = [...this.watchlist, sym];
|
|
try { localStorage.setItem('optionsPricer:watchlist', JSON.stringify(this.watchlist)); } catch {}
|
|
},
|
|
|
|
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'; },
|
|
goScanner(sym) { try { const v = ViewState.load('scanner') || {}; v.symbolsRaw = sym; ViewState.save('scanner', v); } catch {} window.location.href = '/scanner.html'; },
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|