Two New Features, Zero New Data Feeds
The Watchlist Dashboard (Wave 13) and the Stock Screener (Wave 16) look like the kind of features that need a real-time market-data subscription and a search index. Neither got one. Both are built on data the platform already pays for and already caches, and the interesting engineering is in what they refuse to fetch.
BUILT · SEPTEMBER 2026 ClientWeb · AdminWeb · MAUIThe Asset Both Features Stand On
Every night the public ticker page builder grades ~2,500 companies
and writes each one's full summary (grade, score, sector, analyst sentiment, ML health, fundamentals,
multi-year grade trend) as JSON into publictickers. That's the global grade
cache the free tier already reads from. Next to it,
the EOD price cache (fmppricecache) holds years of daily closes for every tracked symbol,
kept warm by the cache warmer and every report run. The two features below are mostly new
reads of those two tables.
flowchart LR
SEO[Nightly grade builder<br/>~2,500 companies] --> PT[(publictickers<br/>grade cache JSON)]
WARM[Cache warmer +<br/>report runs] --> PC[(fmppricecache<br/>daily closes)]
FMP[Market-data provider<br/>quote endpoint] --> QC[WatchlistQuoteCache<br/>60 s, process-wide]
PT --> WL[Watchlist Dashboard]
PC --> WL
QC --> WL
PT --> SNAP[Screener snapshot<br/>parsed once, 10 min]
SNAP --> SC[Stock Screener]
SC --> WALLS[Existing symbol-cap and<br/>report-allowance checks]Watchlist Dashboard: Where Each Column Comes From
One sortable grid of every symbol on the account's lists: price, day change, volume, 52-week range and position within it, RSI(14), analyst sentiment, health score, grade and a 30-day sparkline, with auto-refresh off / 1 / 5 / 15 minutes.
| Columns | Source | Provider cost |
|---|---|---|
| Grade, sector, sentiment, health, fundamentals | global grade cache | none |
| RSI(14), 30-day sparkline | EOD price cache, database read only | none. A symbol with no cached history simply shows no RSI |
| Price, change, volume, 52-week range | the quote endpoint the API already proxies | ≤1 batched call per symbol per minute, across all viewers |
- Fan-in, not per-user polling. A process-wide quote memo (
WatchlistQuoteCache) sits in front of the proxy with a 60-second TTL, equal to the fastest refresh option, so a 1-minute viewer never sees a quote older than the interval they chose. A hundred users watching AAPL cost one quote call a minute, not a hundred. Misses (delisted, OTC, typos) are cached too, so a bad symbol doesn't trigger a provider call on every refresh. - The RSI matches other charting tools. Wilder's RSI, seeded with a simple mean and then Wilder-smoothed, which is the definition charting platforms display. It's pinned against a reference series in the tests. With fewer than 15 closes it returns nothing, because a short-window RSI is a different, noisier number. Today's quote is appended only if it's for a day the EOD cache hasn't recorded, so the RSI and sparkline end at the price shown in the row.
- Missing sorts last in both directions. A missing RSI isn't "the lowest RSI". Ties break on symbol so an auto-refresh never reshuffles equal rows. That logic lives once, in a shared
WatchlistGridclass used by all three apps. - Bounded. A 250-symbol ceiling per request, so a large staff test list can't turn into a thousand-symbol quote sweep. If quotes fail, rows fall back to the last EOD close and are labelled as such.
Stock Screener: A LINQ Pass, Not a Search Cluster
Screens run over the graded universe: grade score, sector, sentiment, P/E, dividend yield, revenue growth, debt/equity, ROE, FCF yield, operating margin, grade trend and ML health where it exists. The standard way to build this is a search index or a table of indexed filter columns. At ~2,500 rows, neither is worth it:
- A 10-minute in-process snapshot.
ScreenerServiceparses the universe's JSON once and keeps the parsed rows in memory, replaced atomically so readers never see a half-loaded list. Grades change at most once a day, so a screen that's 10 minutes stale looks the same as a fresh one, and a search costs one in-memory pass instead of a ~10 MB database read. - One definition of "yield ≥ 3%". Filtering, sorting, paging, sentiment grouping, the summary line and the criteria chips live in one shared
ScreenerEngine(the same pattern as the Earnings Lab math), so the server and all three apps apply identical logic. - Presets say their numbers. The built-ins (Undervalued Large Caps, High Growth Tech, Dividend Champions, Low Debt Leaders, Improving Grades) describe themselves in figures, e.g. "profitable companies above $2B yielding 3% or more with debt/equity of 2 or below". They're filters, not recommendations.
| Tier | Results | Saved presets | Acting on results |
|---|---|---|---|
| Free | top 25 matches of any screen | 1 | "Add to list" and "Generate report" go through the existing symbol-cap and report-allowance checks, not new copies of them |
| Paid / Staff | every match | 50 |
Hitting either free-tier limit logs a wall event (ScreenerResultCapHit,
ScreenerPresetCapHit) into the same conversion telemetry as the other free-tier limits.
Search works without the new screenerpresets table, so the feature degrades to
"no saved presets" rather than failing if the migration lags the deploy.
What These Two Have in Common
| The easy route | What was built | Why |
|---|---|---|
| Real-time streaming quotes | 1-minute memoised quotes, EOD for technicals | A retail watchlist refreshed every 1–15 minutes doesn't need ticks. Streaming would be a new contract and a new failure mode. |
| Per-user quote polling | Process-wide fan-in | Provider cost grows with distinct symbols, not with viewers. |
| Search index / indexed filter columns | In-memory snapshot + LINQ | 2,500 rows that change daily. Nothing new to run, back up or keep in sync. |
| Separate caps for the new pages | Reuse the existing entitlement walls | Billing rules exist in one place. |
| Logic in each app | Shared pure classes, 35 + 25 unit tests | ClientWeb, AdminWeb and MAUI can't drift apart. |
Caveat: the screener's universe is the graded public set (the enabled index groups behind the public grade pages), not every listed stock. Widening it means grading more companies nightly, which is a cost and provider-contract decision, not a screener change.