Running a Backtest¶
Complete example of backtesting an SMA crossover strategy.
Build¶
Enable the backtest module:
Strategy¶
#include "flox/backtest/backtest_runner.h"
#include "flox/book/events/trade_event.h"
#include "flox/engine/symbol_registry.h"
#include "flox/replay/abstract_event_reader.h"
#include "flox/strategy/strategy.h"
#include <deque>
using namespace flox;
class SmaCrossover : public Strategy
{
public:
SmaCrossover(SymbolId symbol, size_t fast, size_t slow, Quantity size,
const SymbolRegistry& registry)
: Strategy(1, symbol, registry), _fast(fast), _slow(slow), _size(size) {}
void start() override { _running = true; }
void stop() override { _running = false; }
protected:
void onSymbolTrade(SymbolContext& ctx, const TradeEvent& ev) override
{
if (!_running)
return;
_prices.push_back(ev.trade.price.toDouble());
if (_prices.size() > _slow)
_prices.pop_front();
if (_prices.size() < _slow)
return;
double fast_sma = sma(_fast);
double slow_sma = sma(_slow);
bool above = fast_sma > slow_sma;
if (above && !_prev_above && !_long)
{
if (_short) { emitMarketBuy(symbol(), _size); _short = false; }
emitMarketBuy(symbol(), _size);
_long = true;
}
else if (!above && _prev_above && !_short)
{
if (_long) { emitMarketSell(symbol(), _size); _long = false; }
emitMarketSell(symbol(), _size);
_short = true;
}
_prev_above = above;
}
private:
double sma(size_t n) const
{
double sum = 0;
auto it = _prices.end();
for (size_t i = 0; i < n; ++i)
sum += *--it;
return sum / n;
}
size_t _fast, _slow;
Quantity _size;
std::deque<double> _prices;
bool _running{false}, _prev_above{false}, _long{false}, _short{false};
};
Main¶
int main(int argc, char* argv[])
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <data_dir> [symbol_id]\n";
return 1;
}
std::filesystem::path data_dir = argv[1];
uint32_t symbol_id = (argc > 2) ? std::stoul(argv[2]) : 1;
// Load data
replay::ReaderFilter filter;
filter.symbols = {symbol_id};
auto reader = replay::createMultiSegmentReader(data_dir, filter);
// Configure backtest
BacktestConfig config;
config.initialCapital = 10000.0;
config.feeRate = 0.0004; // 0.04% taker fee
BacktestRunner runner(config);
// Create registry and register symbol
SymbolRegistry registry;
SymbolInfo info;
info.exchange = "EXCHANGE";
info.symbol = "SYMBOL";
info.tickSize = Price::fromDouble(0.01);
registry.registerSymbol(info);
// Create strategy
SmaCrossover strategy(symbol_id, 10, 20, Quantity::fromDouble(1.0), registry);
runner.setStrategy(&strategy);
// Run
BacktestResult result = runner.run(*reader);
auto stats = result.computeStats();
// Output
std::cout << "Initial: " << stats.initialCapital << "\n";
std::cout << "Final: " << stats.finalCapital << "\n";
std::cout << "Return: " << stats.returnPct << "%\n";
std::cout << "Trades: " << stats.totalTrades << "\n";
std::cout << "Win rate: " << stats.winRate * 100 << "%\n";
std::cout << "Sharpe: " << stats.sharpeRatio << "\n";
std::cout << "Max DD: " << stats.maxDrawdownPct << "%\n";
return 0;
}
Output Example¶
Initial: 10000
Final: 10245.3
Return: 2.453%
Trades: 47
Win rate: 51.0638%
Sharpe: 1.23
Max DD: 3.21%
Mmap-Based Backtesting¶
For faster backtests with pre-aggregated bars, use MmapBarStorage:
#include "flox/backtest/mmap_bar_storage.h"
#include "flox/backtest/mmap_bar_replay_source.h"
// Load pre-aggregated bars
MmapBarStorage storage("/data/BTCUSDT/bars");
// Create replay source
MmapBarReplaySource source(storage, symbol_id);
// Replay bars through your strategy
source.replay([&](const BarEvent& ev) {
strategy.onBar(ev);
});
Pre-aggregate bars offline using preagg_bars:
See Also¶
- Grid Search Optimization — parameter optimization
- Bar Aggregation Pipeline — pre-aggregating bars
- Interactive Mode — step-by-step execution