Find the full reference docs here
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)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).
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.
-
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()
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)- get python 3.14
- run
pip install uv - run
uv run python --versionand it will create a venv for you
This project is using ruff for formatting and linting.
To format the project, run uv run ruff format.
To lint the project, run uv run ruff check.