Skip to content

Backtesting

tradectl backtests replay historical market data through your strategy with a simulated exchange that handles order fills, TP / SL execution, slippage, and fees.

Data model

Backtests use ticker + trade streams (and optionally klines + L2 depth) packaged into a single optimized file.

SourcePurpose
bookTickerBest bid/ask snapshots — drives on_ticker
aggTrades (Binance) / @tradeAggregate trades — drives on_trade, fills limit orders
klinesOptional, for indicators that don't synthesize from ticks
depthOptional L2 — surfaces in StrategyContext::depth

Raw market-data files are prepared into the TCTL v3 binary format — a single-file pack of all streams with a footer index for mmap'd loading:

bash
tradectl prepare \
  --root data/raw \
  --exchange binance \
  --symbol BTCUSDT \
  --interval 2026-01-01:2026-02-01 \
  -o data/prepared.bin

Output formats and load times:

FormatExtensionLoad timeNotes
TCTL v3 binary.bin~18 µs (mmap)Default. Single-file 4-stream pack with footer index
Parquet.parquet~360 msPortable, ZSTD-compressed
CSV / JSONL.csv / .jsonlslowestPre-existing data — convert with tradectl prepare

Prepare options

FlagDescription
--root <DIR>Raw data directory (tradectl collect writes here)
--exchange <NAME>Source exchange
--symbol <SYM>Symbol to prepare
--interval <RANGE>YYYY-MM-DD:YYYY-MM-DD
--ticker-data <PATH>Or supply a single bookTicker file directly
--trade-data <PATH>Or supply a single trades file directly
--trades-onlySynthesize tickers from trades when bookTicker is unavailable
--ticker-bucket-ms <N>Synthesis bucket size (default 100)
--buffer-ms <N>Windowing buffer (default 20, 0 = off)
-o, --output <PATH>Output (.bin or .parquet)

Binance gotcha

On Binance USD-M futures, the @aggTrade stream stopped delivering messages in early 2026 — subscriptions ACK but no events flow. Use @trade instead. The CLI's tradectl collect binance already does this. If you have older @aggTrade recordings, they remain valid.

Running a backtest

Via CLI

bash
tradectl backtest \
  -d data/prepared.bin \
  --balance 10000 \
  --leverage 5 \
  --taker-fee 0.0004 \
  --maker-fee 0.0002 \
  --slippage 0.0001 \
  -p order_size=0.001 \
  -p tp_pct=0.5 \
  -v
FlagDescriptionDefault
-d, --data <PATH>Prepared data filerequired
--balance <FLOAT>Initial balance (USD)10000
--leverage <FLOAT>Leverage multiplier1.0
--taker-fee <FLOAT>Taker fee rate0.0004
--maker-fee <FLOAT>Maker fee rate0.0002
--slippage <FLOAT>Slippage (fraction)0.0001
-p, --param <KEY=VAL>Strategy parameter (repeatable)
-v, --verbosePrint individual tradesfalse
--benchProfile latency / throughputfalse

Via dashboard

  1. BacktestsNew Backtest
  2. Select a strategy
  3. Configure symbols, date range, fees, leverage
  4. Run

Results stream in real-time via WebSocket (backtest:<id>:progress).

Via lab

The local lab runs the same engine with an interactive UI for picking the data file, tweaking params, and inspecting trades. Backtests run in a probed subprocess so a strategy panic doesn't crash the lab.

Results

Each backtest produces:

MetricDescription
Total PnLNet profit/loss percentage
Win RatePercentage of winning trades
Max DrawdownLargest peak-to-trough decline
Sharpe RatioRisk-adjusted return
Profit FactorGross profit / gross loss
Trade CountTotal number of round-trip trades
Scorepnl% × min(trades / min_trades, 1.0) / (1 + max_dd%)

Plus an equity curve, drawdown curve, and the full trade log.

Simulated exchange

The InMemoryExchange simulates real exchange behavior. The mechanics are explicit because they affect score interpretation:

BehaviorDetail
Market entryFills at ask + slippage (long) or bid - slippage (short)
Limit entryFills when ticker price crosses limit (uses <= on long buys — fills at-price, where live exchanges may not)
TP exitExact TP price, maker fee
SL exittrade_price + slippage, taker fee
Force-close at endUses the last ticker's bid/ask, not the current trade price
Partial fillsSupported
Delay exitsHonored — ExitOrder.delay_ms defers placement on the simulator just like the live runner

These differences are documented because they materially shift score for high-frequency limit-order strategies. Always validate a strategy in paper mode (isEmulator: true) before going live.

tradectl — Automate Crypto Trading