├── bitmex_backtest ├── __init__.py └── bitmex_backtest.py ├── tests ├── basic.png ├── advanced.png └── test_bitmex_backtest.py ├── setup.cfg ├── register.py ├── .gitignore ├── .travis.yml ├── requirements.txt ├── LICENSE.txt ├── setup.py └── README.md /bitmex_backtest/__init__.py: -------------------------------------------------------------------------------- 1 | from bitmex_backtest.bitmex_backtest import Backtest 2 | -------------------------------------------------------------------------------- /tests/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10mohi6/bitmex-backtest-python/HEAD/tests/basic.png -------------------------------------------------------------------------------- /tests/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/10mohi6/bitmex-backtest-python/HEAD/tests/advanced.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E203,W503,W504 4 | 5 | [mypy] 6 | ignore_missing_imports = True -------------------------------------------------------------------------------- /register.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.system("rm dist/*") 4 | os.system("python setup.py sdist bdist_wheel") 5 | os.system("twine upload dist/*") 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | .venv/ 4 | *.egg-info/ 5 | dist/ 6 | .coverage 7 | .pytest_cache/ 8 | build/ 9 | .vscode/ 10 | .mypy_cache/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Set the build language to Python 2 | language: python 3 | 4 | # Set the python version 5 | python: 6 | - "3.7" 7 | - "3.8" 8 | - "3.9" 9 | 10 | # Install the codecov pip dependency 11 | install: 12 | - pip install -r requirements.txt 13 | - pip install codecov 14 | 15 | # Run the unit test 16 | script: 17 | - python -m pytest --cov=bitmex_backtest tests/ 18 | 19 | # Push the results back to codecov 20 | after_success: 21 | - codecov -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==20.3.0 2 | certifi==2020.12.5 3 | chardet==4.0.0 4 | coverage==5.5 5 | cycler==0.10.0 6 | idna==2.10 7 | iniconfig==1.1.1 8 | kiwisolver==1.3.1 9 | matplotlib==3.4.1 10 | numpy==1.20.2 11 | packaging==20.9 12 | pandas==1.2.3 13 | Pillow==8.2.0 14 | pluggy==0.13.1 15 | py==1.10.0 16 | pyparsing==2.4.7 17 | pytest==6.2.3 18 | pytest-cov==2.11.1 19 | python-dateutil==2.8.1 20 | pytz==2021.1 21 | requests==2.25.1 22 | six==1.15.0 23 | toml==0.10.2 24 | urllib3==1.26.4 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 10mohi6 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="bitmex-backtest", 5 | version="0.1.3", 6 | description="bitmex-backtest is a python library \ 7 | for backtest with bitmex fx trade rest api on Python 3.7 and above.", 8 | long_description=open("README.md").read(), 9 | long_description_content_type="text/markdown", 10 | license="MIT", 11 | author="10mohi6", 12 | author_email="10.mohi.6.y@gmail.com", 13 | url="https://github.com/10mohi6/bitmex-backtest-python", 14 | keywords="bitmex backtest api python trade fx", 15 | packages=find_packages(), 16 | install_requires=["requests", "numpy", "pandas", "matplotlib"], 17 | python_requires=">=3.7.0", 18 | classifiers=[ 19 | "Development Status :: 4 - Beta", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Financial and Insurance Industry", 27 | "Operating System :: OS Independent", 28 | "Topic :: Office/Business :: Financial :: Investment", 29 | "License :: OSI Approved :: MIT License", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_bitmex_backtest.py: -------------------------------------------------------------------------------- 1 | from bitmex_backtest import Backtest 2 | import pytest 3 | import time 4 | 5 | 6 | @pytest.fixture(scope="module", autouse=True) 7 | def scope_module(): 8 | yield Backtest(test=True) 9 | 10 | 11 | @pytest.fixture(scope="function", autouse=True) 12 | def bt(scope_module): 13 | time.sleep(1) 14 | yield scope_module 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "resolution,exp", 19 | [ 20 | ("1", 1 * 60), 21 | ("5", 5 * 60), 22 | ("60", 60 * 60), 23 | ("1D", 1 * 60 * 60 * 24), 24 | ("1W", 1 * 60 * 60 * 24 * 7), 25 | ("1M", 1 * 60 * 60 * 24 * 7 * 4), 26 | ], 27 | ) 28 | def test_resolution_to_seconds(bt, resolution, exp): 29 | actual = bt._resolution_to_seconds(resolution) 30 | expected = exp 31 | assert expected == actual 32 | 33 | 34 | def test_candles(bt): 35 | actual = bt.candles("XBTUSD") 36 | expected = 500 37 | assert expected == len(actual) 38 | 39 | 40 | def test_candles_count(bt): 41 | params = {"resolution": "5", "count": 1000} 42 | actual = bt.candles("XBTUSD", params) 43 | expected = 1000 44 | assert expected == len(actual) 45 | 46 | 47 | def test_run_basic(bt): 48 | bt.candles("XBTUSD") 49 | fast_ma = bt.sma(period=5) 50 | slow_ma = bt.sma(period=25) 51 | bt.sell_exit = bt.buy_entry = (fast_ma > slow_ma) & ( 52 | fast_ma.shift() <= slow_ma.shift() 53 | ) 54 | bt.buy_exit = bt.sell_entry = (fast_ma < slow_ma) & ( 55 | fast_ma.shift() >= slow_ma.shift() 56 | ) 57 | actual = bt.run() 58 | expected = 11 59 | assert expected == len(actual) 60 | 61 | 62 | def test_run_advanced(bt): 63 | filepath = "xbtusd-60.csv" 64 | if bt.exists(filepath): 65 | bt.read_csv(filepath) 66 | else: 67 | bt.candles("XBTUSD", {"resolution": "60", "count": "5000"}) 68 | bt.to_csv(filepath) 69 | 70 | fast_ma = bt.sma(period=10) 71 | slow_ma = bt.sma(period=30) 72 | exit_ma = bt.sma(period=5) 73 | bt.buy_entry = (fast_ma > slow_ma) & (fast_ma.shift() <= slow_ma.shift()) 74 | bt.sell_entry = (fast_ma < slow_ma) & (fast_ma.shift() >= slow_ma.shift()) 75 | bt.buy_exit = (bt.C < exit_ma) & (bt.C.shift() >= exit_ma.shift()) 76 | bt.sell_exit = (bt.C > exit_ma) & (bt.C.shift() <= exit_ma.shift()) 77 | 78 | bt.quantity = 100 79 | bt.stop_loss = 200 80 | bt.take_profit = 1000 81 | 82 | actual = bt.run() 83 | expected = 11 84 | assert expected == len(actual) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitmex-backtest 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/bitmex-backtest)](https://pypi.org/project/bitmex-backtest/) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | [![codecov](https://codecov.io/gh/10mohi6/bitmex-backtest-python/branch/master/graph/badge.svg)](https://codecov.io/gh/10mohi6/bitmex-backtest-python) 6 | [![Build Status](https://travis-ci.com/10mohi6/bitmex-backtest-python.svg?branch=master)](https://travis-ci.com/10mohi6/bitmex-backtest-python) 7 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bitmex-backtest)](https://pypi.org/project/bitmex-backtest/) 8 | [![Downloads](https://pepy.tech/badge/bitmex-backtest)](https://pepy.tech/project/bitmex-backtest) 9 | 10 | bitmex-backtest is a python library for backtest with bitmex fx trade rest api on Python 3.7 and above. 11 | 12 | 13 | ## Installation 14 | 15 | $ pip install bitmex-backtest 16 | 17 | ## Usage 18 | 19 | ### basic 20 | ```python 21 | from bitmex_backtest import Backtest 22 | 23 | bt = Backtest() 24 | bt.candles("XBTUSD") 25 | fast_ma = bt.sma(period=5) 26 | slow_ma = bt.sma(period=25) 27 | bt.sell_exit = bt.buy_entry = (fast_ma > slow_ma) & (fast_ma.shift() <= slow_ma.shift()) 28 | bt.buy_exit = bt.sell_entry = (fast_ma < slow_ma) & (fast_ma.shift() >= slow_ma.shift()) 29 | bt.run() 30 | bt.plot() 31 | ``` 32 | 33 | ### advanced 34 | ```python 35 | from bitmex_backtest import Backtest 36 | 37 | bt = Backtest(test=True) 38 | filepath = "xbtusd-60.csv" 39 | if bt.exists(filepath): 40 | bt.read_csv(filepath) 41 | else: 42 | params = { 43 | "resolution": "60", # 1 hour candlesticks (default=1) 1,3,5,15,30,60,120,180,240,360,720,1D,3D,1W,2W,1M 44 | "count": "5000" # 5000 candlesticks (default=500) 45 | } 46 | bt.candles("XBTUSD", params) 47 | bt.to_csv(filepath) 48 | 49 | fast_ma = bt.sma(period=10) 50 | slow_ma = bt.sma(period=30) 51 | exit_ma = bt.sma(period=5) 52 | bt.buy_entry = (fast_ma > slow_ma) & (fast_ma.shift() <= slow_ma.shift()) 53 | bt.sell_entry = (fast_ma < slow_ma) & (fast_ma.shift() >= slow_ma.shift()) 54 | bt.buy_exit = (bt.C < exit_ma) & (bt.C.shift() >= exit_ma.shift()) 55 | bt.sell_exit = (bt.C > exit_ma) & (bt.C.shift() <= exit_ma.shift()) 56 | 57 | bt.quantity = 100 # default=1 58 | bt.stop_loss = 200 # stop loss (default=0) 59 | bt.take_profit = 1000 # take profit (default=0) 60 | 61 | print(bt.run()) 62 | bt.plot("backtest.png") 63 | ``` 64 | 65 | ```python 66 | total profit -342200.000 67 | total trades 162.000 68 | win rate 32.716 69 | profit factor 0.592 70 | maximum drawdown 470950.000 71 | recovery factor -0.727 72 | riskreward ratio 1.295 73 | sharpe ratio -0.127 74 | average return -20.325 75 | stop loss 23.000 76 | take profit 1.000 77 | ``` 78 | ![advanced.png](https://raw.githubusercontent.com/10mohi6/bitmex-backtest-python/master/tests/advanced.png) 79 | 80 | 81 | ## Supported indicators 82 | - Simple Moving Average 'sma' 83 | - Exponential Moving Average 'ema' 84 | - Moving Average Convergence Divergence 'macd' 85 | - Relative Strenght Index 'rsi' 86 | - Bollinger Bands 'bband' 87 | - Stochastic Oscillator 'stoch' 88 | - Market Momentum 'mom' 89 | 90 | 91 | ## Getting started 92 | 93 | For help getting started with bitmex REST API, view our online [documentation](https://www.bitmex.com/app/restAPI). 94 | 95 | 96 | ## Contributing 97 | 98 | 1. Fork it 99 | 2. Create your feature branch (`git checkout -b my-new-feature`) 100 | 3. Commit your changes (`git commit -am 'Add some feature'`) 101 | 4. Push to the branch (`git push origin my-new-feature`) 102 | 5. Create new Pull Request -------------------------------------------------------------------------------- /bitmex_backtest/bitmex_backtest.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import os 6 | from typing import Tuple, List, Any 7 | from datetime import datetime, timezone 8 | 9 | 10 | class Backtest(object): 11 | def __init__(self, *, test: bool = True) -> None: 12 | if test: 13 | self._base_url = "https://testnet.bitmex.com" 14 | else: 15 | self._base_url = "https://www.bitmex.com" 16 | self._quantity = 1 17 | self._take_profit = 0 18 | self._stop_loss = 0 19 | self._initial_deposit = 0 20 | 21 | def _resolution_to_seconds(self, resolution: str) -> int: 22 | if "D" in resolution: 23 | return int(resolution[0]) * 60 * 60 * 24 24 | elif "W" in resolution: 25 | return int(resolution[0]) * 60 * 60 * 24 * 7 26 | elif "M" in resolution: 27 | return int(resolution[0]) * 60 * 60 * 24 * 7 * 4 28 | else: 29 | return int(resolution) * 60 30 | 31 | def candles(self, symbol: str, params: Any = {}) -> pd.DataFrame: 32 | url = "{}/api/udf/history".format(self._base_url) 33 | params["symbol"] = symbol 34 | if "resolution" not in params: 35 | params["resolution"] = "1" 36 | _count = 499 37 | if "count" in params: 38 | _count = int(params["count"]) - 1 39 | if "to" not in params: 40 | params["to"] = datetime.now(timezone.utc).timestamp() 41 | if "from" not in params: 42 | params["from"] = ( 43 | params["to"] 44 | - self._resolution_to_seconds(params["resolution"]) * _count 45 | ) 46 | r = requests.get(url, params=params).json() 47 | self.df = pd.DataFrame.from_dict( 48 | { 49 | "T": pd.to_datetime(r["t"], unit="s"), 50 | "O": r["o"], 51 | "H": r["h"], 52 | "L": r["l"], 53 | "C": r["c"], 54 | "V": r["v"], 55 | }, 56 | ).set_index("T") 57 | return self.df 58 | 59 | def exists(self, filepath: str) -> bool: 60 | return os.path.exists(filepath) 61 | 62 | def to_csv(self, filepath: str) -> str: 63 | return self.df.to_csv(filepath) 64 | 65 | def read_csv(self, filepath: str) -> pd.DataFrame: 66 | self.df = pd.read_csv( 67 | filepath, index_col=0, parse_dates=True, infer_datetime_format=True 68 | ) 69 | return self.df 70 | 71 | def sma(self, *, period: int, price: str = "C") -> pd.DataFrame: 72 | return self.df[price].rolling(period).mean() 73 | 74 | def ema(self, *, period: int, price: str = "C") -> pd.DataFrame: 75 | return self.df[price].ewm(span=period).mean() 76 | 77 | def bband( 78 | self, *, period: int = 20, band: int = 2, price: str = "C" 79 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 80 | std = self.df[price].rolling(period).std() 81 | mean = self.df[price].rolling(period).mean() 82 | return mean - (std * band), mean + (std * band) 83 | 84 | def macd( 85 | self, 86 | *, 87 | fast_period: int = 12, 88 | slow_period: int = 26, 89 | signal_period: int = 9, 90 | price: str = "C" 91 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 92 | macd = ( 93 | self.df[price].ewm(span=fast_period).mean() 94 | - self.df[price].ewm(span=slow_period).mean() 95 | ) 96 | signal = macd.ewm(span=signal_period).mean() 97 | return macd, signal 98 | 99 | def stoch( 100 | self, *, k_period: int = 5, d_period: int = 3 101 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 102 | k = ( 103 | (self.df.C - self.df.L.rolling(k_period).min()) 104 | / (self.df.H.rolling(k_period).max() - self.df.L.rolling(k_period).min()) 105 | * 100 106 | ) 107 | d = k.rolling(d_period).mean() 108 | return k, d 109 | 110 | def mom(self, *, period: int = 10, price: str = "C") -> pd.DataFrame: 111 | return self.df[price].diff(period) 112 | 113 | def rsi(self, *, period: int = 14, price: str = "C") -> pd.DataFrame: 114 | return 100 - 100 / ( 115 | 1 116 | - self.df[price].diff().clip(lower=0).rolling(period).mean() 117 | / self.df[price].diff().clip(upper=0).rolling(period).mean() 118 | ) 119 | 120 | @property 121 | def buy_entry(self) -> List[bool]: 122 | return self._buy_entry 123 | 124 | @buy_entry.setter 125 | def buy_entry(self, buy_entry: pd.DataFrame) -> None: 126 | self._buy_entry = buy_entry.values 127 | 128 | @property 129 | def sell_entry(self) -> List[bool]: 130 | return self._sell_entry 131 | 132 | @sell_entry.setter 133 | def sell_entry(self, sell_entry: pd.DataFrame) -> None: 134 | self._sell_entry = sell_entry.values 135 | 136 | @property 137 | def buy_exit(self) -> List[bool]: 138 | return self._buy_exit 139 | 140 | @buy_exit.setter 141 | def buy_exit(self, buy_exit: pd.DataFrame) -> None: 142 | self._buy_exit = buy_exit.values 143 | 144 | @property 145 | def sell_exit(self) -> List[bool]: 146 | return self._sell_exit 147 | 148 | @sell_exit.setter 149 | def sell_exit(self, sell_exit: pd.DataFrame) -> None: 150 | self._sell_exit = sell_exit.values 151 | 152 | @property 153 | def quantity(self) -> int: 154 | return self._quantity 155 | 156 | @quantity.setter 157 | def quantity(self, quantity: int) -> None: 158 | self._quantity = quantity 159 | 160 | @property 161 | def take_profit(self) -> int: 162 | return self._take_profit 163 | 164 | @take_profit.setter 165 | def take_profit(self, take_profit: int) -> None: 166 | self._take_profit = take_profit 167 | 168 | @property 169 | def stop_loss(self) -> int: 170 | return self._stop_loss 171 | 172 | @stop_loss.setter 173 | def stop_loss(self, stop_loss: int) -> None: 174 | self._stop_loss = stop_loss 175 | 176 | @property 177 | def initial_deposit(self) -> int: 178 | return self._initial_deposit 179 | 180 | @initial_deposit.setter 181 | def initial_deposit(self, initial_deposit: int) -> None: 182 | self._initial_deposit = initial_deposit 183 | 184 | @property 185 | def C(self) -> pd.DataFrame: 186 | return self.df.C 187 | 188 | @property 189 | def O(self) -> pd.DataFrame: 190 | return self.df.O 191 | 192 | @property 193 | def H(self) -> pd.DataFrame: 194 | return self.df.H 195 | 196 | @property 197 | def L(self) -> pd.DataFrame: 198 | return self.df.L 199 | 200 | @property 201 | def V(self) -> pd.DataFrame: 202 | return self.df.V 203 | 204 | def run(self) -> pd.Series: 205 | o = self.df.O.values 206 | l = self.df.L.values 207 | h = self.df.H.values 208 | N = len(self.df) 209 | 210 | long_trade = np.zeros(N) 211 | short_trade = np.zeros(N) 212 | 213 | # buy entry 214 | buy_entry_s = np.hstack((False, self._buy_entry[:-1])) # shift 215 | long_trade[buy_entry_s] = o[buy_entry_s] 216 | 217 | # buy exit 218 | buy_exit_s = np.hstack((False, self._buy_exit[:-2], True)) # shift 219 | long_trade[buy_exit_s] = -o[buy_exit_s] 220 | 221 | # sell entry 222 | sell_entry_s = np.hstack((False, self._sell_entry[:-1])) # shift 223 | short_trade[sell_entry_s] = o[sell_entry_s] 224 | 225 | # sell exit 226 | sell_exit_s = np.hstack((False, self._sell_exit[:-2], True)) # shift 227 | short_trade[sell_exit_s] = -(o[sell_exit_s]) 228 | 229 | long_pl = pd.Series(np.zeros(N)) # profit/loss of buy position 230 | short_pl = pd.Series(np.zeros(N)) # profit/loss of sell position 231 | buy_price = sell_price = 0 232 | long_rr = [] # long return rate 233 | short_rr = [] # short return rate 234 | stop_loss = take_profit = 0 235 | 236 | for i in range(1, N): 237 | # buy entry 238 | if long_trade[i] > 0: 239 | if buy_price == 0: 240 | buy_price = long_trade[i] 241 | short_trade[i] = -buy_price # sell exit 242 | else: 243 | long_trade[i] = 0 244 | 245 | # sell entry 246 | if short_trade[i] > 0: 247 | if sell_price == 0: 248 | sell_price = short_trade[i] 249 | long_trade[i] = -sell_price # buy exit 250 | else: 251 | short_trade[i] = 0 252 | 253 | # buy exit 254 | if long_trade[i] < 0: 255 | if buy_price != 0: 256 | long_pl[i] = ( 257 | -(buy_price + long_trade[i]) * self._quantity 258 | ) # profit/loss fixed 259 | long_rr.append( 260 | round(long_pl[i] / buy_price * 100, 2) 261 | ) # long return rate 262 | buy_price = 0 263 | else: 264 | long_trade[i] = 0 265 | 266 | # sell exit 267 | if short_trade[i] < 0: 268 | if sell_price != 0: 269 | short_pl[i] = ( 270 | sell_price + short_trade[i] 271 | ) * self._quantity # profit/loss fixed 272 | short_rr.append( 273 | round(short_pl[i] / sell_price * 100, 2) 274 | ) # short return rate 275 | sell_price = 0 276 | else: 277 | short_trade[i] = 0 278 | 279 | # close buy position with stop loss 280 | if buy_price != 0 and self._stop_loss > 0: 281 | stop_price = buy_price - self._stop_loss 282 | if l[i] <= stop_price: 283 | long_trade[i] = -stop_price 284 | long_pl[i] = ( 285 | -(buy_price + long_trade[i]) * self._quantity 286 | ) # profit/loss fixed 287 | long_rr.append( 288 | round(long_pl[i] / buy_price * 100, 2) 289 | ) # long return rate 290 | buy_price = 0 291 | stop_loss += 1 292 | 293 | # close buy positon with take profit 294 | if buy_price != 0 and self._take_profit > 0: 295 | limit_price = buy_price + self._take_profit 296 | if h[i] >= limit_price: 297 | long_trade[i] = -limit_price 298 | long_pl[i] = ( 299 | -(buy_price + long_trade[i]) * self._quantity 300 | ) # profit/loss fixed 301 | long_rr.append( 302 | round(long_pl[i] / buy_price * 100, 2) 303 | ) # long return rate 304 | buy_price = 0 305 | take_profit += 1 306 | 307 | # close sell position with stop loss 308 | if sell_price != 0 and self._stop_loss > 0: 309 | stop_price = sell_price + self._stop_loss 310 | if h[i] >= stop_price: 311 | short_trade[i] = -stop_price 312 | short_pl[i] = ( 313 | sell_price + short_trade[i] 314 | ) * self._quantity # profit/loss fixed 315 | short_rr.append( 316 | round(short_pl[i] / sell_price * 100, 2) 317 | ) # short return rate 318 | sell_price = 0 319 | stop_loss += 1 320 | 321 | # close sell position with take profit 322 | if sell_price != 0 and self._take_profit > 0: 323 | limit_price = sell_price - self._take_profit 324 | if l[i] <= limit_price: 325 | short_trade[i] = -limit_price 326 | short_pl[i] = ( 327 | sell_price + short_trade[i] 328 | ) * self._quantity # profit/loss fixed 329 | short_rr.append( 330 | round(short_pl[i] / sell_price * 100, 2) 331 | ) # short return rate 332 | sell_price = 0 333 | take_profit += 1 334 | 335 | win_trades = np.count_nonzero(long_pl.clip(lower=0)) + np.count_nonzero( 336 | short_pl.clip(lower=0) 337 | ) 338 | lose_trades = np.count_nonzero(long_pl.clip(upper=0)) + np.count_nonzero( 339 | short_pl.clip(upper=0) 340 | ) 341 | trades = (np.count_nonzero(long_trade) // 2) + ( 342 | np.count_nonzero(short_trade) // 2 343 | ) 344 | gross_profit = long_pl.clip(lower=0).sum() + short_pl.clip(lower=0).sum() 345 | gross_loss = long_pl.clip(upper=0).sum() + short_pl.clip(upper=0).sum() 346 | profit_pl = gross_profit + gross_loss 347 | self.equity = (long_pl + short_pl).cumsum() 348 | mdd = (self.equity.cummax() - self.equity).max() 349 | self.return_rate = pd.Series(short_rr + long_rr) 350 | 351 | s = pd.Series(dtype="object") 352 | s.loc["total profit"] = round(profit_pl, 3) 353 | s.loc["total trades"] = trades 354 | s.loc["win rate"] = round(win_trades / trades * 100, 3) 355 | s.loc["profit factor"] = round(-gross_profit / gross_loss, 3) 356 | s.loc["maximum drawdown"] = round(mdd, 3) 357 | s.loc["recovery factor"] = round(profit_pl / mdd, 3) 358 | s.loc["riskreward ratio"] = round( 359 | -(gross_profit / win_trades) / (gross_loss / lose_trades), 3 360 | ) 361 | s.loc["sharpe ratio"] = round( 362 | self.return_rate.mean() / self.return_rate.std(), 3 363 | ) 364 | s.loc["average return"] = round(self.return_rate.mean(), 3) 365 | s.loc["stop loss"] = stop_loss 366 | s.loc["take profit"] = take_profit 367 | return s 368 | 369 | def plot(self, filepath: str = "") -> None: 370 | plt.subplot(2, 1, 1) 371 | plt.plot(self.equity + self._initial_deposit, label="equity") 372 | plt.legend() 373 | plt.subplot(2, 1, 2) 374 | plt.hist(self.return_rate, 50, rwidth=0.9) 375 | plt.axvline( 376 | sum(self.return_rate) / len(self.return_rate), 377 | color="orange", 378 | label="average return", 379 | ) 380 | plt.legend() 381 | if filepath == "": 382 | plt.show() 383 | else: 384 | plt.savefig(filepath) 385 | 386 | if __name__ == "__main__": 387 | bt = Backtest(test=True) 388 | filepath = "xbtusd-60.csv" 389 | if bt.exists(filepath): 390 | bt.read_csv(filepath) 391 | else: 392 | params = { 393 | "resolution": "60", # 1 hour candlesticks (default=1) 1,3,5,15,30,60,120,180,240,360,720,1D,3D,1W,2W,1M 394 | "count": "5000" # 5000 candlesticks (default=500) 395 | } 396 | bt.candles("XBTUSD", params) 397 | bt.to_csv(filepath) 398 | 399 | fast_ma = bt.sma(period=10) 400 | slow_ma = bt.sma(period=30) 401 | exit_ma = bt.sma(period=5) 402 | bt.buy_entry = (fast_ma > slow_ma) & (fast_ma.shift() <= slow_ma.shift()) 403 | bt.sell_entry = (fast_ma < slow_ma) & (fast_ma.shift() >= slow_ma.shift()) 404 | bt.buy_exit = (bt.C < exit_ma) & (bt.C.shift() >= exit_ma.shift()) 405 | bt.sell_exit = (bt.C > exit_ma) & (bt.C.shift() <= exit_ma.shift()) 406 | 407 | bt.quantity = 100 # default=1 408 | bt.stop_loss = 200 # stop loss (default=0) 409 | bt.take_profit = 1000 # take profit (default=0) 410 | 411 | print(bt.run()) 412 | bt.plot("backtest.png") 413 | --------------------------------------------------------------------------------