Skip to content

Backtesting

Run your strategy against recorded market data. FLOX gives you two paths — one bare, one venue-realistic. Pick the second by default unless you have a specific reason to skip the venue physics.

Prerequisites

  • Completed Recording Data (or have a CSV / .floxlog file)
  • Build / install with backtest support — see Bindings for per-language details

Two pipelines

data file → VenueStack ← Strategy → outcome with fees + funding + liquidation + rate limits
                       (recommended)

data file → BacktestRunner → Strategy → SimulatedExecutor → BacktestResult
                       (minimal, no venue physics)

The bare BacktestRunner path runs your strategy through a SimulatedExecutor with a flat fee rate and nothing else. Useful for indicator sanity checks. Numbers it produces ignore funding, liquidation, queue position, rate limits, and venue outages — the forces that decide whether a perp strategy survives in production.

VenueStack wires the full venue physics in one call. Same strategy class, same fill model, same data source. The diff is what gets simulated around the fills.

Realistic backtest

import flox_py as flox

reg = flox.SymbolRegistry()
btc = reg.add_symbol("binance", "BTCUSDT", tick_size=0.01)

stack = flox.VenueStack.binance_um_futures(account_id=42, equity=10_000.0)
# stack.executor() / stack.account() / stack.liquidation() / stack.fees() / stack.funding()

stack.account() is a cross-margin Account; stack.liquidation() holds the configured MM tier ladder and ADL ranking; stack.fees() binds to the account so 30d VIP tier moves with aggregate notional; stack.funding() settles on the venue's interval. Other factories: bybit_linear, okx_swap, deribit. For custom venues see flox.assemble_custom_venue(...).

See Realistic backtest in one call for the full pattern and Cross-margin accounts for the account API.

Minimal example (bare path)

A strategy that buys when price crosses above a 20-period SMA. This runs through BacktestRunner directly — the bare path. Match the output against a known baseline; do not size positions off these numbers.

import flox_py as flox
from collections import deque

class CrossAboveSMA(flox.Strategy):
    def __init__(self, symbols, period=20):
        super().__init__(symbols)
        self.window = deque(maxlen=period)

    def on_trade(self, ctx, trade):
        self.window.append(trade.price)
        if len(self.window) < self.window.maxlen:
            return
        sma = sum(self.window) / len(self.window)
        if trade.price > sma and ctx.is_flat():
            self.market_buy(1.0)
        elif trade.price < sma and ctx.is_long():
            self.close_position()

reg = flox.SymbolRegistry()
btc = reg.add_symbol("binance", "BTCUSDT", tick_size=0.01)

strat = CrossAboveSMA([btc])
bt = flox.BacktestRunner(reg, fee_rate=0.0004, initial_capital=10_000)
bt.set_strategy(strat)

stats = bt.run_csv("/data/btcusdt_1m.csv", "BTCUSDT")
print(f"Return {stats['return_pct']:.2f}%  Sharpe {stats['sharpe']:.2f}  "
      f"DD {stats['max_drawdown_pct']:.2f}%  trades {stats['total_trades']}")
const flox = require('@flox-foundation/flox');

class CrossAboveSMA {
  constructor(symbols, period = 20) {
    this.symbols = symbols;
    this.period = period;
    this.window = [];
  }
  onTrade(ctx, trade, emit) {
    this.window.push(trade.price);
    if (this.window.length > this.period) this.window.shift();
    if (this.window.length < this.period) return;
    const sma = this.window.reduce((a,b)=>a+b, 0) / this.period;
    if (trade.price > sma && ctx.position === 0) emit.marketBuy(1.0);
    else if (trade.price < sma && ctx.position > 0) emit.closePosition();
  }
}

const reg = new flox.SymbolRegistry();
const btc = reg.addSymbol("binance", "BTCUSDT", 0.01);
const bt  = new flox.BacktestRunner(reg, 0.0004, 10_000);
bt.setStrategy(new CrossAboveSMA([btc]));

const stats = bt.runCsv("/data/btcusdt_1m.csv", "BTCUSDT");
console.log(`Return ${stats.returnPct.toFixed(2)}%  Sharpe ${stats.sharpeRatio.toFixed(2)}`);
from flox.runner import BacktestRunner
from flox.strategy import Strategy
from flox.context import SymbolContext
from flox.types import TradeData
from flox.indicators import StreamingSMA

class CrossAboveSMA(Strategy):
    sma: StreamingSMA
    def __init__(self, symbols: List[int], period: int = 20):
        super().__init__(symbols)
        self.sma = StreamingSMA(period)

    def on_trade(self, ctx: SymbolContext, trade: TradeData):
        v = self.sma.update(trade.price.to_double())
        if not self.sma.ready:
            return
        if trade.price.to_double() > v and self.position() == 0.0:
            self.market_buy(1.0)
        elif trade.price.to_double() < v and self.position() > 0.0:
            self.close_position()

bt = BacktestRunner(reg, fee_rate=0.0004, initial_capital=10_000.0)
bt.set_strategy(CrossAboveSMA([btc]))
stats = bt.run_csv("/data/btcusdt_1m.csv", "BTCUSDT")
#include "flox/backtest/backtest_runner.h"
#include "flox/replay/abstract_event_reader.h"

int main() {
  replay::ReaderFilter filter;
  filter.symbols = {1};
  auto reader = replay::createMultiSegmentReader("/data/btcusdt", filter);

  BacktestConfig config{ .initialCapital = 10000.0, .feeRate = 0.0004 };
  BacktestRunner runner(config);

  SymbolRegistry registry;
  SymbolInfo info{ .exchange = "binance", .symbol = "BTCUSDT",
                   .tickSize = Price::fromDouble(0.01) };
  auto symId = registry.registerSymbol(info);

  MyStrategy strat(symId, registry);
  runner.setStrategy(&strat);

  auto result = runner.run(*reader);
  auto stats  = result.computeStats();
  std::cout << "Return " << stats.returnPct << "%\n";
}

What's in stats

Field Description
total_trades / totalTrades Number of closed trades
final_capital / finalCapital Ending capital
return_pct / returnPct Total return %
sharpe / sharpeRatio Annualised Sharpe
sortino / sortinoRatio Annualised Sortino
max_drawdown_pct / maxDrawdownPct Worst drawdown
win_rate / winRate Win rate (0–1)
profit_factor / profitFactor Gross profit / gross loss

Time-range filtering

Use pandas to slice your CSV before passing in, or pass arrays to run_bars(start_time_ns, end_time_ns, ...) filtered to the window you want.

replay::ReaderFilter filter;
filter.from_ns = 1704067200000000000LL;   // 2024-01-01
filter.to_ns   = 1704153600000000000LL;   // 2024-01-02
filter.symbols = {1};
auto reader = replay::createMultiSegmentReader("/data", filter);

Performance tips

  1. Release buildcmake -DCMAKE_BUILD_TYPE=Release (pip install flox-py already gives you Release)
  2. Filter symbols — only load what you need
  3. Pre-aggregate for repeated parameter sweeps; see Bar aggregation
  4. Avoid logging in callbacks — measure first; logging in the inner loop dominates

Next

The BacktestRunner above is the bare path: flat fee rate, no funding, no liquidation, no rate limits. Good enough for a sanity check; not enough before live. The venue-realistic stack is one call away — flox.VenueStack.binance_um_futures(account_id=42, equity=10_000) wires executor, account, liquidation engine, fees, and funding with venue defaults. See the links below.