Skip to main content

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: The output chart of the above result

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 must conform to the following structure:

class Strategy(Protocol):
    def __call__(
        self,
        universe: Universe,
        current_portfolio: WeightedPortfolio,
        market: MarketView,
        ctx: StrategyContext | None,
    ) -> Decision: ...

In short, a function is called a strategy as long as it takes the universe, the current holdings, a market view, and additional context as parameters, and returns a Decision, which can be constructed using the functions provided in the decision module, re-exported at the library root for convenience.

As inputs, the strategy receives the available universe, current holdings, a view of the market, and a context object for state.

For outputs, the strategy emits a Decision for each decision point in the decision schedule

An toy example strategy can be written as follows, where we allocate our holdings uniformly across our universe:

from backtest_lib import target_weights

def equal_weight_strategy(
    universe,
    current_portfolio,
    market,
    ctx,
):
    return target_weights({sec: 1/len(universe) for sec in universe})

Or alternatively, another trivial strategy where we do nothing after creating our initial portfolio:

from backtest_lib import hold

def buy_and_hold_strategy(
    universe,
    current_portfolio,
    market,
    ctx,
):
    return hold()

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: Universe,
    current_holdings: Holdings,
    market: MarketView,
    ctx: StrategyContext,
) -> Decision:
    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.13
  • run pip install uv
  • run uv run python --version and 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

backtest_lib-0.1.0.tar.gz (2.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

backtest_lib-0.1.0-py3-none-any.whl (2.6 MB view details)

Uploaded Python 3

File details

Details for the file backtest_lib-0.1.0.tar.gz.

File metadata

  • Download URL: backtest_lib-0.1.0.tar.gz
  • Upload date:
  • Size: 2.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","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

Hashes for backtest_lib-0.1.0.tar.gz
Algorithm Hash digest
SHA256 970e832ed6ccec9d619fb131f2a7231e0287af663ebaaf8069f6420d9b1dba9f
MD5 fd17cc96ed2bbe0727f35f4b5caad290
BLAKE2b-256 1651c209a50c202caa7161fe576634a08c6bb67e7354848a4d5f07de324356a6

See more details on using hashes here.

File details

Details for the file backtest_lib-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: backtest_lib-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 2.6 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","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

Hashes for backtest_lib-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dff2adb927c4ec1dd40c67c90e68d8205e5d44965c89c7d1d46842fb50853660
MD5 e24d2211bf5cf7589a692eaddf58da46
BLAKE2b-256 abd2765bf4d246b4db889d1cfd7b61e1cde917e2d0cb5a97300ae524571c7b45

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page