Table of contents
nova-backtest
nova-backtest is a Python library for backtesting crypto trading strategies, published on PyPI as novalabs-backtest (version 1.1.10 in pyproject.toml). It downloads historical candles from Binance, Bybit, OKX, KuCoin, Huobi, MEXC, BTCEX and Oanda, runs a strategy over many pairs at once and reports statistics. This repository is an archived fork of the NovaLabs project; the commits made here in April 2023 rename the package directory and update the README. The interest of the code is how it simulates trades without a bar-by-bar loop.
1. Overview
A strategy is a subclass of BackTest (novalabs-backtest/utils/backtest.py) that fills three methods on a pandas DataFrame of candles:
build_indicators(df)adds indicator columns;entry_strategy(df)setsentry_signal(1 long, −1 short,NaNnothing),take_profit,stop_lossandposition_sizeon the signal candle;exit_strategy(df)setsexit_signal(orNaNeverywhere).
The constructor takes the exchange, the candle interval, the pairs (a list, "All pairs" or "Random N"), the period, the starting bankroll, the leverage, the maximum number of open positions and the maximum holding time. Each position gets leverage / max_pos of the bankroll, and the exchange’s maker fee comes from FEES in utils/constant.py (Binance 0.02%, Bybit 0.01%, OKX 0.015%).
2. Use cases
Python library to download crypto exchange candles and backtest a trading strategy with profit and drawdown stats.
- Download historical candles
- Strategy developerExchange REST API
BackTest.get_historical_data in novalabs-backtest/utils/backtest.py calls the exchange client and caches OHLCV data in database/<exchange>/hist_<pair>_<candle>.csv, starting from 2019-01-01.
- Pick an exchange client
- Strategy developerExchange REST API
clients() in novalabs-backtest/clients/clients.py returns a client for Binance, Bybit, OKX, Huobi, KuCoin, MEXC, Oanda or BTCEX, each following BackTestClientInterface.
- Define entry and exit rules
- Strategy developer
A strategy subclasses BackTest and fills build_indicators, entry_strategy (entry_signal, take_profit, stop_loss, position_size) and exit_strategy (exit_signal).
- Run the backtest
- Strategy developer
run_backtest simulates positions per pair with exchange fees, a maximum number of positions and a maximum holding time, then plots cumulative profit.
- Save performance statistics
- Strategy developer
The run writes results/<strategy>_pairs_stats.csv and results/<strategy>_overall_stats.json with returns, drawdown and per-pair figures.
- Build indicator features
- Strategy developer
TechnicalIndicatorsCreation in novalabs-backtest/utils/indicators.py adds lags, close ratios and support or resistance levels to a candle DataFrame.
2.1. Scenario: Backtest a MACD strategy on Binance pairs
- Install the dependencies listed in requirements.txt (the README's pip command spells the package novelabs-backtest).
- Subclass BackTest with exchange="binance", candle="1h", list_pairs=["BTCUSDT", "ETHUSDT"], a start bankroll, leverage and max_pos.
- Compute the MACD histogram in build_indicators and set entry_signal, take_profit and stop_loss in entry_strategy.
- Call run_backtest(); missing pairs are downloaded into database/binance/ on first use.
- Read the profit chart and the JSON and CSV files written under results/.
Files readREADME.mdnovalabs-backtest/utils/backtest.pynovalabs-backtest/clients/clients.pynovalabs-backtest/clients/client_interface.pynovalabs-backtest/utils/indicators.pyrequirements.txt
3. How a trade is simulated
run_backtest processes each pair with whole-column operations, then merges the pairs.
flowchart TD
D["get_historical_data(pair): candles from the exchange client"] --> I["build_indicators"]
I --> E["entry_strategy: entry_signal, take_profit, stop_loss"]
E --> V["_verify_tp_sl: TP and SL on the right side of the price"]
V --> P["_create_entry_prices_times: enter at the NEXT candle's open"]
P --> X["exit_strategy: exit_signal"]
X --> L["_create_closest_exit: look ahead i = 1 … N−1 candles"]
L --> F["_create_all_exit_point: earliest of SL, TP, max holding, exit signal"]
F --> O["_create_position_df: drop trades that overlap on the same pair, price the exit, compute P&L and fees"]
O --> A["_all_pairs_real_positions: merge pairs, sort by exit, drop trades beyond max_pos"]
A --> S["_create_full_statistics + Plotly equity graph"]
- Entry. A signal on candle t is filled at the open of candle t + 1, so the strategy cannot trade on the close it just used to decide.
- Look-ahead.
_create_closest_exitconverts the maximum holding time into a number of candles N and builds, for each shift i from 1 to N − 1, a column with the time candle t + i touched the stop or the target. For a long, the stop is hit when that candle’s low is at or below the stop; the target counts only when the high reaches it and the low stayed above the stop. When one candle touches both, the trade is recorded as stopped out, which is the cautious reading since the order of the two moves inside a candle is unknown. Shorts mirror this. - Exit. The exit time is the earliest of the closest stop, the closest target, entry + maximum holding, and the candle after an opposite
exit_signal. Stops and targets fill at their own price; the other two exits fill at the next open. - One trade at a time per pair. Trades whose entry falls before the previous trade’s exit on the same pair are removed.
- Profit and fees. On a notional of 100, the move is
side × (exit − entry) / entry, and fees are100 × (2 + move − fee) × fee: one fee on the way in, and one on the exit value after the first fee. - Portfolio cap. All pairs’ trades are merged and sorted by exit time; trades that would open while
max_pospositions are already open are deleted, because a real account could not have taken them. Position sizes areleverage / max_pos × bankroll, optionally compounding (geometric_sizes).
Worked trade. The README’s example strategy, MACD_HIST, goes long when the MACD histogram turns positive and puts the target and the stop tp_sl_delta = 0.05 away from the close. Say the next candle opens at 100 and, for simplicity, the signal close was also 100: the target is 105 and the stop 95. Candle t + 2 has a low of 97 and a high of 103, so nothing happens. Candle t + 3 has a low of 96 and a high of 105.5: the high reaches the target and the low stays above the stop, so the trade exits at 105 on that candle. The move is +5% and the Binance fee 0.02%, so the fees on a notional of 100 are 100 × (2 + 0.05 − 0.0002) × 0.0002 = 0.041 and the realised P&L is 5 − 0.041 = 4.959. With a leverage of 2 and max_pos = 4, each position uses half of the bankroll. (The candle values are illustrative; the steps and formulas are the code’s.)
4. Getting started
pip install novalabs-backtest
The README writes pip install novelabs-backtest, with a typo in the package name. A strategy then subclasses BackTest, implements the three methods and calls run_backtest(), which saves results/{strategy}_pairs_stats.csv and results/{strategy}_overall_stats.json. Some exchange clients need API keys passed as key, secret and passphrase.
5. Repository guide
novalabs-backtest/utils/backtest.py: theBackTestclass (1,262 lines): data, exits, positions, portfolio cap, statistics and plots.novalabs-backtest/clients/: one client per exchange behindclient_interface.py, created by theclients()factory.novalabs-backtest/utils/indicators.py,helpers.py,constant.py: indicators, interval conversions and fees.novalabs-backtest/data/: Discord, Reddit, Telegram and order-book collectors.tests/: client tests (format, earliest timestamp, history, pair info, server time, updates) and indicator tests..github/workflows/: tests, PyPI publishing and documentation builds;docs/is a Sphinx site.
6. Limits
- The look-ahead loop stops at N − 1 candles, one short of the maximum holding time, and its cost grows with N: two new columns per candle of holding time.
- Within one candle, a touch of both stop and target is always counted as a stop, even when the price may have reached the target first.
- Fees use the maker rate for both legs; market-order fills, funding and slippage are not modelled.
- The repository is archived, the directory name
novalabs-backtestcontains a hyphen (so it cannot be imported as written from source), and the README install command has a typo. - The README’s
MACD_HISTexample reads local CSV files and does not subclassBackTest, so it does not show the library’s real API. - The README badges point to
Nova-DevTeam/nova-backtestand a coverage badge in another repository name (nove-backtest).
7. Sources
- GitHub repository
- PyPI package
novalabs-backtest novalabs-backtest/utils/backtest.pynovalabs-backtest/utils/constant.py- README
8. CI/CD pipeline
3 GitHub Actions workflows, 3 jobs. Each box is a job; double-bordered boxes publish or deploy, and arrows follow needs.
Mermaid Pipeline flow
flowchart LR
subgraph wf0["ReadTheDocs Build"]
direction LR
wf0_deploy[["deploy<br/>ubuntu-latest"]]
end
subgraph wf1["Publish Python 🐍 distributions 📦 to PyPI"]
direction LR
wf1_build_n_publish[["Build & publish nova-backtest 🐍<br/>ubuntu-latest"]]
end
subgraph wf2["Python Test and Build"]
direction LR
wf2_deploy["deploy<br/>ubuntu-latest"]
end
t0(["push · manual run"])
t1(["pull request"])
t2(["push · pull request · manual run"])
d0[("GitHub Pages")]
d1[("PyPI")]
t0 --> wf0_deploy
wf0_deploy -.-> d0
t1 --> wf1_build_n_publish
wf1_build_n_publish -.-> d1
t2 --> wf2_deploy
| Workflow | Triggers | Jobs | Publishes to |
|---|---|---|---|
ReadTheDocs Build.github/workflows/make-docs.yml | push, manual run |
| GitHub Pages |
Publish Python 🐍 distributions 📦 to PyPI.github/workflows/publish-to-pypi.yml | pull request |
| PyPI |
Python Test and Build.github/workflows/run-test.yml | push, pull request, manual run |
| — |
Generated from the source at commit dda19e6.
9. Deployment diagram
Mermaid Deployment diagram
flowchart LR
repo[("GitHub repository")]
ci[["GitHub Actions<br/>3 workflows"]]
image["Docker image"]
pub_GitHub_Pages[("GitHub Pages")]
pub_PyPI[("PyPI")]
repo -->|"push or pull request"| ci
ci -->|"docker build"| image
ci -->|"publishes"| pub_GitHub_Pages
ci -->|"publishes"| pub_PyPI
10. External dependencies
14 declared packages (14 runtime, 0 development) in pyproject.toml, requirements.txt.
Package table 14
| Package | Version | Scope | Purpose | Registry |
|---|---|---|---|---|
pandas | ^2.0.0 | runtime | Data analysis | pypi.org |
numpy | ^1.24.2 | runtime | Data analysis | pypi.org |
pytz | ^2023.3 | runtime | World timezone definitions, modern and historical | pypi.org |
python-decouple | ^3.8 | runtime | Strict separation of settings from code. | pypi.org |
matplotlib | ^3.7.1 | runtime | Charts | pypi.org |
aiohttp | ^3.8.4 | runtime | HTTP client | pypi.org |
asyncio | ^3.4.3 | runtime | Deprecated backport of asyncio; use the stdlib package instead | pypi.org |
psaw | ^0.1.0 | runtime | Pushshift.io API Wrapper for reddit.com public comment/submission search | pypi.org |
telethon | ^1.27.0 | runtime | Full-featured Telegram client library for Python 3 | pypi.org |
requests | ^2.28.2 | runtime | HTTP client | pypi.org |
ta | ^0.10.2 | runtime | Market data and trading | pypi.org |
setuptools | — | runtime | Build tooling | pypi.org |
pytest | — | runtime | Testing | pypi.org |
typing | — | runtime | Type checking | pypi.org |
11. Glossary
Repository
The versioned source files, configuration, and history of a project.
README
The repository’s entry document, usually covering its purpose and initial setup.
Dependency
A library, tool, or service that another component needs to build or run.
API
An interface through which software components exchange requests and results. An API does not have to use HTTP.
HTTP
A request-and-response protocol used by browsers and web services. A method, path, headers, and optional body describe a request.
Published port
A host port mapped to a service’s container port. Publishing a port can expose that service beyond its container network.
CLI
A text interface in which commands and arguments select operations. Exit codes let scripts detect success or failure.
Build
The steps that turn source files and dependencies into a runnable program or distributable artifact.
Continuous integration
Automated checks that run when source changes, such as building, testing, and linting. Passing checks cover only what those checks exercise.
Mermaid
A text-based diagram language. Nodes and connections describe a model; the diagram itself does not execute the project.
Fork
A separate repository derived from another project. A change in a fork is not automatically a change in the upstream project.
12. References
- antoinebou12. (2026, November 5). nova-backtest. [Software repository] github.com.
- antoinebou12. (2026, November 5). nova-backtest: README. [Repository documentation] github.com.
- antoinebou12. (2026, November 5). novalabs-backtest/utils/backtest.py. [Source file] github.com.
- antoinebou12. (2026, November 5). novalabs-backtest/utils/constant.py. [Source file] github.com.
- antoinebou12. (2026, November 5). README.md. [Source file] github.com.
- Boucher, A. (2026, October 23). nova-backtest: Historical candles into a strategy simulation. [Project article] antoineboucher.info.
Sources without a publication date show the date they were last checked, November 5, 2026. File links retain the inspected revision where available.