Cipher - Trading Strategy Backtesting Framework
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()
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cipher_bt-0.6.8.tar.gz | 22.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.