Report Generator Beta
The macro engine — one shared dataset, built on a schedule, read by everyone. The opposite shape to Alpha, and deliberately so.
flowchart TB
subgraph TRIG[Triggers]
T1[Daily baseline]
T2[Release-aware<br/>minutes after a print]
T3[Admin-queued run]
end
F[Fetch ~38 economic series]
C[Classify regime<br/>growth x inflation]
G[Score the macro grade<br/>against published bands]
subgraph DER[Derived in the same pass]
D1[ML forecasts]
D2[Sector playbook]
D3[Grade-by-regime study]
D4[Commodities]
D5[Release calendar]
D6[Scorecard]
end
TMP[(Write temp blob)]
LIVE[(Atomic copy<br/>onto the live path)]
R[Three apps + every shared link]
TRIG --> F --> C --> G --> DER --> TMP --> LIVE --> R
F -. run fails: write NOTHING,<br/>readers keep the previous<br/>snapshot and its age .-> RTwo Engines, Opposite Shapes
Alpha and Beta are both .NET console applications on the same virtual machine, and they are built on opposite assumptions. Alpha is per-user, on demand, fan-out: a job arrives, it fetches everything for the symbols in that job, and produces an artifact for one account. Beta is one dataset, on a schedule, fan-in: it runs whether or not anyone is watching, and every reader gets the same snapshot.
| Alpha | Beta | |
|---|---|---|
| Trigger | A user requests a report | A schedule, a data release, or an admin queueing a run |
| Scope | The symbols in one job | The whole economy — one snapshot for everyone |
| Output | An Excel workbook per symbol, owned by an account | A single JSON snapshot read by three apps and every shared link |
| Cost shape | Scales with users | Flat — one run serves one reader or ten thousand |
| Failure | That report fails; the user is told | Nothing is written; readers keep the previous snapshot and are told its age |
That last row is the design decision that matters most, and it is covered below.
What One Run Produces
A single run walks roughly 38 economic series and turns them into every derived product the macro features need. These are not independent jobs — each is computed from the same inputs in the same pass, which is what keeps them consistent with one another.
| Component | Produces |
|---|---|
RegimeClassifier | Which of four macro seasons the economy is in, from blended growth and inflation impulses — plus the margin to the line, so "how firm is this call" is answerable |
MacroGradeScorer | The economy graded out of 100 against published bands — the same grading contract used for companies, applied to the whole economy |
MacroForecastService | ML.NET SSA projections one, two and six months out for each indicator |
RegimePlaybookService | How sectors have historically behaved in conditions like these — descriptive, never a recommendation |
GradeRegimeStudyService | Whether company quality holds up when the tide goes out, across ~484 index constituents |
CommoditiesService | The commodity complex read for risk-on and risk-off signals |
ReleaseCalendarService | A 90-day projected calendar of upcoming government data releases |
ScorecardService | The platform's own record: every past projection scored against what actually printed |
ClaudeNarrativeService | Plain-English outlook text — the only model-written part; everything else is arithmetic |
MacroAlertService | Notifications when the regime or grade actually changes, not on every run |
Release-Aware Scheduling, Not a Fixed Cron
Economic data does not arrive on a tidy daily rhythm. Payrolls land on a Friday morning, CPI mid-month, PCE at month end — and between releases, re-running the engine computes the identical answer from identical inputs.
So the daemon runs on three triggers rather than one: a daily baseline so the snapshot is never stale by more than a day; release-aware runs shortly after a scheduled government release, so the dashboard reflects a new print within minutes rather than by luck of the clock; and an admin-queued run for when someone needs a rebuild now. A fixed hourly cron would have done more work and still been late to the only moments that matter.
Stale but Honest Beats Empty, and Both Beat Wrong
Every reader of this platform's macro features — three applications, plus every shared link ever sent — reads one blob. That concentration is what makes the engine cheap, and it is also what makes a bad write catastrophic: a half-written document would break the dashboard for everyone at once.
So the engine never writes the live snapshot. It writes a temporary blob, and only when that write has completed does it issue a server-side copy onto the live path. From a reader's perspective the swap is atomic: the previous snapshot is served in full until the instant the new one is, and no byte of a partial document is ever visible.
And when a run fails, nothing is written at all. The dashboard keeps serving the previous snapshot and states how old it is. A reader looking at yesterday's numbers, clearly labelled yesterday's, is better served than one looking at an error page — and far better served than one looking at half of today's.
Four Modes, One Binary
| Mode | Purpose |
|---|---|
run | A single engine run. The unit of work everything else schedules. |
daemon | The scheduler loop that runs in production — daily, release-aware, and admin-queue polling. |
backtest | Rebuilds the historical regime and grade timeline, which is what makes the scorecard possible: past claims can only be scored if they were recorded. |
backfill | Seeds regime history from the frozen backtest timeline. Deliberately gated until the grading bands were frozen, so history could not be rewritten by a later change of method. |
One binary with modes rather than four executables: the classification, scoring and derivation logic must be identical whether it is producing today's snapshot or replaying 2008, or the backtest would be scoring a different engine than the one that runs.
Related Reading
- Report Generator Alpha — the per-user engine this one is the mirror of
- Macro Dashboard Architecture — what consumes this output
- Macro Data Scale — the data volumes behind one run
- Blob Storage Architecture — the atomic snapshot swap in context
- Provider Cache Strategy — how this engine stopped re-buying the same market data