GMI Interoperability
Grade My Investments, the sister platform, has spent a year accumulating exactly what the backtester was missing. Plutus reads it through a deliberately narrow door: three coarse endpoints, one nightly export, and no database access at all.
The API that did not exist
The system design says Plutus "consumes GMI's published REST APIs as an ordinary client and never writes." A sweep of GMI's Azure Functions surface on 2026-09-01 found the first half untrue. The published endpoints are application-facing — support requests, billing analytics, share links, health checks, an FMP proxy. There is no data API serving regime state, grades, fundamentals or series to anyone outside GMI. The REST path the design assumed would have to be built.
So the real choice was between building that API and reading GMI's stores directly. The second option looked cheaper for about a day.
What GMI holds
Read from GMI's schema (83 tables) and design documents, not from live row counts — that caveat stands until someone runs the COUNT(*). The column that matters is point-in-time honesty: whether a backtester can consume the asset without lookahead.
| Asset | Depth | Point-in-time | Plutus use |
|---|---|---|---|
| Fundamentals | ~5,500 symbols × up to 35 years | Good, except RestatementCount > 0 | Fundamental strategies, grade replay |
| Macro series (FRED) | Decades, with vintages | Best in the inventory | Macro state, the dollar, the recession label |
| Regime ribbon | One week lived; decades reconstructable | Excellent structure | Regime conditioning |
| Grades, historical | 10–35 years, computed | Good | A backtestable grade signal |
| Daily prices | Per cached symbol, append-only | Excellent | Cross-check only — our own bars reach 1993 |
| Symbol universe | 2,516 curated tickers | — | The tradeable universe |
| Grades, current | Snapshot | Poor — overwritten in place | Universe and today's grade only |
| Profiles, sentiment | Current only | Poor | Current sector; a lexicon cross-check |
| News | Days, growing | Snippets | A symbol-tagged second capture |
| Index constituents | Current confirmed; historical unprobed | TBD | Universe construction, one probe away |
Two rows changed existing decisions outright. DTWEXBGS, the Federal Reserve's broad dollar index, retired the plan to compute a DXY-style basket ourselves from six forex pairs. USREC, the NBER recession flag, is the ground-truth regime label for a thirty-year replay. And because GMI's regime determination is deterministic, published arithmetic over FRED inputs, its ribbon can be recomputed backwards — an open question in the backtester spec became a defined job, with the caveat that a reconstructed ribbon must be stamped as such.

Why direct SQL lost
Two problems, and the first is the one that decided it. The semantics are not in the database. fmpstatementcache.Dataset is a tinyint. It means CashFlow when it is 3 because Gmi.ClassLibrary/Models.cs says so — and so do PeriodType, the regime formula, and the grade computation. A Plutus that reads the tables directly has to duplicate that enum, and the day GMI adds Dataset = 6, Plutus does not fail. It silently misreads or drops rows. Drift with no error is the failure mode this whole architecture exists to avoid.
The second problem is proportion. GMI's MySQL has public access disabled inside its own VNet, so a direct read means VNet peering or private endpoints — an infrastructure project with an address-range collision risk, bought entirely to avoid writing code on the GMI side. A read replica fixes load and nothing else.
Decided: a small API on the GMI side for the recurring reads, a GMI-side blob export for the bulk, and no database access for Plutus. The split is by size, not preference. Regime state, macro series and the universe are kilobytes a day and every value is computed, so an endpoint that resolves GMI's own meaning is exactly right. The statement backfill is roughly 770,000 periods and 1.5–4 GB of JSON; over HTTP that is paginated requests against a 230-second function timeout, and as gzipped JSONL it is one file, written once.
The point-in-time problem
GMI's caches are read-through caches. They mutate — by their own design: not a system of record, profiles refreshed every 30 days, sentiment expiring, statements overwritten on restatement, grades recomputed every report run. Anything that queries a mutating store gets a different answer depending on when it runs, and that breaks the backtester's promise that two runs from the same manifest reproduce bit-identically. Worse, it imports lookahead: a 2019 backtest reading a statement restated in 2023 is trading on information that did not exist.
So a frozen, as-of-stamped copy is mandatory, and an endpoint is not a snapshot — an HTTP response is as live as a SQL query. The ingest job freezes on receipt, stamps provenance (lived or reconstructed), and writes to Plutus's own blob. Nothing in the trading path or the backtest path ever calls GMI. If the backtester can reach GMI by any protocol, the architecture is wrong.
The contract, and its cross-cutting rules
Appendix A of the interoperability document is the concrete version, proposed and not yet agreed. Three endpoints — /plutus/v1/regime, /plutus/v1/macro/snapshot, /plutus/v1/universe — and five export artifacts with a manifest per run. Six rules apply to all of them:
- Enums resolved to names, always.
"dataset": "CashFlow", never3. - Every payload carries
asOfandformulaVersion. - Empty and error must be distinguishable. No rows is
200with"count": 0; failure is non-2xx. Never a silently empty array — the FMP probe showed exactly that for Lehman, Enron and WaMu. - Versioned routes; both versions run until Plutus has moved.
- Read-only by construction. GET only; no route accepts a body.
- Point-in-time fields are mandatory: when it became true, and when it was knowable.
Under direct SQL, GMI's customer PII, payments, Stripe events and support tickets would have been kept from Plutus by a grant list — one line of DDL between the two. Under the adopted design that data is unreachable by any credential Plutus holds: a function key scoped to a /plutus/ route prefix and a read-only credential for one export container, both in Plutus's Key Vault, separately revocable.
The "never writes" invariant is not a policy. Plutus has no credential capable of writing.
The smell test: if a fourth endpoint is being added within a month, the boundary is in the wrong place — export more, query less.