Add Top Movers screener (mid-cap+, options-tradable)

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>
This commit is contained in:
ojy
2026-05-13 07:47:32 +00:00
parent 3ea5fd5209
commit 2e565fae4d
11 changed files with 530 additions and 2 deletions

View File

@@ -402,3 +402,64 @@ export async function scanSymbol(symbol: string): Promise<ScanResult> {
ivJumpPct, ivHv, spike, bigMove, expiry, ivJumpPct, ivHv, spike, bigMove, expiry,
}; };
} }
// ---------------------------------------------------------------------------
// Top-movers screener — pulls Yahoo's predefined screeners (day_gainers,
// day_losers, most_actives, most_shorted_stocks) and filters out small caps.
// Stocks with options ~= virtually all US equities with marketCap ≥ $2B,
// so we trust the cap filter and skip per-symbol option-chain verification.
// ---------------------------------------------------------------------------
export type MoverCategory = "day_gainers" | "day_losers" | "most_actives" | "most_shorted_stocks";
export type MoverRow = {
symbol: string;
name: string;
price: number;
change: number;
changePct: number;
volume: number;
avgVolume: number;
marketCap: number;
exchange: string;
quoteType: string;
};
const MIN_MARKET_CAP = 2_000_000_000; // $2B — mid-cap floor (exclude small/micro caps)
/**
* Returns up to `count` movers in the given Yahoo screener category,
* filtered to common stocks (EQUITY) with marketCap ≥ $2B.
*/
export async function fetchMovers(
category: MoverCategory,
count = 50
): Promise<MoverRow[]> {
// Yahoo's screener returns up to ~250 rows; we ask for 100 and post-filter.
const res = await yf.screener({ scrIds: category, count: 100 }, undefined, { validateResult: false }) as any;
const quotes: any[] = res?.quotes ?? [];
const rows: MoverRow[] = [];
for (const q of quotes) {
if (q?.quoteType !== "EQUITY") continue;
const mc = Number(q.marketCap ?? 0);
if (!Number.isFinite(mc) || mc < MIN_MARKET_CAP) continue;
rows.push({
symbol: String(q.symbol ?? "").toUpperCase(),
name: String(q.shortName ?? q.longName ?? q.displayName ?? ""),
price: Number(q.regularMarketPrice ?? 0),
change: Number(q.regularMarketChange ?? 0),
changePct: Number(q.regularMarketChangePercent ?? 0),
volume: Number(q.regularMarketVolume ?? 0),
avgVolume: Number(q.averageDailyVolume3Month ?? 0),
marketCap: mc,
exchange: String(q.fullExchangeName ?? q.exchange ?? ""),
quoteType: String(q.quoteType ?? ""),
});
if (rows.length >= count) break;
}
return rows;
}

View File

@@ -7,7 +7,7 @@
*/ */
import { Hono } from "hono"; import { Hono } from "hono";
import { fetchOptionsChain, fetchExpirations, scanSymbol } from "../lib/datafetch.js"; import { fetchOptionsChain, fetchExpirations, scanSymbol, fetchMovers, type MoverCategory } from "../lib/datafetch.js";
/** How old a snapshot can be and still count for the term structure (more lenient than primary TTL). */ /** How old a snapshot can be and still count for the term structure (more lenient than primary TTL). */
const TERM_STRUCTURE_TTL_MS = 30 * 60 * 1000; // 30 minutes const TERM_STRUCTURE_TTL_MS = 30 * 60 * 1000; // 30 minutes
@@ -550,6 +550,30 @@ optionsRouter.get("/scan", async (c) => {
return c.json(ok({ count: results.length, results })); return c.json(ok({ count: results.length, results }));
}); });
// ---------------------------------------------------------------------------
// Top-movers screener — Yahoo's day_gainers / day_losers / most_actives /
// most_shorted_stocks, filtered to mid-cap and above (marketCap ≥ $2B).
// ---------------------------------------------------------------------------
const VALID_MOVER_CATEGORIES: MoverCategory[] = [
"day_gainers", "day_losers", "most_actives", "most_shorted_stocks",
];
optionsRouter.get("/movers", async (c) => {
const cat = (c.req.query("category") ?? "day_gainers") as MoverCategory;
if (!VALID_MOVER_CATEGORIES.includes(cat)) {
return c.json(fail(`Invalid category. Use one of: ${VALID_MOVER_CATEGORIES.join(", ")}`), 400);
}
const count = Math.min(Math.max(1, Number(c.req.query("count") ?? 50)), 100);
try {
const rows = await fetchMovers(cat, count);
return c.json(ok({ category: cat, count: rows.length, results: rows }));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return c.json(fail(msg), 500);
}
});
// PATCH /api/orders/:id { status:'open'|'closed', note?:string } // PATCH /api/orders/:id { status:'open'|'closed', note?:string }
optionsRouter.patch("/orders/:id", async (c) => { optionsRouter.patch("/orders/:id", async (c) => {
const id = Number(c.req.param("id")); const id = Number(c.req.param("id"));

View File

@@ -105,6 +105,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

View File

@@ -198,6 +198,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

359
frontend/movers.html Normal file
View File

@@ -0,0 +1,359 @@
<!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 &ge; <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>

View File

@@ -82,6 +82,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

View File

@@ -48,7 +48,7 @@
<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="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="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"><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="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 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="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="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="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>

View File

@@ -75,6 +75,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

View File

@@ -89,6 +89,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

View File

@@ -187,6 +187,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">

View File

@@ -152,6 +152,18 @@
</a> </a>
</li> </li>
<li class="nav-item">
<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" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<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"> <li class="nav-item">
<a class="nav-link" href="scanner.html"> <a class="nav-link" href="scanner.html">
<span class="nav-link-icon d-md-none d-lg-inline-block"> <span class="nav-link-icon d-md-none d-lg-inline-block">