Skip to main content

SLTP ENGINE

Backtest a trading idea in one Python file, against daily US bars since 1998, without a broker account, a data subscription, or a compiled indicator library.

pip install sltp
sltp backtest momentum.py --start 2019-01-01 --chart

That is the whole workflow. Write a class, point the CLI at the file, get a table and a chart back.

Why this one

  • Nothing to compile. SMA, EMA, DEMA, TEMA, TRIX, RSI, MACD, BBANDS, ATR, ADX, STOCH, WILLR, CCI, ROC, MOM, OBV, MFI, AD, SAR and STOCHRSI are implemented in NumPy with TA-Lib's signatures, lookbacks and tuple returns. pip install is the entire build, and there is no C toolchain in sight.
  • No lookahead, by construction. Every callback receives arrays sliced to the current session. The last element is today; tomorrow is not in the object you were handed, so the classic backtest bug cannot be written by accident.
  • One file is one strategy. No registration step, no config file, no framework to learn. sltp compare a.py b.py runs both on the same capital over the same dates.
  • Fills that cost something. Orders fill at the next open with slippage and fees taken out of the fill, dividends credited before that open, and stops and targets filled at the level they were placed, or at the open when the bar gapped through it.
  • No survivorship bias. Universe lists keep delisted symbols, so a 2005 backtest is allowed to buy the company that later went to zero.
  • Reads only what it needs. Bars are chunked by year and a run opens only the chunks its range touches: 4.5 s for a 2020 run against 20.6 s across the full file set, with identical trades, equity and metrics (test/check_range_loading.py proves it).
  • Numbers, not opinions. Every run returns the same metrics, so two strategies are compared on Sharpe, Sortino, probabilistic Sharpe, max drawdown, turnover and cost drag rather than on a chart that looks steeper.

Pure Python on NumPy, SciPy, PyArrow and dateutil. There is no compiled extension of its own, and the version pins are chosen so micropip can install the same package in Pyodide.

This is the engine behind the SLTP app

The web app — research charts, the strategy library, and a Monaco editor that runs Python in the browser — sits on this engine. Its strategies are on the repo home page under app/strategies/, and they are worth reading before you write your own:

  • momentum_20.py — ranks liquid stocks by 12-month return, skips the most recent month, divides by each stock's own volatility, drops anything above a 60% annualised volatility ceiling, and holds the top 20 while SPY is above its 200-day average.
  • low_volatility.py — the boring half of the factor zoo, and usually the half that survives.
  • global_trend_allocation.py — a few asset classes, each held only while its own trend is up.
  • statistical_mean_reversion.py — buys what has stretched away from its own average.
  • benchmark.py — SPY, held from the first session to the last. The number to beat.

They import from app.engine, which is where this package came from. In a sltp install the same five names come from the package instead:

# inside the app                      # in a sltp install
from app.engine.strategy import Strategy              from sltp import Strategy
from app.engine.utils.Order import Order              from sltp import Order
from app.engine.utils.StockData import StockData      from sltp import StockData
from app.engine.utils.PositionType import PositionType        from sltp import PositionType
from app.engine.utils.RebalanceFrequency import RebalanceFrequency  from sltp import RebalanceFrequency

The body of the strategy does not change.

Code, issues and the app itself: https://github.com/ItsMacc/SLTP

Install

python -m pip install sltp

Requires Python 3.11+ (NumPy 2.4 does not build on 3.10). For development:

python -m pip install -e ".[dev]"

Quickstart

Two things: a data directory, and a strategy file.

Set SLTP_DATA_DIR to the folder holding your Parquet bars, or drop the files at <project>/data/market/. The layout is in Data below.

Then write buy_and_hold.py:

from sltp import Backtester, Strategy
from sltp.utils import Order, PositionType

class BuyAndHold(Strategy):
    TICKER = "SPY"

    def initialize_universe(self, current_date, histories):
        self.tickers.clear()
        if self.TICKER not in histories:
            return False
        self.add_ticker(self.TICKER)
        return True

    def on_signal(self, ticker, data):
        if ticker in self.open_tickers:
            return None
        return Order(
            ticker=ticker,
            position_type=PositionType.LONG,
            allocation=1.0,
            order_date=str(data.dates[-1]),
            stop_loss=0.0,
            take_profit=0.0,
        )

And run it:

sltp backtest buy_and_hold.py --start 2020-01-02 --end 2020-12-31
Base Strategy
  2020-01-02 → 2020-12-31 · 253 sessions

  Ending value   118.01   (+18.01%)
  CAGR           +18.08%
  Sharpe         0.66
  Sortino        0.75
  PSR            0.37
  Max drawdown   33.69%
  Win rate       —
  Trades         1
  Turnover       99.67%
  Cost drag      0.15%

SPY bought and held in 2020, and that ending value is the flat-fee-with-dividends baseline the range-loading test pins itself to. Three things about the output:

  • The title is the strategy's name, which starts as Base Strategy. Set self.name in your own __init__ to label a run — sltp compare uses it as the column heading.
  • — in the win rate is not zero. The SPY lot is still open on the last session, so no trade has closed and there is nothing to score yet.
  • The file defines a class and nothing else, so importing it costs nothing and the CLI does the only run.

To skip the CLI, the same object is one line away:

from sltp import Backtester

run = Backtester(strategy=BuyAndHold(), start_date="2020-01-02", end_date="2020-12-31").run()
print(run["metrics"]["sharpe"])

run() returns {"metrics": dict, "equity": list[dict], "trades": list[Trade], "rejections": dict}. Metrics carries the equity curve too, so pop it before serialising if you only want scalars.

What the engine does with your class

Each session, in this order: your run_daily, then resting stops and targets against today's bar, then queued exits and entries at today's open, then on_signal for every name the universe still allows, then should_exit on what you hold, then one mark and one equity log. A Backtester runs once — build a new one for the next run.

Command line

pip install sltp puts sltp on the path (python -m sltp works too).

sltp --help
usage: sltp [-h] [--version] COMMAND ...

Backtest trading strategies against local daily bars.

positional arguments:
  COMMAND
    backtest  run one strategy
    compare   run several strategies side by side

options:
  -h, --help  show this help message and exit
  --version   show program's version number and exit

sltp backtest

One strategy, one period, one table.

sltp backtest momentum.py

This is the default output, and it is meant to be read, not parsed:

Momentum 20
  2020-01-02 → 2020-12-31 · 253 sessions

  Ending value   139.97   (+39.97%)
  CAGR           +40.14%
  Sharpe         1.54
  Sortino        1.95
  PSR            0.70
  Max drawdown   17.29%
  Win rate       —
  Trades         2
  Turnover       83.77%
  Cost drag      0.12%

A — means the number does not exist yet, not zero: here both positions were still open at the end, so there is no closed-trade win rate to report.

Pick the period with -s/--start and -e/--end. With neither, a run covers the last three years of stored sessions.

sltp backtest momentum.py --start 2019-01-01 --end 2024-12-31
sltp backtest momentum.py -s 2019-01-01 -e 2024-12-31

Draw the curve with -g/--chart/--graph. It writes an SVG and opens it:

sltp backtest momentum.py --chart

Or write it somewhere specific with -o/--out, which implies the chart and does not open a browser — the right form for a script or a server:

sltp backtest momentum.py --out equity.svg

Get JSON instead of the table with --json, for a dashboard or a diff between runs:

sltp backtest momentum.py --json > result.json
sltp backtest momentum.py --json | jq '.metrics.sharpe'

Ignore cash distributions with --no-dividends when you want to see what the price move alone did.

sltp compare

Several strategies, one period, one table, one chart. It is backtest for a list, so a strategy compared against itself and backtest give the same numbers.

sltp compare value.py momentum.py
sltp compare a.py b.py c.py --start 2019-01-01 --out compare.svg
                 Momentum 20   Static 60/40
  ─────────────────────────────────────────
  Return             +39.97%        +13.36%
  CAGR               +40.14%        +13.40%
  Sharpe                1.54           0.85
  Sortino               1.95           0.91
  PSR                   0.70           0.44
  Max drawdown        17.29%         19.23%
  Win rate                 —              —
  Trades                   2              2
  Turnover            83.77%         96.41%
  Cost drag            0.12%          0.15%
  2020-01-02 → 2020-12-31 · 253 sessions

Every strategy in the chart gets its own colour, and the legend carries its final value. Give compare a single strategy and it adds SPY bought and held, so there is always something to beat. --json returns one object with a runs list.

Strategy files

A strategy is any .py file with a Strategy subclass and a no-argument constructor. The file's own directory joins sys.path, so it can import its siblings, and nothing else in the project needs to know it exists.

If one file defines several strategies, name the class with :ClassName:

sltp backtest strategies.py:Momentum
sltp compare strategies.py:Momentum strategies.py:Value

When the class is ambiguous or missing, the CLI says so and stops with exit code 1 rather than guessing:

error: /path/strategies.py defines several strategies (Momentum, Value). Add :ClassName.

Dates are validated the same way — a bad date, a reversed range, a missing file or a base class that was never subclassed all print one line and exit 1, with no traceback.

Data

Market data is not shipped inside the package: it is 800 MB of Parquet, and you should choose where it lives. The engine reads bar files from a data directory, resolved in this order:

  1. SLTP_DATA_DIR, pointing either at the directory of bar files or at its parent.
  2. Walking up from the installed package looking for <root>/data/market/market_data.parquet.
  3. ~/.sltp/data/market.

The layout the resolver expects:

data/
├── market/
│   ├── market_data_1998_2003.parquet   # chunked layout (preferred)
│   ├── market_data_2004_2009.parquet
│   ├── ...                             # or one market_data.parquet instead
│   ├── market_data_recent.parquet      # optional tail, read after the history
│   └── market_actions.parquet          # splits and dividends
└── symbol_data/
    ├── symbols.parquet                 # the catalog, with the Category column
    └── symbol_status.parquet           # which symbols still trade

Set SLTP_DATA_DIR before importing sltp: the read layer resolves its paths once, at import. Importing still works with no data present — the error comes when a run asks for bars, and it names the file it wanted.

When market_data_<year>_<year>.parquet chunks exist the reader uses them and ignores the single master, so the same bars are never loaded twice. A symbol that spans several chunks is merged back into one ascending history. scripts/chunk_market_data.py builds the chunks from a master file.

Charts

sltp.graphs draws equity curves as plain SVG: no matplotlib, no browser, no extra dependency, so the same call works in a script, a notebook and Pyodide. Each strategy gets its own colour, and a second panel shows its drawdown, which is the fastest way to see whether a higher return was bought with pain.

from sltp.graphs import ChartSeries, equity_chart, write_chart

svg = equity_chart([ChartSeries.from_equity("Momentum", run["equity"])])
write_chart("equity.svg", svg)

The SVG carries an intrinsic 1100×636 size and shrinks to whatever container it lands in.

Public API

from sltp import Strategy, Backtester, Order, PositionType, Trade, StockData
from sltp import PortfolioManager, RebalanceFrequency, OrderSizing, Event, EventType, CALENDAR

That is the whole surface: the engine, the value types it passes around, and the portfolio manager. sltp.data is deliberately not exported — it resolves paths and reads Parquet, and a strategy gets its bars from the engine rather than from the reader.

Indicators

sltp.indicators implements SMA, EMA, DEMA, TEMA, TRIX, RSI, MACD, BBANDS, ATR, ADX, STOCH, WILLR, CCI, ROC, MOM, OBV, MFI, AD, SAR and STOCHRSI with TA-Lib's signatures, warm-up periods and tuple returns, including matype arguments accepted for compatibility. Only matype=0 (simple) is implemented; asking for another raises rather than quietly returning the wrong smoothing. Verified against TA-Lib 0.8.x by test/compare_indicators.py.

A strategy reaches them through helpers, which cache one computation per series and cut today's window from it:

close = data.close_price
fast = self._ema(close, 20)
slow = self._ema(close, 50)
rsi = self._rsi(close, 14)

Project layout

src/sltp/
├── backtesting/     # Backtester: the daily loop
├── portfolio/       # PortfolioManager: orders, fills, positions, equity
├── strategy/        # Strategy base class and the indicator helpers
├── utils/           # Order, Trade, Event, StockData, date codes
├── indicators.py    # NumPy TA-Lib replacements
├── graphs/          # SVG equity and drawdown charts
├── cli.py           # sltp backtest, sltp compare
└── data/            # Parquet read layer, path resolution and the symbol catalog

Tests

python -m pip install -e ".[dev]"
python test/smoke_backtest.py            # end to end against the real bar files
python test/check_range_loading.py       # chunk pruning returns identical results
python test/compare_indicators.py        # needs TA-Lib installed; compares numerically

License

MIT.

Metadata

Release files for sltp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sltp 0.1.0
File Size Uploaded Provenance Actions
sltp-0.1.0.tar.gz 62.9 kB Without provenance Details for sltp-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sltp 0.1.0
File Interpreter ABI Platform Actions
sltp-0.1.0-py3-none-any.whl Python 3 none any Details for sltp-0.1.0-py3-none-any.whl

Total release size: 126.7 kB

Release files / sltp-0.1.0.tar.gz

Download URL sltp-0.1.0.tar.gz
Size 62.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d793e06979b2b7ab29711302e5aa57fcdf7c060a8f845bc0cae77c028a782cea
BLAKE2b-256 checksum
How to use checksums
92a86491faf61a4545e2bbfb98c5e775670da57f7fca15f1f2e2e77767e602d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / sltp-0.1.0-py3-none-any.whl

Download URL sltp-0.1.0-py3-none-any.whl
Size 63.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8970db56afa944abc07db482f1d8022c763579abbaed80a61965451443dc1a01
BLAKE2b-256 checksum
How to use checksums
5a779d57fef3dcec19ed5f9460920dadd040747b1d5cb9c265691d98afda108b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for sltp 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page