One loop, two layers
Every experiment in this repo — a two-line naive baseline on gasoline CPI, a curriculum-trained oil analyst — walks the same loop. Data enters through adapters, one fetch() per source; there is nothing special about the built-in StatCan, FRED, and yfinance adapters, and nothing stops you from wrapping a CSV, an API, or a database the same way. Registered on a DataService, the data is fenced behind a cutoff, handed to predictors as one fresh context per forecast origin, and comes out the other side as scored, cached, comparable results.
That scored loop is Track 1. The same agents can also be used interactively — scenario analysis, monitoring, open-ended Q&A — without emitting a Prediction the harness can score (Track 2). This atlas is about the scored loop.
The two layers matter: the core library aieng.forecasting owns everything drawn solid below. You author the dashed pieces — a dataset, a spec, a predictor lineup — and the loop does the rest.
The temporal fence
A backtest is only honest if every forecast is made from what was knowable at its origin — and "knowable" is not the same as "existed". A market close exists the moment the bell rings but publishes the next morning; official statistics trail their reference month by weeks. The repo encodes this as data, not discipline: every row carries a released_at stamp alongside its timestamp (absent the stamp, the timestamp stands in), and predictors can only see through the fence it defines. The same fence covers context — news snapshots and documents are cutoff-scoped exactly like series rows.
context(as_of). Hollow red = recorded but not yet released — the stamp, not the timestamp, decides.How it's enforced
- Predictors receive a ForecastContext, never the raw
DataService— through that path, series leakage is structurally impossible. - Every series read routes through the CutoffEnforcer — keep rows where
released_at ≤ as_of, falling back totimestamp; document reads are filtered by publication date the same way. - Honest
released_atstamps are your responsibility when onboarding data — the one leak the library can't catch for you.
The exception: agents leak
An agent can leak through its tools, not the database — a web search, or any tool that trusts a model-supplied cutoff. The harness pushes back — a seeded cutoff plus an independent verifier (§05) — but the filter sometimes fails. Agent backtests are optimistic by default; audit traces.
More tool freedom, less information control — that's structural. It pushes evaluation toward fenced, pre-cached context (less agency) or live forecasting, where there is no future to scrape — parametric knowledge and the model's training cutoff still apply.
One interface, four families
Everything that forecasts implements the same two-member contract: a predictor_id and a predict(task, context) that returns one prediction per horizon. That is the whole trick that makes the leaderboard possible — a naive baseline, a gradient-boosted model, a single structured LLM call, and a multi-turn agent all answer the same question from the same fenced context, so their scores mean the same thing.
The four families below are less a taxonomy than an escalation of how much machinery sits between the context and the answer. Keep the left two honest and strong: an agent is only as interesting as its margin over the best baseline you can field.
predictor_id: str · predict(task, context) → list[Prediction]
Baselines
The calibration floor. Anything that loses to naive is a finding.
Numerical
Darts-backed (Prophet aside), covariate-aware, probabilistic via sampling.
LLM Processes
One structured LLM call — series + metadata in, distribution out. No tools.
Agentic
A multi-turn ADK agent with tools, wrapped to honor the same contract (§05).
CRPS
binary → Brier
categorical → RPS
— lower is better, everywhere.
Backtest to develop, evaluate to commit
Specs are experiment design, kept in YAML and out of code: a task, a window of forecast origins, a stride, a warmup — or, for irregular calendars like BoC's meeting dates, an explicit origin_dates list. Because the lineup and the spec are independent, the same predictors run against a two-origin smoke spec, a full development backtest, and a protected evaluation — without edits.
The two loops answer different questions. backtest() is the open loop you run freely while developing and tuning. evaluate() is for the answer you commit to — and tuning against it destroys the answer. Attach an EvalTracker and a max_runs cap and the harness refuses extra runs and never caches, so spend stays visible. Call it without a tracker and the budget does not apply.
warmup history behind it — 250 trading days on those specs, 24 months on CPI, 8 meetings on BoC — and all horizons resolvable ahead of it, or it is silently skipped.backtest() | evaluate() | |
|---|---|---|
| purpose | develop & tune | the committed answer |
| runs | unlimited | max_runs budget, if an EvalTracker is attached |
| caching | cached & resumable | never — spend stays visible |
| window | historical dev window | held-out, often post-LLM-cutoff |
Two protection styles in the repo
Enforced — EvalSpec + max_runs (getting_started, S&P 500, BoC). Convention — energy's notebook 06: disjoint 2026 window, run-guards defaulting to committed artifacts, checksummed agent state.
Anatomy of an analyst agent
Identity (what the agent is) and role (its job in one experiment) are deliberately separate objects. The identity — persona, toolbelt, skills, model — is an AgentConfig; the role — what payload it sees per origin and what structured forecast it must return — lives on the AgentPredictor that wraps it. One identity can play many tasks, and rival strategies can play the same task fairly.
The capabilities column is where the fence from §02 gets stress-tested: the search tool runs a grounded sub-agent whose brief passes through an independent verifier before the analyst ever sees it. It reduces leakage; it does not eliminate it.
Identity — AgentConfig
namekeys the predictor_idinstructionpersona + supplementsmodelLITE / ADVANCEDcontext_retrievalsearch sub-agentcode_executionE2B sandboxfunction_toolse.g. run_forecastextra_toolse.g. skill-mutation toolsskills_dirsSKILL.md playbooksComposed from a toolbelt in the energy starter agent — one ToolSpec per capability, folded onto the config; the other starters set the same fields directly. Adding a tool is one line.
Capabilities — build_adk_agent → tools
Role — AgentPredictor
prompt_buildertask + context → payloadoutput_schemastructured forecast → PredictionThe payload carries as_of, horizons, quantile levels, and compressed history — wrap the builder to inject anything else: pre-cached news, engineered features, prior forecasts.
Five instantiations of one architecture
Same loop, same interface, same fence — different data, payloads, and machinery switched on. The matrix reads horizontally as "what does this implementation exercise" and vertically as "where do I find a working example of this component".
| # Implementation | Target · payload | Covariates | LLM-Process | Agent predictors | Code exec | Adaptive skills | Budgeted eval | Docs / reports | LLM judge | Irregular origins |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 getting_started the smallest end-to-end loop |
CA gasoline CPI, 1 mo continuous |
— | — | — | — | — | ● | — | — | — |
| 1 sp500_forecasting leak-safe covariate discipline |
cumulative log returns over 1/5/21 bd continuous |
● | ● | ○ | ○ | — | ● | — | — | — |
| 2 food_price report-grounded prompting |
food CPI: aggregate + 8 sub-indices, 6–17 mo continuous ×9 |
— | ● | ○ | ○ | — | — | ● | — | — |
| 3 energy_oil the agentic staircase |
daily WTI, 5/10/21 bd continuous + binary |
● | ● | ● | ● | ● | conv. | ● | — | — |
| 4 boc_rate_decisions judged categorical reasoning |
cut / hold / hike per meeting categorical |
● | ● | ● | ○ | — | ● | ● | ● | ● |
● built into the curriculum · ○ available as an option or starter-agent toggle · — not used · conv. protected by convention, not max_runs (notebook 06).
Docs/reports: food = CFPR reports via DocumentStore; energy = pre-cached news snapshots (the adaptive curriculum); BoC's press releases serve as the judge's reference text. #1–#4 each end with a hackable 99_starter_agent; #0 adds a repo-concierge Q&A agent.
What each one teaches
The numbering is not arbitrary: it mirrors the bootcamp progression — conventional methods, then LLM processes, then agents, then agentic evaluation. Each step assumes the last and adds one idea.
Where do you land?
| Situation | Do this |
|---|---|
| Need the loop to click first | Run 00_environment_check.ipynb in getting_started, then §01–§04 of this page. |
| Have a CSV that is none of the five problems above | Path B — guide 1 → 2 → 4 (guide 3 if you add an agent). The sample dataset in guide 1 runs offline. |
| Want to change how an existing agent thinks | Path A — that implementation's 99_starter_agent.ipynb + guide 3 (start at "which starter are you on"), then guide 4. |
| Want news / tools / adaptive strategy | energy_oil's README then its 99_starter_agent — guide 3 as written is this path (ToolSpec toolbelt). |
| Want covariates / report-grounded prompting / judged categorical | sp500 / food_price / boc README respectively, then Path A on that starter (toggles; guide 3's levers are the same AgentConfig fields). |
Build your own forecaster
The architecture exists so that a new forecaster is always one of two paths — extend a reference implementation, or bring your own dataset — and every move on either path has a step-by-step guide in the repo.
Path A — extend a reference implementation
Closest to a problem already in §06? Skip onboarding and specs entirely. Pick your implementation, open its 99_starter_agent.ipynb, work guide 3's levers, and close with guide 4 — no new dataset or spec needed until you want one.
Path B — bring your own dataset
Any source works — a CSV, an API, a database, scraped files. Map it onto timestamp · value · released_at, choose the release stamp honestly, register it on a DataService.
A task + window in YAML, a lineup in code, cached_multi_backtest, a leaderboard — with strong baselines in every lineup: the naive floor plus the best statistical model you can field. An agent is only as interesting as its margin over them. Hold out your eval window before you tune.
Persona, toolbelt, search brief, skills — every lever that changes how the agent thinks, each with a worked change, scored against the baselines. Only needed if you're adding an agent.
guides/03-customize-agent-strategy.mdPredictor and run backtest() / evaluate() against the baselines — then audit the result before you believe it (guides/04-audit-your-results.md). Everything else in this atlas exists to keep that comparison honest.