An experiment on building a composable C++ trading backtesting library.
At this moment, the implementation covers only very basic features, in order to explore and validate design and architecture choices.
Criticism is very welcome!
- User Guide: https://jailop.codeberg.page/tzutrader/docs/
- API Reference: https://jailop.codeberg.page/tzutrader/docs/html/
#include <iostream>
#include <string>
#include <utility>
#include "tzu.h"
using namespace tzu;
int main(int argc, char** argv) {
bool verbose = (argc > 1 && std::string(argv[1]) == "-v");
RSIStrat strat;
BasicPortfolio portfolio(
100000.0, // initial capital
0.001, // trading fee 0.1%,
0.10, // stop-loss 10%
0.20 // take-profit 20%
);
Csv<Ohlcv> csv(std::cin);
BasicRunner<BasicPortfolio, RSIStrat, Csv<Ohlcv>> runner(
portfolio,
strat,
csv);
runner.run(verbose);
return 0;
}Build the backtesting example:
cd build && cmake .. && cmake --build . && cd ..
The example reads CSV OHLCV data from stdin. That file includes data from 2015 to 2026.
cat tests/data/btcusd.csv | ./build/example01
The output:
init_time:1419984000 curr_time:1767052000 init_cash:100000.0000
curr_cash:197422.2894 num_trades:92 num_closed:46 num_wins:28
num_losses:18 win_rate:0.6087 num_stop_loss:18 num_take_profit:7
quantity:0.0000 holdings:0.0000 valuation:197422.2894
total_costs:14952.7706 profit:97422.2894 total_return:0.9742
annual_return:0.0638 buy_and_hold_return:277.2788
buy_and_hold_annual:0.6677 max_drawdown:0.5280 sharpe:0.3694
Doing the output prettier with Unix tools:
cat tests/data/btcusd.csv | ./build/example01 | tr ' ' '\n' | column -t -s ':'
Will produce:
init_time 1419984000
curr_time 1767052000
init_cash 100000.0000
curr_cash 197422.2894
num_trades 92
num_closed 46
num_wins 28
num_losses 18
win_rate 0.6087
num_stop_loss 18
num_take_profit 7
quantity 0.0000
holdings 0.0000
valuation 197422.2894
total_costs 14952.7706
profit 97422.2894
total_return 0.9742
annual_return 0.0638
buy_and_hold_return 277.2788
buy_and_hold_annual 0.6677
max_drawdown 0.5280
sharpe 0.3694
- The
Csvclass reads OHLCV data from a CSV file and streams it asOhlcvobjects. - The
RSIStratclass implements a simple RSI-based trading strategy. It generates buy/sell signals based on RSI thresholds. - The
BasicPortfolioclass manages the trading portfolio, tracking cash, holdings, and performance metrics. - The
BasicRunnerclass orchestrates the backtesting process, feeding data to the strategy and updating the portfolio accordingly.
Here is the RSIStrat's update method:
Signal update(const Ohlcv& data) {
double rsi_value = rsi.update(data);
Signal signal = {data.timestamp, Side::NONE, data.close};
if (std::isnan(rsi_value))
return signal;
if ((rsi_value < oversold) && (last_side != Side::BUY))
last_side = signal.side = Side::BUY;
else if ((rsi_value > overbought) && (last_side != Side::SELL))
last_side = signal.side = Side::SELL;
return signal;
}- Indicators: SMA, EMA, Moving Variance, RSI, and MACD.
- Data types: OHLCV, trades, and single value time series.
- Input data formats: csv
- Built-in Strategies: Crossover, RSI, and MACD.
- Basic portfolio management
- Initial testing suite for the core components
Documentation is available through the user guide and API reference listed above, with tutorials for creating custom indicators and strategies, and detailed explanations of the library's architecture.
The library is designed into several core components:
- Data Streamers: Responsible for reading and streaming input data in various formats (e.g. CSV).
- Indicators: Implement various indicators (e.g. SMA, EMA, RSI, MACD) that can be used by trading strategies to generate signals. Indicators are implemented using circular buffers to efficiently compute values.
- Strategies: Implement trading strategies that generate buy/sell signals based on the indicators' values. Strategies can be as simple or complex as needed, and can use multiple indicators to make decisions.
- Portfolio Management: Manages the trading portfolio, tracking cash and holdings. It handles the execution of trades, applying trading fees, and making decisions about position sizing, stop-loss, and take-profit.
- Performance Metrics: Computes various performance metrics, such as total return, annualized return, buy-and-hold return, max drawdown, and Sharpe ratio.
- Runner: Orchestrates the backtesting process, feeding data to the strategy and updating the portfolio accordingly.
The library is intended to let users to easily implement their own custom indicators, strategies, and portfolio management approaches, by providing clear interfaces and a modular design that allows to easily swap out different components.
- Designed to be as simple and lightweight as possible, with a focus on core functionality.
- Built to be easily composable, with a modular design that allows to easily swap out different components, as well as to implement their own custom indicators, strategies, and portfolio management approaches.
- The library processes data in a streaming fashion, allowing it to handle large datasets without needing to load everything into memory at once. This also allows for more realistic backtesting, as it simulates the way that real trading systems operate, processing data as it arrives, and protecting against look-ahead bias.
The library is inspired by the Unix philosophy of building small, composable tools that do one thing well, and can be combined together to create more complex systems.
- Improve the architecture and design of the library, based on feedback and experimentation.
- Add more examples to demonstrate the library's features and usage.
- Add support for the most common input data formats, e.g. JSON.
- Implement a minimal but useful set of built-in trading strategies and indicators.
- Add support for a more realistic portfolio, risk and order management features, e.g. multiple assets, position sizing, slippage, etc.
Not considered:
- Include an extensive set of built-in strategies and indicators. The library is designed to be small, and users are encouraged to implement their own custom strategies and indicators using the provided interfaces.
- Support for data retrieval APIs. The library is designed to be agnostic to data sources, and users can implement their own data retrieval logic using the provided interfaces.
To build the documentation locally:
make -f Makefile.docs all
This will generate:
- User guide and tutorials in
./docs/ - API reference in
./docs/html/
Requirements:
- mkdocs:
pip install mkdocs - doxygen:
apt install doxygen(or equivalent)
To serve the documentation locally:
make -f Makefile.docs serve
Available targets: all, docs, api, serve, clean, rebuild, help