Skip to main content

Backtest Storage Layer

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 Backtest Data
  • Report API: see Backtest Reports