N°05 / Build

Backtest harness

Run the same Strategy class against historical tick data, get a full equity curve and summary stats. The Strategy doesn’t change — only the venues and context underneath. Paper-mode semantics, deterministic, fast.

Why this design

We don’t want a separate “backtest framework” with its own interface — that creates two codebases for one strategy. Instead we keep the Strategy / Venue / Context contracts and swap the implementations beneath them:

  • Live: Polymarket() → real WS → live PaperContext/LiveContext
  • Backtest: HistoricalVenue → tick stream from a TickSourceBacktestContext

Same Strategy code, same risk envelope, same handlers. Only the plumbing differs.

Run a backtest

from decimal import Decimal
from banger.backtest import Backtester, InMemoryTickSource

from my_strategy import MyStrategy

# Load ticks from wherever — in-memory list, parquet, Polymarket export, ...
source = InMemoryTickSource(ticks=[...])

result = Backtester(
    MyStrategy,
    source,
    starting_capital=Decimal("10000"),
).run()

print(result)
# → BacktestResult(return=$5.67 (0.06%), max_dd=$107.49, trades=118, sharpe=0.022)

Tick sources

A TickSource is anything that yields (Market, Tick) pairs in chronological order:

  • InMemoryTickSource(ticks) — pass a list. Useful for unit tests and small smoke runs.
  • ParquetTickSource(path) — reads a parquet file with our standard schema. (Coming soon — stub raises NotImplementedError today.)
  • PolymarketHistoricalSource(asset_ids, start, end)— pulls from Polymarket’s historical data API. (Coming soon.)

Roll your own

from banger.backtest import TickSource

class MyCsvSource(TickSource):
    def __init__(self, path):
        self.path = path

    async def universe(self):
        # Return all Markets present in your data.
        ...

    async def stream(self):
        # Async generator yielding (Market, Tick) pairs in time order.
        ...

BacktestResult

Returned by Backtester.run():

result.summary       # dict with total_return_usd, max_drawdown_usd, sharpe, win_rate, trades, ...
result.equity_curve  # list of (timestamp, equity_usd) pairs
result.fills         # list of every paper fill
result.save_csv("equity.csv")  # equity curve as CSV

Demo

See packages/sdk-python/examples/backtest_demo.py in the repo for a working end-to-end backtest against a synthetic random-walk source.

Caveats

  • Paper fills are at the last seen tick price. We don’t model slippage in v0. Real venue execution has slippage; the backtest is mildly optimistic.
  • Position sizing via ctx.kelly() assumes win_prob = 0.5 as a placeholder — pass your own edge calculation if precision matters.
  • Sharpe in the summary is a crude tick-bucketed approximation. For real analysis, dump the equity curve and compute properly downstream.

See also