Skip to content

Running a Backtest

Complete example of backtesting an SMA crossover strategy.

Build

Enable the backtest module:

cmake .. -DFLOX_ENABLE_BACKTEST=ON
make -j$(nproc)

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:

./preagg_bars /data/raw /data/bars 60 300 900 3600

See Also