nanvel
cipher-bt
Python

Trading strategy backtesting framework supporting multiple concurrent sessions, complex exit strategies, and multi-exchange data sources with simple Python implementation

Last updated Jun 10, 2026
19
Stars
1
Forks
0
Issues
0
Stars/day
Attention Score
9
Language breakdown
Python 99.6%
Jinja 0.4%
Files click to expand
README

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, fastemalength=9, slowemalength=21, trendemalength=200): self.fastemalength = fastemalength self.slowemalength = slowemalength self.trendemalength = trendemalength

def compose(self): df = self.datas.df df["fastema"] = talib.EMA(df["close"], timeperiod=self.fastema_length) df["slowema"] = talib.EMA(df["close"], timeperiod=self.slowema_length) df["trendema"] = talib.EMA(df["close"], timeperiod=self.trendema_length)

df["difference"] = df["fastema"] - df["slowema"]

# 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.stoploss = row["min6"] session.takeprofit = row["close"] + 1.5 * (row["close"] - row["min6"])

elif row["difference"] < 0 and row["close"] < row["trend_ema"]: # Open a new short position session.position -= "0.01" session.stoploss = row["max6"] session.takeprofit = row["close"] - 1.5 * (row["max6"] - 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 ontakeprofit(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 onstoploss(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.addsource("binancespot_ohlc", symbol="BTCUSDT", interval="1h") cipher.set_strategy(EmaCrossoverStrategy()) cipher.run(startts="2025-01-01", stopts="2025-04-01") cipher.set_commission("0.00075") print(cipher.sessions) print(cipher.stats) cipher.plot()

if name == "main": main()

ema<em>crossover</em>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.

🔗 More in this category

© 2026 GitRepoTrend · nanvel/cipher-bt · Updated daily from GitHub