explicit-backtest
A candle-driven backtest engine where the assumptions the engine cannot derive from the data are stated rather than implied.
Zero runtime dependencies — pure standard library. Python ≥3.10.
Quickstart
A strategy is any object with three methods. The engine owns everything else: order management, intrabar fills, fees and slippage, funding, equity tracking.
from datetime import datetime, timedelta, timezone
from explicit_backtest import (
BacktestConfig,
Candle,
StrategySignal,
run_backtest,
)
class BreakoutStrategy:
"""Long a 20-bar high, flat on a 10-bar low."""
name = "breakout_20_10"
def signal(self, candles, index, position_side):
prior = candles[max(0, index - 20) : index] # never includes this bar
if len(prior) < 20:
return StrategySignal(candles[index].open_time, "flat", "warmup")
close = candles[index].close
if position_side is None and close > max(c.high for c in prior):
side = "long"
elif position_side == "long" and close < min(c.low for c in prior[-10:]):
side = "flat"
else:
side = position_side or "flat"
return StrategySignal(candles[index].open_time, side, reason="breakout")
def atr_at(self, candles, index):
"""Volatility estimate that sizes the position and places the stop."""
window = candles[max(0, index - 13) : index + 1]
if len(window) < 14:
return None # warming up — the engine declines to open a position
return sum(c.high - c.low for c in window) / len(window)
def trailing_stop(self, side, candles, index):
return None # this strategy exits on its signal, not on a trail
start = datetime(2024, 1, 1, tzinfo=timezone.utc) # aware timestamps required
prices = [100 + i * 0.5 if i < 60 else 130 - (i - 60) * 0.8 for i in range(100)]
candles = [
Candle("BTC-PERP", "1h", start + timedelta(hours=i), p, p + 0.2, p - 0.2, p)
for i, p in enumerate(prices)
]
result = run_backtest(
candles, BacktestConfig(initial_equity=10_000), BreakoutStrategy()
)
print(f"final equity: {result.final_equity:,.2f}")
for t in result.trades:
print(
f"{t.side} {t.entry_price:.2f} -> {t.exit_price:.2f} "
f"net {t.net_pnl_usd:+.2f} fees {t.fee_usd:.2f} {t.exit_reason}"
)
final equity: 11,368.18
long 110.50 -> 125.20 net +1368.18 fees 9.94 signal_exit
Position size comes from the ATR estimate:
size = (risk_per_trade × equity) / (ATR × atr_stop_multiplier), with the
initial stop the same distance from entry. Both are frozen at entry.
What is stated rather than implied
Intrabar ordering is a deliberate, documented choice, not a side effect of code order. Several things can be true within one bar and the data cannot say which happened first. The engine picks one resolution, always the same one, and says which at the point where it happens:
- A signal is decided at bar i's close and is actionable at bar i+1's open — never at the close that produced it.
- On that bar an open-time (flat) signal exit resolves before the intrabar stop: live, it fills at the open, which precedes any intrabar touch of the stop on the same bar. Checking the stop first would take a later, worse fill and mislabel a signal exit as a stop-out.
- The stop is checked against the entry bar itself. A position fills at
that bar's open and the same bar's range can breach the stop before the next
iteration; skipping it gives every trade a free look at its entry bar, and
the realized loss can then exceed
risk_per_trade. - A stop or liquidation that gaps takes the worse of open or level: a long gapping below its stop fills at the open, not at the stop it jumped over.
The full per-bar order is in engine.run_backtest's module docstring.
The funding leg is off by default and refuses to run on thin coverage. No
funding_history means no funding. With one supplied, the engine measures the
history's coverage of the candle span at its own settlement cadence and
raises when it falls below funding_coverage_min (default 0.9) rather than
silently taking a default rate on the uncovered hours. Set
funding_coverage_min=None for a deliberate partial-coverage run.
Funding keys must be timezone-aware and on a UTC hour boundary, and candle timestamps must be timezone-aware. Both are checked, because both failure modes are silent: an unreachable key returns the default rate on every bar forever.
Fees and slippage are declared models, not baked-in constants — fee_bps
and slippage_bps on the config, plus a standalone orderbook-walking estimator
in slippage.simulate_fill for sizing a realistic slippage_bps.
The risk defaults are opinionated, and two of them act on a bare config:
max_leverage=2.0 caps notional at entry, and max_drawdown_kill=0.25 stops
the run at -25% from peak (BacktestResult.halted says so). For an engine that
only does what it is told:
BacktestConfig(risk=RiskConfig(max_leverage=float("inf"), max_drawdown_kill=None))
Not perp-only: funding defaults to zero, so the engine runs on any candle series.
What it deliberately does not do
- No metrics. The result is trades and an equity curve; Sharpe, drawdown
and the rest belong to whatever consumes it.
timeframe.pysupplies the bar duration those calculations annualize against. - No re-entry on an exit bar. Every exit ends that bar's work, so the earliest re-entry is the next bar's open.
- The last bar never opens a position. Signals come from bars
0..n-2and fill on the following bar, so the final candle can only close one. - One position at a time, one instrument at a time. No pyramiding, no portfolio, no cross-instrument margin.
- Equity points are stamped at a bar's
open_timebut valued at itsclose. The mark is the bar's outcome; the timestamp names the bar it belongs to. - No data loading. Candles come from the caller. That is why the dependency list is empty and stays empty.
Install
pip install explicit-backtest
Development
pip install -e ".[dev]"
ruff check src tests && ruff format --check src tests
pytest -q
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file explicit_backtest-0.1.0.tar.gz.
File metadata
- Download URL: explicit_backtest-0.1.0.tar.gz
- Upload date:
- Size: 36.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bfc3231265ff49cf04294e2fee6c026e2ae9879f2994f402bd60b25846a4cd48
|
|
| MD5 |
6cadb8cf350b973cdcab981339ca05a1
|
|
| BLAKE2b-256 |
ca1d2deef7d56dd75b7068e9d5e44cadd8a95311c81beecbb3dc488dda265e4c
|
Provenance
The following attestation bundles were made for explicit_backtest-0.1.0.tar.gz:
Publisher:
release.yml on bond-labs-dev/explicit-backtest
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
explicit_backtest-0.1.0.tar.gz -
Subject digest:
bfc3231265ff49cf04294e2fee6c026e2ae9879f2994f402bd60b25846a4cd48 - Sigstore transparency entry: 2449371192
- Sigstore integration time:
-
Permalink:
bond-labs-dev/explicit-backtest@995a67c9fbb694bdf80e4a27d5e47d13e9f1aedc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bond-labs-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@995a67c9fbb694bdf80e4a27d5e47d13e9f1aedc -
Trigger Event:
release
-
Statement type:
File details
Details for the file explicit_backtest-0.1.0-py3-none-any.whl.
File metadata
- Download URL: explicit_backtest-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba2c6140bd86f807ad1d926514b8d648448ff4ad96299cbfd33145a9578024e9
|
|
| MD5 |
3da64e916f0407d0c566cfa4d3c84596
|
|
| BLAKE2b-256 |
ab2ac5e33a5a6ffae2db69bea7001699a9f4195ffc2adfca57b6773397767744
|
Provenance
The following attestation bundles were made for explicit_backtest-0.1.0-py3-none-any.whl:
Publisher:
release.yml on bond-labs-dev/explicit-backtest
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
explicit_backtest-0.1.0-py3-none-any.whl -
Subject digest:
ba2c6140bd86f807ad1d926514b8d648448ff4ad96299cbfd33145a9578024e9 - Sigstore transparency entry: 2449371213
- Sigstore integration time:
-
Permalink:
bond-labs-dev/explicit-backtest@995a67c9fbb694bdf80e4a27d5e47d13e9f1aedc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bond-labs-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@995a67c9fbb694bdf80e4a27d5e47d13e9f1aedc -
Trigger Event:
release
-
Statement type: