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 installis 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.pyruns 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.pyproves 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 asBase Strategy. Setself.namein your own__init__to label a run —sltp compareuses 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:
SLTP_DATA_DIR, pointing either at the directory of bar files or at its parent.- Walking up from the installed package looking for
<root>/data/market/market_data.parquet. ~/.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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.