Backtesting¶
Run your strategy against recorded market data.
Prerequisites¶
- Completed Recording Data (or have a CSV /
.floxlogfile) - Build / install with backtest support — see Bindings for per-language details
The pipeline¶
BacktestRunner 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.
To run the same strategy against a venue's fill mechanics, hand the
runner a VenueStack:
"""Run a BacktestRunner against a VenueStack's fill mechanics.
The runner feeds the stack's executor market data and harvests its fills,
so the run uses that venue's queue model and depth, iceberg refresh
latency, venue availability and rate limits.
Two things this does not change: fees still come from BacktestConfig
rather than the stack's FeeSchedule (fills carry is_maker, so per-side
rates work, but the schedule's 30-day volume tiering is not consulted),
and funding and liquidation are driven by explicit calls rather than the
replay loop. Read the result as "this venue's fill mechanics", not "this
venue's full economics".
"""
import pathlib
from collections import deque
import flox_py as flox
CSV = str(
pathlib.Path(flox.__file__).resolve().parent
/ "templates/research/data/btcusdt_sample.csv"
)
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()
def main():
registry = flox.SymbolRegistry()
btc = registry.add_symbol("binance", "BTCUSDT", tick_size=0.01)
bt = flox.BacktestRunner(registry, fee_rate=0.0004, initial_capital=10_000.0)
bt.set_strategy(CrossAboveSMA([btc]))
stack = flox.VenueStack.binance_um_futures(account_id=42, equity=10_000.0)
bt.set_venue_stack(stack) # pass None to revert to the built-in executor
stats = bt.run_csv(CSV, "BTCUSDT")
print(f"trades : {stats['total_trades']}")
print(f"return : {stats['return_pct']:.2f}%")
print(f"venue fills : {len(stack.executor().fills_list())}")
if __name__ == "__main__":
main()
The runner then feeds that stack's executor market data and harvests its fills, so the run uses the venue's queue model and depth, iceberg refresh latency, venue availability and rate limits.
Two things it does not change, so read the result as "this venue's fill mechanics" and not "this venue's full economics":
- Fees still come from
BacktestConfig, not from the stack'sFeeSchedule. Fills carryis_maker, somaker_fee_rate/taker_fee_ratecharge the real spread between posting and taking; what the stack still knows and the result does not is 30-day volume tiering. - Funding and liquidation are driven by explicit calls
(
FundingSchedulesettlement,LiquidationEngine.on_marks), not by the replay loop.
See Realistic backtest in one call and Cross-margin accounts.
Minimal example¶
A strategy that buys when price crosses above a 20-period SMA. 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_ratio']:.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 SMA
class CrossAboveSMA(Strategy):
sma: SMA
def __init__(self, symbols: List[int], period: int = 20):
super().__init__(symbols)
self.sma = SMA(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¶
Python and Codon use snake_case, Node.js camelCase. C++ reads the
same fields off BacktestStats.
| Python / Codon | Node.js | Description |
|---|---|---|
total_trades |
totalTrades |
Number of closed trades |
final_capital |
finalCapital |
Ending capital |
return_pct |
returnPct |
Total return % |
sharpe_ratio |
sharpeRatio |
Annualised Sharpe |
sortino_ratio |
sortinoRatio |
Annualised Sortino |
calmar_ratio |
calmarRatio |
Calmar ratio |
max_drawdown_pct |
maxDrawdownPct |
Worst drawdown |
win_rate |
winRate |
Win rate (0-1) |
profit_factor |
profitFactor |
Gross profit / gross loss |
Python BacktestRunner.run_* returns a plain dict; Codon returns a
BacktestStats object with attribute access; Node.js returns an object
literal. Every key follows the snake_case to camelCase rule with no
exceptions, and every producer emits all three risk ratios -- the C++
BacktestStats field name is the source both are derived from.
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.
Performance tips¶
- Release build —
cmake -DCMAKE_BUILD_TYPE=Release(pip install flox-pyalready gives you Release) - Filter symbols — only load what you need
- Pre-aggregate for repeated parameter sweeps; see Bar aggregation
- Avoid logging in callbacks — measure first; logging in the inner loop dominates
Next¶
BacktestRunner is a flat fee rate, no funding, no liquidation, no
rate limits. Good enough for a sanity check; not enough before live.
flox.VenueStack.binance_um_futures(account_id=42, equity=10_000)
gives you an executor, account, liquidation engine, fee schedule and
funding schedule with venue defaults, driven directly. See the links
below.
- Realistic backtest in one call — venue stack
- Cross-margin accounts — share equity across positions
- Liquidation and ADL — cascade behaviour
- Paper trading — same strategy class, live feed
- Connect FLOX to a CCXT exchange — promote to live
- Inspect a tape and run in the replay viewer
- Running a Backtest — fuller SMA crossover walkthrough
- Grid search — sweep parameters
- Realistic fills — slippage and queue position
- Bar aggregation — pre-aggregate offline for speed