The keep it simple backtesting framework for Python.
Project description
kissbt
kissbt ("keep it simple backtesting") is a small Python backtesting framework for people who want clear execution semantics, a compact API, and deterministic CLI output. It is built for pandas-based strategy research, scripted backtest runs, and machine-friendly result handling without the weight of a large framework. It stays comfortable for simple single-asset ideas, but is also flexible enough for multi-asset and whole-universe workflows.
Why kissbt?
- Small public API:
Strategy,Broker,Engine,Analyzer - Clear next-bar execution model with explicit
OPEN,CLOSE, andLIMITbehavior - Works directly with pandas
DataFrameinput using("timestamp", "ticker")MultiIndex data - Flexible enough for single-asset, multi-asset, and whole-universe strategies
- Supports long-only and long/short workflows
- Structured backtest results for Python code and deterministic JSON output for shell/CI/agents
- Fails fast on invalid inputs instead of silently guessing
Installation
Install with pip:
pip install kissbt
Install with parquet support:
pip install "kissbt[parquet]"
Install with uv:
uv add kissbt
Or with conda:
conda install -c conda-forge kissbt
Supported Python versions: 3.12 to 3.14.
Quickstart
This example is intentionally small enough to verify by inspection.
import pandas as pd
from kissbt import Broker, Engine, Order, Strategy
class BuyAndHoldOnce(Strategy):
def initialize(self) -> None:
self.has_bought = False
def generate_orders(self, current_data, current_timestamp) -> None:
if not self.has_bought:
self.broker.place_order(Order(ticker="AAPL", size=10))
self.has_bought = True
index = pd.MultiIndex.from_tuples(
[
(pd.Timestamp("2024-01-01"), "AAPL"),
(pd.Timestamp("2024-01-02"), "AAPL"),
],
names=["timestamp", "ticker"],
)
market_data = pd.DataFrame(
{
"open": [100.0, 101.0],
"high": [102.0, 103.0],
"low": [99.0, 100.0],
"close": [101.0, 102.0],
},
index=index,
)
broker = Broker(start_capital=10_000)
strategy = BuyAndHoldOnce(broker)
engine = Engine(broker, strategy)
result = engine.run(market_data)
trade = result.closed_positions[0]
print(trade.entry_price)
print(trade.exit_price)
print(round(result.final_portfolio_value, 2))
Expected output:
101.0
102.0
10007.97
Why those numbers:
- The order is placed on
2024-01-01 - It executes on the next bar at the
2024-01-02openprice of101.0 Engine.run(...)liquidates the remaining position on the final bar at the same day'scloseprice of102.0- With the default
0.1%fee on entry and exit, final portfolio value becomes10007.97
Execution Model
kissbt uses a simple next-bar execution model:
Strategy.generate_orders(...)runs after the broker has processed the current bar- Orders placed during bar
tare evaluated on bart + 1 OPENorders use the next baropenCLOSEorders use the next barcloseLIMITorders use the next baropen/high/lowaccording to the limit-fill rulesEngine.run(...)liquidates any remaining positions on the final bar after strategy execution
Two additional behaviors matter in practice:
- If a held ticker disappears from the current universe,
Broker.update(...)closes it at the previous barclose - Good-till-cancel orders remain pending across later bars when unfilled, including when a ticker is temporarily missing
Input Data Requirements
Engine.run(data) expects a pandas DataFrame with:
- Exactly two index levels named
("timestamp", "ticker") - Unique
("timestamp", "ticker")rows - Required columns:
open,close - Additional columns for
LIMITorders:high,low - If
Broker(benchmark=...)is configured, the benchmark ticker must be present for every timestamp
If your input is not already indexed, the CLI also accepts CSV or Parquet files with
timestamp and ticker columns and converts them into the required MultiIndex shape.
Flexible Strategy Workflows
Each call to Strategy.generate_orders(...) receives the full bar for the current
timestamp as a DataFrame indexed by ticker. That makes it natural to:
- Run a single instrument strategy
- Scan a watchlist of symbols
- Rank, filter, or rebalance across a whole universe on each bar
You can keep the strategy logic small, or prepare richer indicator columns in pandas
before calling Engine.run(...).
Python API
Define a strategy:
from kissbt import Order, OrderType, Strategy
class MyStrategy(Strategy):
def generate_orders(self, current_data, current_timestamp) -> None:
for ticker in current_data.index:
close_price = current_data.loc[ticker, "close"]
sma_128 = current_data.loc[ticker, "sma_128"]
if close_price > sma_128:
self.broker.place_order(
Order(ticker=ticker, size=10, order_type=OrderType.OPEN)
)
current_data is the full cross-section for the current timestamp, so the same strategy
shape works for one ticker, a small basket, or a full universe.
Create a broker and engine, then run the backtest:
from kissbt import Broker, Engine
broker = Broker(start_capital=100000, fees=0.001)
strategy = MyStrategy(broker)
engine = Engine(broker, strategy)
result = engine.run(market_data)
result is a BacktestResult with:
historyclosed_positionsfinal_portfolio_value
Analyze performance after the broker has processed at least one bar:
from kissbt import Analyzer
metrics = Analyzer(broker).get_performance_metrics()
print(metrics["total_return"])
CLI And Automation
The CLI is designed for shell scripts, CI jobs, and agent workflows that need strict, machine-consumable output.
Run a backtest
Create a strategy module, for example my_strategies/golden_cross.py:
from kissbt import Order, Strategy
class GoldenCrossStrategy(Strategy):
def generate_orders(self, current_data, current_timestamp) -> None:
for ticker in current_data.index:
if (
current_data.loc[ticker, "sma_128"]
>= current_data.loc[ticker, "sma_256"]
and ticker not in self.broker.open_positions
):
self.broker.place_order(Order(ticker=ticker, size=1))
Write JSON to a file:
kissbt backtest \
--input tests/data/tech_stocks.parquet \
--strategy my_strategies.golden_cross:GoldenCrossStrategy \
--benchmark SPY \
--output backtest_result.json
Or write JSON to stdout:
kissbt backtest \
--input tests/data/tech_stocks.parquet \
--strategy my_strategies.golden_cross:GoldenCrossStrategy
Parquet input requires an installed parquet engine such as pyarrow. The
kissbt[parquet] extra installs that dependency for you.
Useful flags:
--input-format auto|csv|parquet--start-capital 100000--fees 0.001--allow-short--short-fee-rate 0.005--benchmark SPY--bar-size 1D
Failure behavior:
- Invalid inputs exit with a non-zero status
- Errors are printed as concise user-facing messages
- Non-finite numeric values in JSON are normalized to
null
JSON output contract
The command writes a JSON report with:
summarymetricsclosed_positionsevents
summary contains:
barsfinal_portfolio_valueclosed_positionsevents
Timestamps are emitted in ISO 8601 format. Field names are deterministic so the output is stable for downstream scripts and automation.
Example shape:
{
"summary": {
"bars": 252,
"final_portfolio_value": 108734.12,
"closed_positions": 14,
"events": 28
},
"metrics": {
"total_return": 0.0873412,
"profit_factor": 1.91
},
"closed_positions": [
{
"ticker": "AAPL",
"size": 10.0,
"entry_price": 100.0,
"entry_timestamp": "2024-01-02T00:00:00",
"exit_price": 108.5,
"exit_timestamp": "2024-01-15T00:00:00",
"pnl": 85.0
}
],
"events": [
{
"type": "order_executed",
"timestamp": "2024-01-02T00:00:00",
"ticker": "AAPL",
"size": 10.0,
"order_type": "open",
"price": 100.0
}
]
}
When kissbt is a good fit
kissbt is a good fit if you want:
- A Python backtesting engine that stays easy to read end-to-end
- pandas-first research workflows over OHLC or indicator-enriched data
- Flexible strategies that scale from one instrument to a whole universe
- Reproducible batch backtests from the command line
- Deterministic JSON output for automation, reporting, or agent orchestration
It is probably not the right tool if you want a large built-in ecosystem, live trading connectors, or a framework that hides the execution model behind many abstractions.
Development
For development, this repository uses uv.
- Development baseline: Python
3.13 - Supported Python versions:
3.12to3.14
uv python install 3.13
uv venv --python 3.13
uv sync --extra dev
uv run ruff format .
uv run ruff check .
uv run mypy kissbt tests
uv run pytest
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 kissbt-0.1.7.tar.gz.
File metadata
- Download URL: kissbt-0.1.7.tar.gz
- Upload date:
- Size: 37.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8d0e3705f0a63b5a193846e7b57ff08989ab30477ca5bb89b1fc33fdaa082c7
|
|
| MD5 |
7441f8b0102f3c2484d46ffaa30d718e
|
|
| BLAKE2b-256 |
736293d5f15d0cd402efeb1b153fe429d2ef491966780684e84b80c67898c834
|
Provenance
The following attestation bundles were made for kissbt-0.1.7.tar.gz:
Publisher:
python-publish.yaml on FinBlobs/kissbt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kissbt-0.1.7.tar.gz -
Subject digest:
d8d0e3705f0a63b5a193846e7b57ff08989ab30477ca5bb89b1fc33fdaa082c7 - Sigstore transparency entry: 1052803872
- Sigstore integration time:
-
Permalink:
FinBlobs/kissbt@d27a23bec1e389cc72c606db9bbc1a9707aca388 -
Branch / Tag:
refs/tags/v0.1.7 - Owner: https://github.com/FinBlobs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yaml@d27a23bec1e389cc72c606db9bbc1a9707aca388 -
Trigger Event:
release
-
Statement type:
File details
Details for the file kissbt-0.1.7-py3-none-any.whl.
File metadata
- Download URL: kissbt-0.1.7-py3-none-any.whl
- Upload date:
- Size: 27.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa1904917367a7cccb5811c6f0a6b644623cc8cbd37e1a969099fef6e01ae100
|
|
| MD5 |
14d1f2b20a1e49a6dc3dcce8ec78a805
|
|
| BLAKE2b-256 |
73aa7833754e91ee96722c697ae772816fa8bb39ab50e1cf0e6698906699cb92
|
Provenance
The following attestation bundles were made for kissbt-0.1.7-py3-none-any.whl:
Publisher:
python-publish.yaml on FinBlobs/kissbt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kissbt-0.1.7-py3-none-any.whl -
Subject digest:
fa1904917367a7cccb5811c6f0a6b644623cc8cbd37e1a969099fef6e01ae100 - Sigstore transparency entry: 1052803884
- Sigstore integration time:
-
Permalink:
FinBlobs/kissbt@d27a23bec1e389cc72c606db9bbc1a9707aca388 -
Branch / Tag:
refs/tags/v0.1.7 - Owner: https://github.com/FinBlobs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yaml@d27a23bec1e389cc72c606db9bbc1a9707aca388 -
Trigger Event:
release
-
Statement type: