Skip to main content

Backtest Storage Layer

This page covers organizing, indexing, and querying collections of backtest bundles. For the contents and portability of an individual .obtf bundle, see Open Backtest Format.

Once you start sweeping parameter grids and walk-forward windows, you quickly end up with hundreds or thousands of backtests on disk. Comparing them all in a single HTML dashboard stops scaling at that point — every comparison re-decodes multi-MB metric blobs just to read a Sharpe number, and the resulting report.html becomes too heavy for a browser to open.

The backtest storage layer solves this with a simple idea: save your backtests to a folder, build a small SQLite index next to them, and filter/rank in that index before you ever open a bundle or render a report.

Just have a handful of backtests?

If you're only running a few backtests at a time, you probably don't need any of this — just pass them straight to BacktestReport as shown in Backtest Reports. Come back here once you're sweeping large parameter grids.

Quick Start: the 5-step workflow​

This is the loop most users run, whether they have 10 backtests or 10,000.

1. Run a sweep, persist the bundles​

from investing_algorithm_framework import BacktestRunConfiguration
from investing_algorithm_framework import Study, Universe, BacktestWindow

study = Study(
universe=Universe(market="bitvavo", trading_symbol="EUR"),
backtest_windows=[
BacktestWindow(train_range=dr)
for dr in [range_2022, range_2023, range_2024]
],
)

backtests = app.run_backtests(
strategies=[StrategyA(), StrategyB(), StrategyC()],
study=study,
run_configuration=BacktestRunConfiguration(
n_workers=-1,
backtest_storage_directory="./my-backtests/",
show_progress=True,
),
)

This gives you a folder of .obtf bundle files — the single source of truth for everything downstream.

2. Build the index​

iaf index ./my-backtests/

Or from Python:

from investing_algorithm_framework.cli.index_command import build_index

build_index("./my-backtests/")

This writes an index.sqlite file next to your bundles, with every summary metric (Sharpe, Calmar, number of trades, etc.) promoted to its own column. It's idempotent — re-run it any time after adding new bundles.

3. Filter / rank without opening any bundles​

from investing_algorithm_framework.cli.index_command import rank_index

# Top 20 by Sharpe, but only among bundles with > 50 trades.
top = rank_index(
"./my-backtests/",
by="sharpe_ratio",
where="summary_number_of_trades > 50",
limit=20,
)

for r in top:
print(r["algorithm_id"], r["summary_sharpe_ratio"])

Or from the shell:

iaf rank ./my-backtests/ --by sharpe_ratio \
--where "summary_number_of_trades > 50" -n 20

This is sub-100 ms even over 10k+ bundles — no bundles are opened at this step.

4. Load only the bundles you need​

from investing_algorithm_framework.services.backtest_store import (
LocalDirStore,
)

store = LocalDirStore("./my-backtests/")
backtests = [store.open(row["bundle_path"]) for row in top]

You only pay the decode cost for the backtests you actually selected.

5. Render the report​

from investing_algorithm_framework import BacktestReport

BacktestReport(backtests=backtests).save("top20.html")

Keeping report.html fast​

BacktestReport inlines every backtest's full data (equity curve, trades, positions, etc.) into one self-contained HTML file, so size grows with how many backtests you put in it:

Backtests in reportApprox. HTML sizeBrowser experience
1 – 10tens of KB to ~1 MBinstant
10 – 50a few MBsmooth
50 – 20010 – 50 MBslower, still usable
200+100 MB+browsers struggle / refuse to open

Rule of thumb: keep any single report to ≤ 50 backtests. Use the index (step 3 above) to pick the winners, and render a few focused reports (top strategies, best Calmar, one per regime) instead of one giant one:

# DON'T: decodes and inlines every bundle in the folder.
report = BacktestReport.open(directory_path="./my-backtests/")
report.save("everything.html") # can be 100s of MB

# DO: filter first, then render only the winners.
top = rank_index("./my-backtests/", by="sharpe_ratio", limit=25)
store = LocalDirStore("./my-backtests/")
BacktestReport(
backtests=[store.open(r["bundle_path"]) for r in top],
).save("top25_by_sharpe.html")

Scaling further: LocalTieredStore​

LocalDirStore (a flat folder of .obtf files) is the default and is enough for most users — it's simple to inspect and works with normal tools (ls, rsync, git lfs).

If your collection grows very large, LocalTieredStore is a drop-in replacement with the same API that adds:

  • Cross-bundle analytics over Parquet sidecars, without decoding bundles (e.g. with DuckDB/Polars).
  • OHLCV deduplication across bundles that reference the same market data.
from investing_algorithm_framework.services.backtest_store.\
local_tiered_store import LocalTieredStore

store = LocalTieredStore("./my-backtests/") # same API as LocalDirStore

Migrate an existing collection with:

iaf migrate-store --from local-dir    --src ./my-backtests/ \
--to local-tiered --dst ./tiered/

Full example​

A complete, runnable example (seed bundles → build index → rank → load winners → render dashboard) lives at examples/storage_layer_demo/:

source .venv/bin/activate
python examples/storage_layer_demo/demo.py

Reference​

  • CLI: iaf index, iaf list, iaf rank, iaf migrate-store (see iaf <cmd> --help)
  • Python: investing_algorithm_framework.cli.index_command.{build_index, list_index, rank_index}
  • Stores: investing_algorithm_framework.services.backtest_store.{LocalDirStore, LocalTieredStore}
  • Bundle format: see Open Backtest Format
  • Report API: see Backtest Reports