A high-performance multi-symbol backtesting engine meticulously architected to mirror the Binance Futures trading environment. It combines a C++20 core built for speed and determinism with a React/TypeScript dashboard (BackBoard) that provides comprehensive visualization of simulation results.
This system is designed to simulate the intricacies of the Binance Futures ecosystem with high fidelity. It operates on a streamlined workflow:
- Market Data Ingestion: High-efficiency Parquet time series (OHLCV, Mark Price) located in
Data/. - Core Simulation: A C++ engine executing vectorized bar processing with precise handling of isolated margin, funding rates, and fee structures specific to Binance.
- Result Generation: Comprehensive output generation in timestamped directories under
Results/. - Visualization: Interactive analysis via BackBoard, reading saved run data from
Results/<run>/orResults/<run>/BackBoard/.
The engine supports multi-symbol portfolio backtesting in a single execution, maintaining explicit control over execution logic including bar assumptions, magnifier bar granularity, and isolated margin mechanics.
One real-world benchmark (user-measured):
- Universe: 10 symbols
- Period: ~7 years
- Trading bars: 1h
- Magnifier bars: 1m
- Runtime: ~10 seconds (typical)
Approximate scale (order-of-magnitude; depends on date range and missing bars):
- Trading bars per
symbol:
$7\nobreakspace\text{years} \times 365\nobreakspace\text{days} \times 24\nobreakspace\text{hours} \approx 61\nobreakspace\text{k bars}$ - Magnifier bars per symbol:
$61\nobreakspace\text{k} \times 60 \approx 3.7\nobreakspace\text{M bars}$ - Total magnifier bars (10 symbols):
$\approx 37\nobreakspace\text{M bars}$
Figure: Edit the strategy and run a backtest.
Figure: Run overview — equity curve and key metrics.
Figure: Per-symbol performance.
Figure: Symbol chart with indicators and executed trades.
Figure: Trade list (paged view).
Figure: Run manifest and runtime log snippet.
This is a minimal, end-to-end sketch of how a local backtest is typically configured using the public API.
#include "Engines/Backtesting.hpp"
#include <string>
#include <vector>
using namespace std;
using namespace backtesting::main;
int main() {
const vector<string> symbols = {"BTCUSDT", "ETHUSDT", "SOLUSDT"};
Backtesting::SetMarketDataDirectory("D:/Dev/Backtesting/Data");
// Core engine settings (project directory is used for outputs and source auto-detection)
Backtesting::SetConfig()
.SetProjectDirectory("D:/Dev/Backtesting")
.SetBacktestPeriod() // empty = full available range
.SetUseBarMagnifier(true);
// Bar streams
Backtesting::AddBarData(symbols, "1h", "D:/Dev/Backtesting/Data/Continuous Klines", TRADING);
Backtesting::AddBarData(symbols, "1m", "D:/Dev/Backtesting/Data/Continuous Klines", MAGNIFIER);
Backtesting::AddBarData(symbols, "1m", "D:/Dev/Backtesting/Data/Mark Price Klines", MARK_PRICE);
// Exchange metadata (optional but recommended)
Backtesting::AddExchangeInfo("D:/Dev/Backtesting/Data/exchange_info.json");
Backtesting::AddLeverageBracket("D:/Dev/Backtesting/Data/leverage_bracket.json");
Backtesting::AddFundingRates(symbols, "D:/Dev/Backtesting/Data/Funding Rates");
// Strategy (one per run)
// Backtesting::AddStrategy<MyStrategy>("My Strategy");
Backtesting::RunBacktesting();
return 0;
}After a run completes, BackBoard lists valid folders under Results/ and loads the selected run.
- C++ Engine (headers):
Includes/Engines/,Includes/Indicators/,Includes/Strategies/ - C++ Engine (implementation):
Sources/Cores/Engines/,Sources/Cores/Indicators/,Sources/Cores/Strategies/ - BackBoard (Electron + React + TypeScript):
Sources/Clients/- Main entry:
Sources/Clients/launch.js - Electron process:
Sources/Clients/Electron/Main.js - Server:
Sources/Clients/Servers/Launch.js(Express + WebSocket) - React application:
Sources/Clients/Apps/(TypeScript + Vite) - Strategy Editor:
Sources/Clients/Apps/Components/StrategyEditor/
- Main entry:
- Market data:
Data/Continuous Klines/(OHLCV Parquet)Mark Price Klines/(mark price Parquet)Funding Rates/(JSON)exchange_info.json,leverage_bracket.json
- Backtest outputs:
Results/<YYYYMMDD_HHMMSS>/ - BackBoard runtime directory:
BackBoard/(storeseditor.json, icons/logos, and C++ runtime binaries underCores/)
- Bar-driven execution
- Core processing is based on OHLC traversal; the exact intrabar ordering is controlled by the engine’s internal price-queue assumptions.
- Magnifier bars (optional)
- When enabled, the engine refines execution inside a trading bar using a smaller timeframe bar stream.
- Mark price integration
- Unrealized PnL and liquidation checks use mark price when available; when mark price data is missing, the engine falls back to market price.
- Isolated margin entries
- Each entry manages its own margin; concurrent entries are restricted to a single direction per symbol (no hedge-style long+short concurrency).
- Funding and margin accounting
- Funding fees are reflected on isolated margin, and funding receive/pay counts and amounts are exported per trade.
- Positions can add margin during a run, and liquidation-price changes caused by funding are logged.
- Slippage models
- The engine supports both percentage slippage and market-impact slippage through
Config::SetSlippage(...).
- The engine supports both percentage slippage and market-impact slippage through
- Single-strategy constraint per run
- The engine runs one
Strategyper backtest execution. - BackBoard can be used to compare/compose results across multiple independent runs.
- The engine runs one
- DLL-based Strategy/Indicator Loading
- Strategies and indicators are compiled into separate DLLs and loaded by the backtesting engine at runtime.
Bars are expected as Parquet with the following logical columns:
open_time(epoch ms)openhighlowclosevolumeclose_time(epoch ms)
Timeframes follow a compact string convention such as 1m, 1h, 1d.
Each run creates a timestamped directory:
Results/<YYYYMMDD_HHMMSS>/
config.json
trade_list.json
backtesting.log
Indicators/
<IndicatorName>/
<IndicatorName>.parquet
Sources/
<StrategyClass>.cpp
<StrategyClass>.hpp
<IndicatorClass>.cpp
<IndicatorClass>.hpp
When the engine is run outside BackBoard server mode, the same payload is stored under BackBoard/:
Results/<YYYYMMDD_HHMMSS>/
BackBoard/
config.json
trade_list.json
backtesting.log
Indicators/
<IndicatorName>/
<IndicatorName>.parquet
Sources/
<StrategyClass>.cpp
<StrategyClass>.hpp
<IndicatorClass>.cpp
<IndicatorClass>.hpp
... (BackBoard static assets may also be copied alongside)
Notes:
- BackBoard resolves result files from
Results/<run>/first, then falls back toResults/<run>/BackBoard/. config.jsonis a comprehensive run manifest (symbols, bar coverage, exchange/leverage/funding metadata, engine settings, strategy/indicator descriptors).trade_list.jsonis exported as UTF-8 with BOM for compatibility.Indicators/*stores indicator time series for plotted (non-OHLCV) indicators.Sources/*stores copies of the strategy/indicator source/header files when paths are available.- If a local BackBoard package is present at
Sources/Clients/BackBoard Package, it is copied into the run directory; otherwise, the engine can fetch a packaged BackBoard from a GitHub release as a fallback.
BackBoard is an Electron-based desktop application that serves two main purposes:
- Results Visualization: Interactive analysis of backtesting results
- Strategy Editor: Integrated development environment for creating and running backtests
After a backtest completes, BackBoard provides:
- Overview: Equity curve, Sharpe ratio, max drawdown, and key performance metrics
- Performance Analysis: Detailed statistics (win rate, profit factor, average trade duration, etc.)
- Plot: Equity/drawdown, net PnL by time reference, holding-time PnL distribution, and per-symbol performance
- Chart View: Interactive price charts with indicators and executed trades
- Trade List: Comprehensive trade history with filtering and sorting capabilities
- Trade Filter: Sidebar filter controls for symbols, strategies, entry/exit names, time ranges, holding time, and numeric trade fields
- Config: Run configuration and source/indicator metadata
- Logs: Execution logs with search/navigation support
- Log search supports result counting, previous/next navigation, direct click-to-select on highlighted matches, chunk navigation for large logs, and scrollbar-side search markers.
The Strategy Editor is a complete IDE for backtesting, accessible directly from BackBoard. It provides:
-
Symbol Management
- Configure trading symbols (BTCUSDT, ETHUSDT, etc.)
- Auto-detect available symbols from data directory
- Custom symbol pair selection
-
Bar Data Configuration
- Configure multiple timeframes (Trading, Magnifier, Reference, Mark Price)
- Support for standard timeframes (1m, 5m, 15m, 1h, 4h, 1d, etc.)
- Automatic data path detection from
Data/Continuous Klines/ - Download or update configured bar data from BackBoard
-
Exchange Settings
- Exchange info and leverage bracket configuration
- Funding rates directory selection
- Last data update tracking
-
Engine Configuration
- Project directory selection
- Backtest period (start/end dates or full range)
- Bar magnifier toggle
- Initial balance
- Fee configuration (taker/maker)
- Slippage models (Percentage or Market Impact)
- Quantity validation settings
- Bar data duplication checks
-
Strategy Selection
- Browse and select strategy DLLs
- Configure header/source directory paths for strategies and indicators
- Build strategy/indicator sources into
Builds/Strategies/<StrategyName>/<StrategyName>.dll - Automatic source file detection
-
Backtest Execution
- One-click backtest launch
- Strategy build before backtest execution
- Real-time log streaming
- Progress monitoring
- Stop capabilities for backtest and data fetch/update flows
- Automatic result directory creation
- Automatic result list refresh and selection after a successful run
The Strategy Editor is implemented as a React/TypeScript application with:
- State Management: Context-based architecture (
StrategyContext) managing all configuration state - Result Selection:
ResultsContextloads valid result folders fromResults/and shares the selected run across tabs - WebSocket Communication: Real-time bidirectional communication between UI and C++ engine
- Configuration Persistence: Settings saved to
BackBoard/editor.json - Process Management: Direct integration with backtesting engine via WebSocket server
- Filtering Workers: Trade filters use worker-side processing for large trade lists
- Open BackBoard and switch to "Strategy Editor" tab
- Configure symbols, bar data, exchange settings, and engine parameters
- Configure strategy/header/source paths and indicator header/source directories
- Click "Run Backtest" to launch
- BackBoard builds the strategy DLL, then starts the backtest
- Monitor real-time logs during execution
- View the automatically selected result in the Results Viewer upon completion
The editor eliminates the need for manual configuration files or command-line compilation for the normal BackBoard workflow.
The repository already includes a built-in Simple Moving Average indicator:
- Class:
SimpleMovingAverage - Header:
Includes/Indicators/SimpleMovingAverage.hpp
SMA is typically instantiated inside a strategy constructor via AddIndicator<T>(...) and stored as a reference:
// Example snippet inside a Strategy constructor
SimpleMovingAverage& sma = AddIndicator<SimpleMovingAverage>(
"sma", trading_timeframe,
Line(Rgba::orange, 2, SOLID, SIMPLE, false, 0, true), close, 20);SMA indicator code (based on the repository implementation):
// Includes/Indicators/SimpleMovingAverage.hpp
#pragma once
#include <vector>
#include "Engines/Indicator.hpp"
#include "Engines/Logger.hpp"
/// Simple Moving Average (SMA)
// Note: If this indicator is built/loaded as a DLL, the build system automatically handles proper linkage
class BACKTESTING_API SimpleMovingAverage final : public Indicator {
public:
explicit SimpleMovingAverage(const string& name, const string& timeframe,
const Plot& plot, Indicator& source,
double period);
private:
Indicator& source_;
double double_period_;
size_t sizet_period_;
int count_;
double sum_;
bool can_calculate_;
vector<double> buffer_;
size_t buffer_idx_;
void Initialize() override;
Numeric<double> Calculate() override;
};// Sources/Cores/Indicators/SimpleMovingAverage.cpp
#include "Indicators/SimpleMovingAverage.hpp"
#include <algorithm>
SimpleMovingAverage::SimpleMovingAverage(const string& name,
const string& timeframe,
const Plot& plot, Indicator& source,
const double period)
: Indicator(name, timeframe, plot),
source_(source),
double_period_(period),
sizet_period_(static_cast<size_t>(period)),
count_(0),
sum_(0.0),
can_calculate_(false),
buffer_(static_cast<size_t>(period), 0.0),
buffer_idx_(0) {
if (period <= 0) {
Logger::LogAndThrowError(
format("SimpleMovingAverage period [{}] must be > 0", period),
__FILE__, __LINE__);
}
}
void SimpleMovingAverage::Initialize() {
count_ = 0;
sum_ = 0.0;
can_calculate_ = false;
ranges::fill(buffer_, 0.0);
buffer_idx_ = 0;
}
Numeric<double> SimpleMovingAverage::Calculate() {
const double value = source_[0];
const double old = buffer_[buffer_idx_];
buffer_[buffer_idx_] = value;
buffer_idx_ = (buffer_idx_ + 1) % sizet_period_;
sum_ += value;
if (!can_calculate_) {
if (count_++ < static_cast<int>(sizet_period_) - 1) {
return NAN;
}
can_calculate_ = true;
} else {
sum_ -= old;
}
return sum_ / double_period_;
}Notes:
- OHLCV references (
open,high,low,close,volume) are provided by default. - Only non-OHLCV indicators with an active plot configuration are eligible for persistence under
Results/<run>/BackBoard/Indicators/.
This minimal example follows the same pattern used in the codebase (see TestStrategy):
- Adds one
SimpleMovingAverageindicator - Enters on price crossing SMA
Source path auto-detection (used for saving sources into Results/<run>/BackBoard/Sources/) expects:
Includes/Strategies/<ClassName>.hppSources/Cores/Strategies/<ClassName>.cpp
// Includes/Strategies/SmaStrategy.hpp
#pragma once
#include "Engines/Strategy.hpp"
class BACKTESTING_API SmaStrategy final : public Strategy {
public:
explicit SmaStrategy(const string& name);
~SmaStrategy() override;
void Initialize() override;
void ExecuteOnClose() override;
void ExecuteAfterEntry() override;
void ExecuteAfterExit() override;
private:
SimpleMovingAverage& sma_;
};// Sources/Cores/Strategies/SmaStrategy.cpp
#include "Strategies/SmaStrategy.hpp"
SmaStrategy::SmaStrategy(const string& name)
: Strategy(name),
sma_(AddIndicator<SimpleMovingAverage>(
"sma", "1h", Line(Rgba::orange, 2, SOLID, SIMPLE, false, 0, true),
close, 20)) {}
SmaStrategy::~SmaStrategy() = default;
void SmaStrategy::Initialize() {}
void SmaStrategy::ExecuteOnClose() {
const double position_size = order->GetCurrentPositionSize();
const double order_size = 0.01;
if (position_size == 0) {
if (close[0] > sma_[0] && close[1] < sma_[1]) {
order->MarketEntry("SMA Long", Direction::LONG, order_size, 10);
return;
}
if (close[0] < sma_[0] && close[1] > sma_[1]) {
order->MarketEntry("SMA Short", Direction::SHORT, order_size, 10);
return;
}
}
}
void SmaStrategy::ExecuteAfterEntry() {}
void SmaStrategy::ExecuteAfterExit() {}Important:
- The engine allows one strategy per backtest run.
- Constraint: Automatic source detection requires the file name and class name to match and be placed under
Includes/Strategies/+Sources/Cores/Strategies/orIncludes/Indicators/+Sources/Cores/Indicators/. - This follows the principle of isolating strategies into separate accounts; aggregation of multi-strategy ( multi-account) results will be supported in BackBoard in a future release.
- When compiling Strategies or Indicators as DLLs that the engine will load at runtime, the class declaration *
must* be annotated with the
BACKTESTING_APImacro so symbols are exported/imported correctly on Windows (MSVC). Example:
// Includes/Strategies/MyStrategy.hpp
class BACKTESTING_API MyStrategy final : public Strategy {
// ...
};- The same applies to indicators:
// Includes/Indicators/MyIndicator.hpp
class BACKTESTING_API MyIndicator final : public Indicator {
// ...
};- The
BACKTESTING_APImacro is defined inIncludes/Engines/Export.hppand resolves to__declspec(dllexport)whenBACKTESTING_EXPORTSis defined (building the DLL) and to__declspec(dllimport)when consuming it. Omitting the macro can cause unresolved symbols or runtime DLL load failures.
This repository is governed by the terms in the root LICENSE file.
In particular, it is provided for personal, educational, and non-commercial use only; commercial use requires prior
written permission from the author. Refer to LICENSE for the complete terms.
For commercial licensing inquiries: haseung.ryu.ai@gmail.com
