GMI · TECHNOLOGY OBSERVATORY // ALL SYSTEMS NOMINAL
ENGINEERED BY LEOPARD DATA

Investor Legends & Smart Lists: Published Criteria, Tested One at a Time

Eight screens built from criteria famous investors put in print, run weekly over GMI's graded universe, with a pass, fail or "not evaluated" for every criterion on every company. Any screen can then be saved as a list that keeps itself current and logs why each symbol came or went.

BUILT · SEPTEMBER 2026 In final testing ClientWeb · AdminWeb · MAUI

What Wave 25 Built

Investor Legends are eight pre-built screens. Each one names its source book, and each company gets a per-criterion breakdown: "Passes 6 of 7 Graham tests; fails dividend record: 14 years, needs 20." Smart Lists turn any screen, a legend or a custom v1 screen, into a symbol list whose membership refreshes on a schedule, with every entry and exit logged with its reason: "NKE left: PEG rose to 1.4 (needs below 1)."

It extends the Wave 16 stock screener. There is still one screener (ScreenerService, ScreenerEngine, the presets table), not a parallel one, and the v1 presets that read as judgments were renamed while the old keys kept resolving: Undervalued Large Caps became Low P/E Large Caps and Dividend Champions became 3%+ Yield, Moderate Debt, so saved links still work.

The Eight Screens and What Each Can't Test

Screen ("Inspired by …")SourceA sample of what's testedNot evaluated or informational
Peter LynchOne Up on Wall Street (1989)PEG under 1; 5-year EPS growth 15–30%; debt/equity ≤ 0.33; net cash per share > 0; inventory not outgrowing salesKnowing the story, the company category, the product: judgment calls. Inventory is "not applicable" when a company holds none
Benjamin Graham (defensive)The Intelligent Investor, ch. 14Revenue ≥ $750M; current ratio ≥ 2; 10 years of positive earnings; 20 years of dividends; P/E × P/B ≤ 22.5Price-based tests for foreign-currency filers
Buffett-style qualityBerkshire letters; Hagstrom; BuffettologyROE > 15% in 8 of 10 years; debt ≤ 5× net income; steady operating margin; positive, growing free cash flowManagement quality and the durability of a competitive advantage. Buffett never published a numeric screen, so this one follows Hagstrom's and Buffett & Clark's readings
Greenblatt Magic FormulaThe Little Book That Beats the MarketCombined earnings-yield and return-on-capital rank in the top 30; financials and utilities excludedNothing; ranks 31–60 count as near misses
Piotroski F-ScoreJournal of Accounting Research (2000)The nine binary tests; a match is 8 or 9Nothing; book-to-market is shown as information only
John NeffJohn Neff on InvestingP/E ≤ 60% of the universe median; total-return ratio ≥ 2× medianCyclicality, "growing field", fundamental case: judgment calls
David DremanContrarian Investment StrategiesBottom-fifth P/E among the 1,500 largest; debt below sector medianPrice-based tests for foreign-currency filers
William O'Neil CAN SLIMHow to Make Money in StocksC, A, N (near 52-week high), L (relative strength ≥ 80th percentile)I (no licensed institutional-ownership data), M (a whole-market call, not a company test), N's new products

Every screen is labelled "Inspired by", with a card disclaimer: GMI's interpretation of published criteria, not endorsed by or affiliated with the investor. Two thresholds are GMI's own readings and are listed as open questions in the design doc: Graham's 1971 "$100M of sales" CPI-adjusted to $750M, and Lynch's balance-sheet ideal expressed as debt/equity ≤ 0.33.

Decision: Four States, Not Two

A pass/fail screen has to do something with a criterion it can't compute. The two easy choices are both wrong. Treating it as a pass inflates matches: every CAN SLIM candidate would "pass" institutional sponsorship. Treating it as a fail hides good companies behind a data gap: every bank would fail Graham's current-ratio test because banks don't report current assets.

So every criterion resolves to Pass, Fail, Not evaluated (with the reason printed, e.g. "statements reported in TWD; GMI does not convert currencies for price-based tests") or Not applicable. A company matches when every required criterion passes. A near miss fails exactly one, and that one has to be a real Fail: a company one "not evaluated" away from matching isn't shown as almost there, because we don't know that it is.

The near-miss view turned out to be the teaching surface. "Fails dividend record: 14 years, needs 20" explains Graham's standard better than the list of companies that pass it.

PRECOMPUTE AND REFRESH
rendering diagram…
flowchart TB
    subgraph SEO[gmi-seopagebuilder - weekly when idle]
      P1[Phase 1: metrics per company<br/>cached statements + prices]
      P2[Phase 2: universe statistics<br/>medians, percentiles,<br/>Magic Formula ranks, RS]
      P3[Phase 3: evaluate 8 legends<br/>Pass, Fail, Not evaluated,<br/>Not applicable]
      P1 --> P2 --> P3
    end
    FMP[Cached market-data client] --> P1
    DEEP[ForceStatementRefetch<br/>one-time deep fetch] -.-> FMP
    ST[(investorlegendsymbolstate)] -.-> DEEP
    P3 --> R[(investorlegendresults)]
    R --> API[Legends API]
    API --> APPS[ClientWeb, AdminWeb, MAUI]
    subgraph SCH[gmi-schedulesengine - SmartListPass]
      DUE[Due Smart Lists] --> RUN[Re-run the screen]
      RUN --> SEL[SmartListMath<br/>free-tier selection + diff]
    end
    R --> RUN
    SEL --> SL[(symbollists<br/>real membership)]
    SEL --> CH[(smartlistchanges<br/>reason per entry and exit)]
    CH -. shaped for .-> AL[Alerts detector<br/>not wired yet]
The legend pass runs weekly inside the SEO page builder, which already owns the cached market-data client and the graded universe. Smart Lists refresh in the schedules engine and read only the stored results, so a list refresh never calls the market-data provider.

The Cache Said "Complete" at 4 Years for a 22-Year Question

Graham needs 20 years of dividends, and his 10-year EPS test needs 13 years of statements. The pass asks the cached market-data client for 22 annual periods. For most symbols it got back four.

The statement cache treats a window as complete when every period it holds is settled. That rule is deliberate: without it, a company with only three years of history would be refetched on every request, forever. Its side effect is that a symbol the SEO builder had warmed at 4 periods answers a 22-period request with those same 4 rows, and it looks like a young company rather than a cache miss.

OptionWhy not / why
Change the completeness ruleIt changes behaviour for every consumer of the cache and brings back the refetch-forever problem for young companies
Invalidate statements for the whole universeAn admin-scale action that throws away settled data and costs the same calls
A scoped, one-time deep fetch (chosen)CachingFmpApiClient.ForceStatementRefetch() returns an IDisposable scope backed by an AsyncLocal flag. Inside it, statement reads skip the cache; the write path is unchanged, so the deep history is cached exactly as a normal miss would cache it. The pass records the deep fetch in investorlegendsymbolstate and repeats it only if the cache later holds fewer periods than it saw

AsyncLocal matters here: the pass processes symbols concurrently, and a static flag would leak the bypass into unrelated requests. The cost is budgeted rather than hoped for: about 10k provider calls once (three deep statement calls plus a price call for ~2.5k companies), then roughly one price call per company per week. A config switch drops the price calls entirely; CAN SLIM's price tests then become "not evaluated" instead of running on stale prices.

Twenty Years of Data Has Twenty Years of Corporate Actions

Short-window features mostly never meet these problems. A 10- and 20-year screen meets all of them:

  • Splits in EPS history. The provider's annual EPS isn't guaranteed to be split-adjusted, and a 4-for-1 split looks like a 75% earnings collapse. LegendMetricsBuilder.DetectSplitFactors treats a jump in diluted shares near a split ratio, with net income roughly unchanged, as a split and restates earlier EPS. A stock-funded merger, where earnings jump along with the share count, is deliberately not restated.
  • Splits in cached prices. The price cache is append-only (INSERT IGNORE), so pre-split closes can sit beside post-split ones. A one-day move within 3% of a split ratio restates the earlier closes before the 52-week-high and 12-month-return tests run. This was found in the post-merge hardening pass, not in the first build.
  • Foreign filers. Statements come in the reporting currency; price is in USD. Dividing one by the other is how GMI once produced a $50-trillion Taiwanese company. Tests that mix the two are marked "not evaluated" with the currency named, rather than being run on FX-skewed numbers.
  • Banks and insurers. Where current assets and liabilities aren't reported, current-ratio tests are "not evaluated", not failed.

The same hardening pass fixed a quiet bug: the precompute cleared the whole EF change tracker between batches, which also detached the run row, so progress and status saves silently stopped. It now detaches per entity type. A pass that can't start (typically a migration not yet applied) now waits an hour instead of logging an error on every 60-second builder poll.

Decision: A Smart List Is a Real Symbol List

The tempting design was a virtual list: a saved query evaluated when someone looks at it. Instead, a Smart List is an ordinary symbol list plus a smartlists row that owns its membership. Because the backing list is real, three existing systems work with no special case:

  • tracked-symbol counting (AccountEntitlementService) counts it like any list;
  • a recurring report on the list reports its current membership, because the report engine reads list items when the job runs;
  • add-to-list, dashboards and one-off reports just work.

A refresh re-runs the screen (legend lists read the stored results; custom lists run ScreenerEngine over the graded universe), diffs old against new with a pure SmartListMath.Diff, and writes each change to smartlistchanges with a reason. Legend exits name the first failing criterion and its value; custom exits name the first failing filter ("P/E 16.2 is above the screen's 15"). A refresh never empties a list because data is missing: no completed legend pass, or an empty universe, skips the refresh with a status instead of logging every member as an exit.

Free tier: the account-wide cap is 8 distinct tracked symbols, so a free Smart List keeps pinned symbols that still match, then the top-ranked matches, and never pushes the account's distinct total above 8. A symbol already tracked on another list costs nothing. A symbol that still matches but falls outside the cap is logged as an exit with that reason ("still matches; outside the free tier's top 8"), and the page says so: "Showing 8 of 23 matches — free tier." Paid lists have a safety ceiling of 500.

The Change Log Is Shaped for Alerts

Wave 25 was built in parallel with Wave 24 Alerts, so the change rows were designed as alert source records from the start: EventType (ScreenEntry / ScreenExit), Symbol, the owning SymbolListID, a PayloadJson with the reason, a unique DedupeKey, and AlertEventID / AlertProcessedDateTime left NULL for a detector to claim.

The honest status: the seam is in the schema, but no alerts detector reads it yet. "A symbol entered your screen" is not an alert anyone can subscribe to today. Wiring it is a small detector, not a redesign, and that's the point of shaping the rows now.

Schema, Tests and What It Doesn't Do

AreaDetail
Tables (5)investorlegendruns, investorlegendresults (per symbol × legend: pass counts, match and near-miss flags, sort score, one-sentence summary, per-criterion JSON), investorlegendsymbolstate, smartlists, smartlistchanges
Endpoints (10)Legends list, legend search, a company's breakdown across all eight; Smart List CRUD, refresh-now and history. Account-scoped with the admin-or-owner check; nothing under the Functions host's reserved admin/ prefix
Tests (44)Piotroski's nine tests on known answers, Greenblatt ranking, Graham's 22.5 product and near-miss summary, Lynch PEG, Neff total-return ratio, Dreman bottom fifth, CAN SLIM's not-evaluated parts, split detection in EPS and prices, foreign filers, Smart List selection, diff and free-tier cap, an end-to-end precompute over a mocked provider, and a voice test that fails on "buy", "should", "undervalued" or "gem" in any screen text
Deliberately not builtScreen backtesting (it needs point-in-time universes and a survivorship-bias disclosure first); a public SEO teaser page; plain-English screening via Claude

Descriptive voice only: a company "passes 5 of 6 tests", "matches" or "is a near miss". Never "buy", "undervalued" or "gems". A screen that matches a published rule is a description of the company's numbers, not a recommendation.