Skip to main content

Cipher - Trading Strategy Backtesting Framework

Tests PyPI version Python Versions License: MIT

cipher

Documentation: https://cipher.nanvel.com

Features:

  • Well-structured, intuitive, and easily extensible design
  • Support for multiple concurrent trading sessions
  • Sophisticated exit strategies including trailing take profits
  • Multi-source data integration (exchanges, symbols, timeframes)
  • Clean separation between signal generation and handling
  • Simple execution - just run python my_strategy.py
  • Compatibility with Google Colab
  • Built-in visualization with finplot and mplfinance plotters

Usage

Set up a new strategies workspace and create your first strategy:

mkdir strategies
cd strategies
uv init
uv add 'cipher-bt[finplot,talib]'

uv run cipher init
uv run cipher new my_strategy
uv run python my_strategy.py

Complete EMA crossover strategy example:

import numpy as np
import talib

from cipher import Cipher, Session, Strategy


class EmaCrossoverStrategy(Strategy):
    def __init__(self, fast_ema_length=9, slow_ema_length=21, trend_ema_length=200):
        self.fast_ema_length = fast_ema_length
        self.slow_ema_length = slow_ema_length
        self.trend_ema_length = trend_ema_length

    def compose(self):
        df = self.datas.df
        df["fast_ema"] = talib.EMA(df["close"], timeperiod=self.fast_ema_length)
        df["slow_ema"] = talib.EMA(df["close"], timeperiod=self.slow_ema_length)
        df["trend_ema"] = talib.EMA(df["close"], timeperiod=self.trend_ema_length)

        df["difference"] = df["fast_ema"] - df["slow_ema"]

        # Signal columns must be boolean type
        df["entry"] = np.sign(df["difference"].shift(1)) != np.sign(df["difference"])

        df["max_6"] = df["high"].rolling(window=6).max()
        df["min_6"] = df["low"].rolling(window=6).min()

        return df

    def on_entry(self, row: dict, session: Session):
        if row["difference"] > 0 and row["close"] > row["trend_ema"]:
            # Open a new long position
            session.position += "0.01"
            session.stop_loss = row["min_6"]
            session.take_profit = row["close"] + 1.5 * (row["close"] - row["min_6"])

        elif row["difference"] < 0 and row["close"] < row["trend_ema"]:
            # Open a new short position
            session.position -= "0.01"
            session.stop_loss = row["max_6"]
            session.take_profit = row["close"] - 1.5 * (row["max_6"] - row["close"])

    # def on_<signal>(self, row: dict, session: Session) -> None:
    #     """Custom signal handler called for each active session.
    #     Adjust or close positions and modify brackets here."""
    #     # session.position = 1
    #     # session.position = base(1)  # equivalent to the above
    #     # session.position = '1'  # int, str, float are converted to Decimal
    #     # session.position = quote(100)  # position worth 100 quote asset
    #     # session.position += 1  # add to the position
    #     # session.position -= Decimal('1.25')  # reduce position by 1.25
    #     # session.position += percent(50)  # add 50% more to position
    #     # session.position *= 1.5  # equivalent to the above
    #     pass
    #
    # def on_take_profit(self, row: dict, session: Session) -> None:
    #     """Called when take profit is hit. Default action closes the position.
    #     Modify position and brackets here to continue the session."""
    #     session.position = 0
    #
    # def on_stop_loss(self, row: dict, session: Session) -> None:
    #     """Called when stop loss is hit. Default action closes the position.
    #     Modify position and brackets here to continue the session."""
    #     session.position = 0
    #
    # def on_stop(self, row: dict, session: Session) -> None:
    #     """Called for each active session when dataframe ends.
    #     Close open sessions here, otherwise they will be ignored."""
    #     session.position = 0


def main():
    cipher = Cipher()
    cipher.add_source("binance_spot_ohlc", symbol="BTCUSDT", interval="1h")
    cipher.set_strategy(EmaCrossoverStrategy())
    cipher.run(start_ts="2025-01-01", stop_ts="2025-04-01")
    cipher.set_commission("0.00075")
    print(cipher.sessions)
    print(cipher.stats)
    cipher.plot()


if __name__ == "__main__":
    main()

ema_crossover_plot

Development

brew install uv
uv sync --all-extras
source .venv/bin/activate

pytest tests
cipher --help

Disclaimer

This software is for educational purposes only. Do not risk money you cannot afford to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.

Metadata

Release files for cipher-bt 0.6.8

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

Source distribution (sdist)

Source distribution for cipher-bt 0.6.8
File Size Uploaded Provenance
cipher_bt-0.6.8.tar.gz 22.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cipher-bt 0.6.8
File Interpreter ABI Platform
cipher_bt-0.6.8-py3-none-any.whl Python 3 none any Details

Total release size: 62.0 kB

Release files / cipher_bt-0.6.8.tar.gz

Download URL cipher_bt-0.6.8.tar.gz
Size 22.1 kB
Tags Source
SHA-256 checksum
How to use checksums
461e16c188f870cdb64ec6ec57cc8a52d2237ed0c1deb5cee36c0fbc56a61128
BLAKE2b-256 checksum
How to use checksums
e2bd3bf2aea8af6ca15af5c1b7938d863e0ab264d53aed4a32b19165bd8a85c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.17

Release files / cipher_bt-0.6.8-py3-none-any.whl

Download URL cipher_bt-0.6.8-py3-none-any.whl
Size 40.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca1313060c593a3fb79a70296873c71edbf056e3c558e054953902c421c5b75d
BLAKE2b-256 checksum
How to use checksums
e1f66064e80bdcd78036a15ad8087f9ea34b4e952f115ec88a52f80c4be0e272
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.17

Security for cipher-bt 0.6.8

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.

Release history Release notifications | RSS feed

This release

0.6.8 This release

2 release files No provenance

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