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 /
.floxlogfile) - 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¶
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.
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¶
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.
- 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