Vector Institute · agentic-forecasting · bootcamp atlas

Agentic Forecasting Atlas

The system architecture of the bootcamp repo, read as one continuous story: how a series and its context get fenced behind a cutoff, how predictors — from naive baselines to tool-wielding agents — consume them, how the harness scores the results and judges the reasoning, and how five reference implementations put the same machinery to work. This atlas is the map — the step-by-step worksheets live in guides/, and §06–§07 below route you to the right one.

data layer predictors & agents evaluation harness temporal fence
as_of the information cutoff history the predictor may see context — news · documents — fenced by the same cutoff quantile fan · scored later by CRPS + a rationale you can judge
01System architecture

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.

Data sources StatCan · FRED yfinance your source CSV · API · DB · files fetch Adapters one fetch() per source timestamp · value · released_at cached under data/ by fetch scripts register DataService series_id → frame + metadata CutoffEnforcer released_at ≤ as_of context(as_of) ForecastContext get_series(id) get_documents(src) everything a predictor sees, scoped to one origin date one fresh context per origin Spec — specs/*.yaml task · target_series_id · horizons window · stride · warmup · spec_id Predictor lineup — in code baselines · Darts · LLM-Process · agents one shared interface (§03) origins predict backtest() / evaluate() for each origin: 1 build the cutoff-scoped context 2 predictor.predict(task, context) 3 resolve outcomes from later data 4 score — CRPS · Brier · RPS retries per origin · skips, never fabricates BacktestResult predictions · scores · mean_score data/predictions/<spec_id>/…yaml cached — crash-safe, instant re-runs leaderboard · analysis · traces mean ± SE · per-horizon · rationales
Dashed boxes are authored per use case; solid boxes ship in the core library. The red enforcer is the subject of the next section.
aieng-forecasting/aieng/forecasting/{data, evaluation, methods} · guides/0102 walk this loop end to end
02The load-bearing idea

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.

as_of ← visible in ForecastContext the future → spot close released_at = t market close released_at = t+1 bday exists, but publishes tomorrow → blocked official stats released_at = t+21 days publication lag → blocked
Filled = visible through 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 to timestamp; document reads are filtered by publication date the same way.
  • Honest released_at stamps 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.

data/cutoff.py · data/context.py · data/service.py · guide 01 — choosing released_at honestly
03Methods

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
predictor_id: str  ·  predict(task, context) → list[Prediction]

Baselines

LastValueHistoricalFrequencyCategoricalFrequency

The calibration floor. Anything that loses to naive is a finding.

Numerical

AutoARIMA · ETS · KalmanLinearRegression · LightGBMProphet (impl-local)

Darts-backed (Prophet aside), covariate-aware, probabilistic via sampling.

LLM Processes

QuantileGridSampledTrajectoryBinary / Categorical prob.

One structured LLM call — series + metadata in, distribution out. No tools.

Agentic

AgentPredictor= AgentConfig + prompt  builder + output schema

A multi-turn ADK agent with tools, wrapped to honor the same contract (§05).

The task's payload type fixes the metric: continuous → CRPS binary → Brier categorical → RPS — lower is better, everywhere.
evaluation/predictor.py · methods/{baselines, numerical, llm_processes, agentic} · LastValuePredictor is the annotated reference implementation
04Evaluation harness

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.

2025 2026 warmup from the spec development backtest open loop · cached · run freely origins — every stride-th step horizons resolve against later observations disjoint protected eval held-out · budgeted · uncached max_runs end trails the data by ≥ max(horizons)
An energy / S&P-shaped example (2025 backtest, 2026 eval). Every origin needs 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()
purposedevelop & tunethe committed answer
runsunlimitedmax_runs budget, if an EvalTracker is attached
cachingcached & resumablenever — spend stays visible
windowhistorical dev windowheld-out, often post-LLM-cutoff

Two protection styles in the repo

EnforcedEvalSpec + 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.

evaluation/{backtest, eval, artifacts}.py · implementations/*/specs/*.yaml · the results cache is keyed by ids, never by spec contents — bump spec_id when the window changes · guide 02 — specs, the cache-by-id gotcha, protected eval · guide 04 — don't believe the mean
05The agent stack

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_id
instructionpersona + supplements
modelLITE / ADVANCED
context_retrievalsearch sub-agent
code_executionE2B sandbox
function_toolse.g. run_forecast
extra_toolse.g. skill-mutation tools
skills_dirsSKILL.md playbooks

Composed 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

search_web query, cutoff grounded search sub-agent · own instruction brief verifier LLM independent model, strips post-cutoff ≤ 3 attempts filtered brief [SEARCH_VERIFICATION _FAILED]
run_code — E2B cloud sandbox. Fresh sandbox per call: prompt for self-contained scripts, batch-job style.
run_forecast — AutoARIMA behind a fixed, auditable interface. Statistics without code generation.
SkillToolset — SKILL.md playbooks, listed up front, loaded on demand. The adaptive agent's learned strategy is a mutable skill, edited through dedicated extra_tools.

Role — AgentPredictor

prompt_buildertask + context → payload
output_schemastructured forecast → Prediction

The payload carries as_of, horizons, quantile levels, and compressed history — wrap the builder to inject anything else: pre-cached news, engineered features, prior forecasts.

__as_of__ — the harness seeds the session cutoff, overriding whatever the model passes to search. Necessary, not sufficient: verification can fail, so agent backtests stay optimistic (§02).
Langfuse — with tracing configured, every prediction carries its trace URL. Read one full trace before trusting a score; guide 04 turns that habit into a method.
methods/agentic/{agent_factory, predictor}.py · analyst_agent (energy, BoC) · starter_agent (#1–#4) · guide 03 — every customization lever, with a worked change for each · which starter you are on — toolbelt vs toggles
06Reference implementations

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".

#  ImplementationTarget · payload CovariatesLLM-ProcessAgent predictorsCode execAdaptive skillsBudgeted evalDocs / reportsLLM judgeIrregular 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.

0 getting_started the loop itself: backtest → evaluate, CRPS, naive vs AutoARIMA 1 sp500 + discipline: leak-safe covariate panel, naive + five Darts methods, budgeted protected eval 2 food_price + context: nine targets at once, expert reports grounding the LLM-Process prompt 3 energy_oil + agency: news → code → tools, then an adaptive agent that trains its own strategy skill 4 boc_rate_decisions + judgment: categorical decisions on an irregular meeting calendar, LLM-judged rationales every step scores against the same harness
Each implementation stands alone — start at the problem closest to yours. The staircase is a reading order, not a dependency graph.

Where do you land?

SituationDo 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 Bguide 124 (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).
implementations/{getting_started, sp500_forecasting, food_price_forecasting, energy_oil_forecasting, boc_rate_decisions} · each README is the full walkthrough
07Extending the foundation

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.

guides/03-customize-agent-strategy.md — the levers

Path B — bring your own dataset

1 · Onboard data

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.

guides/01-onboard-a-dataset.md
2 · Declare an experiment

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.

guides/02-create-an-experiment.md
3 · Shape the agent

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.md
On either path, the shape of a new forecaster is always the same: implement Predictor 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.