Backtesting¶
Run your strategy against recorded market data.
Prerequisites¶
- Completed Recording Data
- Recorded
.floxlogfiles - Build with
-DFLOX_ENABLE_BACKTEST=ON
1. BacktestRunner Overview¶
BacktestRunner replays recorded data through your strategy with simulated execution:
2. Minimal Example¶
#include "flox/backtest/backtest_runner.h"
#include "flox/replay/readers/binary_log_reader.h"
#include "flox/engine/symbol_registry.h"
#include "flox/strategy/strategy.h"
using namespace flox;
int main()
{
// 1. Load recorded data
replay::ReaderFilter filter;
filter.symbols = {1}; // Only symbol ID 1
auto reader = replay::createMultiSegmentReader("/data/market_data", filter);
// 2. Configure backtest
BacktestConfig config;
config.initialCapital = 10000.0;
config.feeRate = 0.0004; // 0.04% taker fee
BacktestRunner runner(config);
// 3. Create registry and strategy
SymbolRegistry registry;
SymbolInfo info;
info.exchange = "BINANCE";
info.symbol = "BTCUSDT";
info.tickSize = Price::fromDouble(0.01);
SymbolId symbolId = registry.registerSymbol(info);
MyStrategy strategy(symbolId, registry);
runner.setStrategy(&strategy);
// 4. Run
BacktestResult result = runner.run(*reader);
// 5. Get statistics
auto stats = result.computeStats();
std::cout << "Return: " << stats.returnPct << "%\n";
std::cout << "Sharpe: " << stats.sharpeRatio << "\n";
std::cout << "Max DD: " << stats.maxDrawdownPct << "%\n";
std::cout << "Trades: " << stats.totalTrades << "\n";
}
3. Writing a Backtest-Ready Strategy¶
Use Strategy base class with signal emission:
class MyStrategy : public Strategy
{
public:
MyStrategy(SymbolId sym, const SymbolRegistry& registry)
: Strategy(1, sym, registry) {}
void start() override { _running = true; }
void stop() override { _running = false; }
protected:
void onSymbolTrade(SymbolContext& ctx, const TradeEvent& ev) override
{
if (!_running) return;
// Your logic here
if (shouldBuy(ev.trade.price))
{
emitMarketBuy(symbol(), Quantity::fromDouble(1.0));
}
}
private:
bool _running{false};
};
Key points:
- Inherit from
Strategy, notIStrategy - Use
emitMarketBuy()/emitMarketSell()— BacktestRunner intercepts these signals - Access order book via
ctx.book, position viactx.position
4. BacktestConfig Options¶
BacktestConfig config;
config.initialCapital = 10000.0; // Starting capital
config.feeRate = 0.0004; // 0.04% per trade
config.slippage = 0.0; // Price slippage (0 = fill at signal price)
5. BacktestResult Statistics¶
auto stats = result.computeStats();
stats.initialCapital; // Starting capital
stats.finalCapital; // Ending capital
stats.returnPct; // Total return %
stats.totalTrades; // Number of trades
stats.winRate; // Win rate (0-1)
stats.sharpeRatio; // Annualized Sharpe
stats.sortinoRatio; // Sortino ratio
stats.maxDrawdownPct; // Maximum drawdown %
stats.profitFactor; // Gross profit / gross loss
6. Time Range Filtering¶
Backtest only a portion of your data:
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);
7. Using Bars Instead of Raw Data¶
For bar-based strategies, aggregate on the fly:
class BarStrategy : public Strategy
{
public:
BarStrategy(SymbolId sym, const SymbolRegistry& registry)
: Strategy(1, sym, registry)
{
_aggregator = std::make_unique<TimeBarAggregator>(
TimeBarPolicy(std::chrono::seconds(60)),
this // Strategy receives BarEvents
);
}
protected:
void onSymbolTrade(SymbolContext& ctx, const TradeEvent& ev) override
{
_aggregator->onTrade(ev);
}
void onBar(const BarEvent& ev) override
{
// Your bar-based logic
if (ev.bar.close > ev.bar.open)
{
emitMarketBuy(symbol(), Quantity::fromDouble(1.0));
}
}
private:
std::unique_ptr<TimeBarAggregator> _aggregator;
};
8. Inspecting Data Before Backtest¶
auto summary = replay::BinaryLogReader::inspect("/data/market_data");
std::cout << "Events: " << summary.total_events << "\n";
std::cout << "Duration: " << summary.durationHours() << " hours\n";
std::cout << "Segments: " << summary.segment_count << "\n";
9. Performance Tips¶
- Build in Release mode:
cmake .. -DCMAKE_BUILD_TYPE=Release - Filter symbols: Only load what you need
- Disable logging: Comment out
FLOX_LOGcalls - Use indexed data: Enables fast seeking
Next Steps¶
- Running a Backtest — Complete SMA crossover example
- Grid Search Optimization — Find optimal parameters
- Bar Aggregation — Pre-aggregate for faster backtests