Skip to main content

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, and LIMIT behavior
  • Works directly with pandas DataFrame input 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-02 open price of 101.0
  • Engine.run(...) liquidates the remaining position on the final bar at the same day's close price of 102.0
  • With the default 0.1% fee on entry and exit, final portfolio value becomes 10007.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 t are evaluated on bar t + 1
  • OPEN orders use the next bar open
  • CLOSE orders use the next bar close
  • LIMIT orders use the next bar open/high/low according to the limit-fill rules
  • Engine.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 bar close
  • 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 LIMIT orders: 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:

  • history
  • closed_positions
  • final_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:

  • summary
  • metrics
  • closed_positions
  • events

summary contains:

  • bars
  • final_portfolio_value
  • closed_positions
  • events

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.12 to 3.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

kissbt-0.1.7.tar.gz (37.5 kB view details)

Uploaded Source

Built Distribution

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

kissbt-0.1.7-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

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

Hashes for kissbt-0.1.7.tar.gz
Algorithm Hash digest
SHA256 d8d0e3705f0a63b5a193846e7b57ff08989ab30477ca5bb89b1fc33fdaa082c7
MD5 7441f8b0102f3c2484d46ffaa30d718e
BLAKE2b-256 736293d5f15d0cd402efeb1b153fe429d2ef491966780684e84b80c67898c834

See more details on using hashes here.

Provenance

The following attestation bundles were made for kissbt-0.1.7.tar.gz:

Publisher: python-publish.yaml on FinBlobs/kissbt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for kissbt-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 fa1904917367a7cccb5811c6f0a6b644623cc8cbd37e1a969099fef6e01ae100
MD5 14d1f2b20a1e49a6dc3dcce8ec78a805
BLAKE2b-256 73aa7833754e91ee96722c697ae772816fa8bb39ab50e1cf0e6698906699cb92

See more details on using hashes here.

Provenance

The following attestation bundles were made for kissbt-0.1.7-py3-none-any.whl:

Publisher: python-publish.yaml on FinBlobs/kissbt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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