Backtesting engine implemented in two languages with a unified Streamlit interface. The Python implementation serves as a readable reference; the C++17 core handles performance-critical runs over long historical series.
DataHandler → MarketEvent
Strategy → SignalEvent
Portfolio → OrderEvent
Broker → FillEvent → Portfolio::update()
The standard alternative — vectorized backtesting — applies a strategy across an entire price series at once. This introduces look-ahead bias: the strategy can inadvertently use future information when computing signals, producing results that are impossible to replicate in live trading.
An event-driven engine eliminates this by processing time strictly forward. Each
bar arrives as a MarketEvent, the strategy reacts with a SignalEvent, the
portfolio converts it to an OrderEvent, and the broker returns a FillEvent.
No component can reference data beyond its current position in the queue.
The baseline strategy uses two Simple Moving Averages with windows fast < slow.
- Entry: go long when the fast SMA crosses above the slow SMA — the short-term trend accelerating relative to the long-term trend is interpreted as bullish momentum
- Exit: close the position when the fast SMA crosses back below the slow SMA
fast_ma = mean(Close[-fast:])
slow_ma = mean(Close[-slow:])
signal = LONG if fast_ma > slow_ma and not invested
signal = EXIT if fast_ma < slow_ma and invested
This is a trend-following strategy: it profits in trending markets and underperforms in mean-reverting or choppy regimes. The SMA windows control the sensitivity/lag trade-off.
The SMACrossWithStopLoss extension adds a dynamic stop-loss derived from the
Average True Range (ATR), a measure of recent realised volatility:
ATR(n) = mean(High - Low) over the last n bars [simplified]
stop_price = entry_price - atr_mult × ATR(atr_window)
The stop-loss exits the position if the price closes below stop_price, regardless
of the SMA crossover signal. This limits drawdown in fast-moving adverse markets
without waiting for the slower crossover signal to flip.
The parameter atr_mult controls the tightness of the stop: a larger multiplier
tolerates more volatility before exiting; a smaller one cuts losses earlier at the
cost of more false exits.
The SMACrossWithTrendFilter extension adds a long-term trend filter: a long
entry is only allowed if the current price is above its 200-period moving average,
a classic proxy for a bull market regime.
trend_ma = mean(Close[-trend_window:])
in_uptrend = price > trend_ma
entry condition: fast_ma > slow_ma AND in_uptrend
This avoids buying into crossovers that occur in a broader downtrend, which historically tend to produce poor risk-adjusted returns. The exit condition is also extended: the position is closed if either the SMA crossover reverses or the price falls back below the trend MA.
All metrics are computed on the equity curve and benchmarked against a configurable index (default: SPY).
where
where
The worst peak-to-trough decline over the entire period. Measures the worst realised loss an investor would have experienced before recovery.
Return per unit of maximum drawdown. Unlike the Sharpe ratio, it uses the worst-case historical loss as the risk denominator, which is more intuitive for strategies where tail events dominate the risk profile.
backtest/
├── core/ ← C++17 engine (performance-critical)
│ ├── include/
│ │ ├── events.hpp # std::variant<MarketEvent, SignalEvent, OrderEvent, FillEvent>
│ │ ├── bar.hpp # OHLCV struct
│ │ ├── data_handler.hpp
│ │ ├── strategy.hpp # Strategy base + SMACrossStrategy
│ │ ├── portfolio.hpp
│ │ ├── broker.hpp # fill at open, 0.1% flat commission
│ │ ├── performance.hpp # Sharpe, CAGR, Max DD, Calmar
│ │ └── export.hpp # CSV export
│ └── src/
│ └── main.cpp
│
├── strategies/ ← Python engine (reference implementation)
│ ├── events.py # dataclasses: MarketEvent, SignalEvent, OrderEvent, FillEvent
│ ├── data_handler.py # yfinance, sequential bar streaming
│ ├── strategy.py # SMACross, SMACrossWithStopLoss, SMACrossWithTrendFilter
│ ├── portfolio.py # position sizing, equity tracking
│ ├── broker.py # simulated execution at open + commission
│ ├── performance.py # metrics computation
│ └── runner.py # event loop
│
├── app.py ← Streamlit, single entry point
├── scripts/
│ └── download_data.py
├── CMakeLists.txt
├── Justfile
└── pyproject.toml
Python strategies/ |
C++17 core/ |
|
|---|---|---|
| Event dispatch | queue.Queue + if/elif |
std::queue + std::visit |
| Event types | dataclasses |
std::variant |
| Data source | yfinance live | pre-downloaded CSV |
| Output | inline Streamlit | results/*.csv |
| Use case | rapid prototyping, strategy development | long-period backtest, performance runs |
The Python implementation is intentionally the reference: readable, easy to extend with new strategies. The C++ core mirrors the same event loop and is invoked as a subprocess from the Streamlit app, writing results to CSV for the UI to consume.
# 1. Download data
just download AAPL,MSFT
# or: python scripts/download_data.py AAPL MSFT --start 2015-01-01 --end 2024-01-01
# 2. Build C++ core
just build
# or: cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j4
# 3. Launch UI
just app
# or: streamlit run app.py
# Full workflow in one command
just all AAPL,MSFTdocker compose up --buildOpen http://localhost:8501. The data/ and results/ directories are mounted
as local volumes — data and results persist across container restarts.
The current implementation makes several simplifying assumptions that a production system would address:
- Execution model: fills at next open with flat 0.1% commission. No slippage, no partial fills, no market impact.
- Position sizing: fixed fraction of capital per trade. No Kelly criterion or volatility-scaled sizing.
- Data: daily OHLCV only. No tick data, no intraday resolution.
- Strategy universe: single-asset or independent multi-asset. No cross-asset correlation or portfolio-level risk constraints.
Planned extensions: transaction cost modelling with market impact, volatility-scaled position sizing, walk-forward optimisation to avoid in-sample overfitting.