A python library for evaluating trading strategies
Project description
backtest-lib
Find the full reference docs here
Usage
Quickstart
Below is an example of a buy-and-hold strategy uniform over the entire universe specified by the data in spot_prices.csv.
import polars as pl
import backtest_lib as btl
prices = pl.read_csv("docs/assets/data/spot_prices.csv")
market = btl.MarketView(prices)
initial_portfolio = btl.uniform_portfolio(market.securities, value=1_000_000)
def buy_and_hold(universe, current_portfolio, market, ctx):
return btl.hold()
backtest = btl.Backtest(
strategy=buy_and_hold,
market_view=market,
initial_portfolio=initial_portfolio,
)
results = backtest.run()
print("total return:", results.total_return)
results.values_held.plot().properties(width=1000, height=600)
Output:
Strategy
This library provides a lightweight framework for backtesting trading strategies. At its core, you define a strategy as a simple Python function that maps the current market state and portfolio into a decision about what to hold next. The library handles the rest: simulating trades over time, applying your decision rules at an optionally specified frequency, and generating performance statistics.
A strategy is any callable that returns a Decision:
Strategy = Callable[..., Decision]
Inputs are injected by parameter name (pytest-fixture style). Your strategy can request any subset of:
universe:tuple[str, ...]current_portfolio:backtest_lib.portfolio.Portfoliomarket:backtest_lib.market.MarketViewctx:backtest_lib.strategy.context.StrategyContext
At each decision point in the decision schedule, your strategy returns one Decision object.
Examples:
from backtest_lib import hold, target_weights
def equal_weight_strategy(universe):
return target_weights({sec: 1 / len(universe) for sec in universe})
def buy_and_hold_strategy():
return hold()
def monthly_rebalance(universe, market, ctx):
if ctx.now.day != 1 or len(market.prices.close.by_period) < 21:
return hold()
latest = market.prices.close.by_period[-1]
month_ago = market.prices.close.by_period[-21]
strength = {
sec: max(latest[sec] / month_ago[sec] - 1.0, 0.0)
for sec in universe
}
total = sum(strength.values())
if total == 0:
return hold()
return target_weights(
{sec: score / total for sec, score in strength.items()},
fill_cash=True,
)
Decision objects are created with helper functions such as hold, trade, target_weights, target_holdings, reallocate, and combine (all re-exported from backtest_lib).
Market
Inside the strategy function, the main way to interact with market data is through the MarketView object. This object provides a time-fenced view of historical prices, volumes, and tradability up to the current decision point. The data is time-fenced so that the strategy only sees information available at each step, as it marches forward through periods to reduce the risk of lookahead bias.
Main MarketView properties:
-
market.prices: access to OHLC price histories
-
market.volume: access to per-security volume histories
-
market.tradable: access to masks indicating which securities were tradable
Each of these is a PastView, which means we can:
Access the latest snapshot of close prices with market.prices.close.by_period[-1],
access the data for only the last 5 periods with market.prices.close.by_period[-5:],
access a single security’s full history with market.prices.close.by_security["AAPL"],
or restrict the view to a time window with market.volume.after(ctx.now - timedelta(days=90)).
For instance, if we wanted to calculate the rolling 30 day mean trading volume of MSFT, we can use the expression market.volume.after(ctx.now - timedelta(days=30)).by_security["MSFT"].mean()
More fleshed out: AAPL volume filter + momentum strategy
Assuming we are using daily data, we can implement a momentum/volume filter strategy like below. We keep the universe limited to a single security (AAPL) for simplicity.
def aapl_momentum_with_liquidity(
universe,
market,
):
if "AAPL" not in universe:
return hold()
aapl_close = market.prices.close.by_security["AAPL"]
aapl_tradable = market.tradable.by_security["AAPL"]
aapl_volume = (
market.volume.by_security["AAPL"] if market.volume is not None else None
)
momentum_lookback = 126 # ~6 months
vol_window = 60 # ~3 months
# make sure we have enough history
if len(aapl_close) < momentum_lookback + 1:
return hold()
# momentum: simple ratio of the current price over the price at (lookback) days ago.
recent_price = aapl_close[-1]
past_price = aapl_close[-(momentum_lookback + 1)]
momentum = (recent_price / past_price) - 1.0
# liquidity filter: average recent volume
if aapl_volume is not None and len(aapl_volume) >= vol_window:
avg_vol = aapl_volume[-vol_window:].mean()
vol_ok = avg_vol is not None and avg_vol > 0
else:
avg_vol = None
vol_ok = True # if no volume source, don't block the trade.
# make sure AAPL is tradable at the decision point.
tradable_now = bool(aapl_tradable[-1])
go_long = (momentum > 0.0) and vol_ok and tradable_now
target = {"AAPL": 1.0} if go_long else {"AAPL": 0.0}
return target_weights(target, fill_cash=True)
Building
- get python 3.14
- run
pip install uv - run
uv run python --versionand it will create a venv for you
Code style
This project is using ruff for formatting and linting.
Formatting
To format the project, run uv run ruff format.
Linting
To lint the project, run uv run ruff check.
Project details
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 backtest_lib-0.2.0.tar.gz.
File metadata
- Download URL: backtest_lib-0.2.0.tar.gz
- Upload date:
- Size: 224.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba93b82b2229ebdedac0e2bbf81515fad3431616717ba63a524ccacc7989fcde
|
|
| MD5 |
b1f84b90a8cc289172d8be6a39e28d87
|
|
| BLAKE2b-256 |
e2ef214fdddf5eadca5bef0849965962e7908f126f9297f10bf6f4dc755676e4
|
File details
Details for the file backtest_lib-0.2.0-py3-none-any.whl.
File metadata
- Download URL: backtest_lib-0.2.0-py3-none-any.whl
- Upload date:
- Size: 57.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dcb66f01d237aa417a139a8115bea3e3395cb8fec79657bf0058cca0f85c138d
|
|
| MD5 |
0b08097cd232876af6974f3c7366b9d9
|
|
| BLAKE2b-256 |
7b6715fa3e013f8d9ef79f484a4f7856c9da1b8279d06bb486d420d5590bce7a
|