06.Live scanner & Finnhub WS pipeline
The 6-panel intraday scanner, the 25-symbol chunked subscription layer, the in-process DashMap fan-out, TTS voice alerts, and the Webull read-only live snapshot. All live stores are bounded with oldest-first eviction so multi-day sessions don't grow without limit.
The six panels
The scanner view (frontend/js/views/live_scanner.js) is a six-panel grid where each panel is an independent leaderboard over the same Finnhub WebSocket tick stream. By default:
- % Gainers — biggest % up vs prior close.
- % Losers — biggest % down.
- Volume leaders — highest cumulative session volume.
- Volume vs avg-10 — biggest relative-volume spike (RVOL).
- Range expansion — biggest intraday range relative to ATR(14).
- Tape velocity — most prints per second (works best mid-session).
Each panel is independently filterable (price range, min volume, min market cap, exclude OTC, exclude ETFs) and independently sortable. The leaderboard re-renders at most twice per second to keep the WebView responsive even on a hot tape.
Finnhub WebSocket — chunked subs
The free Finnhub tier caps a single WebSocket at 25 simultaneous symbol subscriptions. The scanner needs hundreds. The pipeline:
- Universe — the set of symbols the panels collectively need. Re-computed every minute from the union of: panel filters (e.g. "all US equities > $5 with volume > 1M") + your watchlists + open positions.
- Chunking — split the universe into N chunks of 25. Open N WebSocket connections, each subscribed to one chunk.
- Fan-in — every chunk's ticks land in a single in-process
DashMap<Symbol, Tick>+tokio::sync::broadcastchannel. - Fan-out — the browser opens
WS /ws/ticks, picks a per-panel filter, and only receives ticks for symbols its filter matches. The server-side filter keeps WS bandwidth bounded even when the universe is large. - Reconnect — chunks reconnect on disconnect with exponential backoff (start 1 s, cap 60 s, jitter ±20%). The in-memory tick store keeps the last value for every symbol so panels stay populated through a reconnect.
The free tier is honored with no premium key. Bring your own Finnhub key from Settings → API Keys → Finnhub if you want the paid tier (more symbols / connection, no rate-limit reconnects).
TTS voice alerts
The Voice alerts toggle in the scanner header reads matching leaderboard entries out loud via speechSynthesis. The phrasing is templated:
"Halt: S H W Z, code T-One." "Volume spike: N V D A, R-VOL twelve point three." "New gainer: A M D, plus four point five percent."
Ticker symbols are spelled letter-by-letter (better than the synth's word-guess), volumes are quantized to a one-decimal multiplier, percentages round to one place. Throttled to one utterance per 3 s per panel to avoid talk-over.
Under Tauri's custom scheme speechSynthesis.speak can throw a cross-origin SecurityError on first call; it's wrapped in a try/catch so the panel keeps working — speech just silently fails. Falls back gracefully if no voices are available.
Bounded in-memory stores
Every live stream is sized so a multi-day session can't OOM the host:
- Ticks — DashMap keyed by symbol, last-tick-only. Bounded by the universe size (rotates as the universe shifts).
- Halts — VecDeque, cap 2 000 oldest-first eviction (step 07).
- Catalysts — VecDeque, cap 10 000 oldest-first eviction (step 07).
- Webull snapshot — single struct, last poll only.
The cap values are configurable in config.default.toml if you want to keep more history in memory; defaults are sized for a 16 GB host running everything else.
Webull live snapshot
Paste your Webull session tokens once in Settings → Webull. The server-side adapter polls tradeapi.webullbroker.com every 5 seconds and pushes a snapshot containing positions, day P&L, buying power, and open orders to WS /ws/webull. Tokens stay in memory only — never written to disk, never logged — and expire when you quit the app.
- What it can do — read-only position + order display, live P&L on the Live Positions tile.
- What it can't do — place orders. By design.
- If the token expires — the adapter logs a warning, stops polling, and the Live Positions tile shows "session expired — re-paste tokens". The rest of the app is unaffected.
~/.bash_history if you pasted them from a terminal you ran echo in.
Pre-market & markets snapshot
The Pre-market tile (GET /api/premarket/snapshot) shows futures + commodities + crypto + FX + economic events in one cached page. The Markets tile (GET /api/markets/snapshot) gives a 16-symbol world-markets grid (S&P, NASDAQ, DAX, FTSE, Nikkei, Hang Seng, etc.). Both routes use futures_util::future::join_all internally — the serial-loop version blocked for 150 s and starved the pool; the join_all version finishes in ~3 s.
Markets snapshot is cached for 60 s in-process so the grid doesn't re-fan-out on every browser refresh.