├── .gitattributes ├── .gitignore ├── .idea ├── .name ├── backtrader-binance-bot-master.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── other.xml ├── vcs.xml └── workspace.xml ├── Data └── GDAX │ ├── BCH │ ├── .15Min.csv.swp │ ├── 1Hour.csv │ └── 4Hour.csv │ ├── BTC │ ├── .15Min.csv.swp │ ├── 1Hour.csv │ └── 4Hour.csv │ ├── ETH │ ├── .15Min.csv.swp │ ├── 1Hour.csv │ ├── 1_Min.csv │ ├── 3_Min.csv │ └── 4Hour.csv │ └── LTC │ ├── .15Min.csv.swp │ ├── 1Hour.csv │ └── 4Hour.csv ├── Dockerfile ├── LICENSE ├── Makefile ├── OptStrategy.py ├── PRODUCTION.py ├── PrepareCSV.py ├── README.md ├── TestStrategy.py ├── WFA.py ├── WFO.py ├── WalkForward ├── LICENSE ├── README.md ├── WalkForwardWorksheet.ipynb ├── blackbox.py ├── output.csv └── walkforwardworksheet.py ├── btwalkforward.py ├── ccxtbt ├── __init__.py ├── ccxtbroker.py ├── ccxtfeed.py └── ccxtstore.py ├── config.py ├── dataset ├── binance_nov_18_mar_19_btc.csv ├── dataset.py ├── queries.sql └── symbol_config.yaml ├── functions.py ├── img.png ├── img_1.png ├── indicators ├── macd_hist.py └── stoch_rsi.py ├── oa1bbtwalkforward.py ├── requirements.txt ├── screenshot.png ├── sizer └── percent.py ├── strategies ├── BBKCBreak.py ├── BBKCReverse.py ├── BBReverse.py ├── BasicRSI.py ├── BollReverse.py ├── BollingBear.py ├── ConnorRSI.py ├── DonchainChannels.py ├── DualMA.py ├── DualMASign.py ├── MovingAverage.py ├── RSI.py ├── SectorRollRSI.py ├── SimpleBollinger.py ├── Swing.py ├── TrendLine.py ├── VolatileBoll.py ├── base.py ├── draft.py ├── longshort.py └── study_strategy.py ├── test.json ├── test.py ├── toolkit.py ├── utils.py └── walkforward.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | basic_rsi.py -------------------------------------------------------------------------------- /.idea/backtrader-binance-bot-master.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Data/GDAX/BCH/.15Min.csv.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/Data/GDAX/BCH/.15Min.csv.swp -------------------------------------------------------------------------------- /Data/GDAX/BTC/.15Min.csv.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/Data/GDAX/BTC/.15Min.csv.swp -------------------------------------------------------------------------------- /Data/GDAX/ETH/.15Min.csv.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/Data/GDAX/ETH/.15Min.csv.swp -------------------------------------------------------------------------------- /Data/GDAX/LTC/.15Min.csv.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/Data/GDAX/LTC/.15Min.csv.swp -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | WORKDIR /app 3 | ADD requirements.txt . 4 | RUN pip install -r requirements.txt 5 | CMD ["./main.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rodrigo Brito 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | virtualenv -p python3 venv 3 | 4 | install: 5 | pip install -r requirements.txt 6 | 7 | build-docker: 8 | docker build -t rodrigobrito/backtrader . 9 | 10 | run: 11 | docker run -ti -v`pwd`:/app -d -e ENVIRONMENT=production rodrigobrito/backtrader 12 | -------------------------------------------------------------------------------- /OptStrategy.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from datetime import datetime 3 | import optunity.metrics 4 | import toolkit as tk 5 | from PrepareCSV import prepare_data 6 | from backtrader_plotting import Bokeh 7 | import quantstats 8 | import math 9 | import backtrader as bt 10 | from strategies.BBKCReverse import BBKCReverse 11 | 12 | 13 | class CommInfoFractional(bt.CommissionInfo): 14 | def getsize(self, price, cash): 15 | '''Returns fractional size for cash operation @price''' 16 | return self.p.leverage * (cash / price) 17 | 18 | 19 | class AllSizer(bt.Sizer): 20 | def _getsizing(self, comminfo, cash, data, isbuy): 21 | if isbuy is True: 22 | return math.floor(cash / data.high) 23 | if isbuy is False: 24 | return math.floor(cash / data.low) 25 | else: 26 | return self.broker.getposition(data) 27 | 28 | 29 | class MyBuySell(bt.observers.BuySell): 30 | plotlines = dict( 31 | buy=dict(marker='^', markersize=4.0, color='lime', fillstyle='full'), 32 | sell=dict(marker='v', markersize=4.0, color='red', fillstyle='full') 33 | # 34 | # buy=dict(marker='$++$', markersize=12.0), 35 | # sell=dict(marker='$--$', markersize=12.0) 36 | # 37 | # buy=dict(marker='$✔$', markersize=12.0), 38 | # sell=dict(marker='$✘$', markersize=12.0) 39 | ) 40 | 41 | 42 | # ---------------------- 43 | # prepare data 44 | t0str, t9str = '2019-01-01', '2020-01-01' 45 | symbol = 'ETHUSDT' 46 | fgCov = False 47 | df = prepare_data(t0str, t9str, symbol, fgCov=fgCov, prep_new=True, mode='opt') 48 | # print(df) 49 | data = tk.pools_get4df(df, t0str, t9str, fgCov=fgCov) 50 | dataha = data.clone() 51 | dataha.addfilter(bt.filters.HeikinAshi(dataha)) 52 | 53 | 54 | def runstrat(window, bbdevs, kcdevs, bias_pct, volatile_pct): 55 | cerebro = bt.Cerebro() 56 | cerebro.addstrategy(BBKCReverse, window=int(window), bbdevs=bbdevs, bias_pct=bias_pct, kcdevs=int(kcdevs), volatile_pct=volatile_pct) 57 | dmoney0 = 100.0 58 | cerebro.broker.setcash(dmoney0) 59 | # cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='SharpeRatio', timeframe=bt.TimeFrame.Months, compression=3, factor=4) 60 | cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn") 61 | cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') 62 | commission = 0.0004 63 | comminfo = CommInfoFractional(commission=commission) 64 | cerebro.broker.addcommissioninfo(comminfo) 65 | cerebro.addsizer(bt.sizers.FixedSize) 66 | tframes = dict(minutes=bt.TimeFrame.Minutes, days=bt.TimeFrame.Days, weeks=bt.TimeFrame.Weeks, months=bt.TimeFrame.Months, years=bt.TimeFrame.Years) 67 | cerebro.resampledata(data, timeframe=tframes['minutes'], compression=1, name='Real') 68 | cerebro.resampledata(dataha, timeframe=tframes['minutes'], compression=1, name='Heikin') 69 | # cerebro.adddata(data) 70 | results = cerebro.run() 71 | strat = results[0] 72 | anzs = strat.analyzers 73 | # 74 | warnings.filterwarnings('ignore') 75 | portfolio_stats = strat.analyzers.getbyname('pyfolio') 76 | returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items() 77 | returns.index = returns.index.tz_convert(None) 78 | dsharp = quantstats.stats.smart_sharpe(returns, periods=365) 79 | sortino = quantstats.stats.smart_sortino(returns, periods=365) 80 | sqn = anzs.sqn.get_analysis().sqn 81 | dcash9 = cerebro.broker.getvalue() 82 | dcash0 = cerebro.broker.startingcash 83 | pnl = dcash9 / dcash0 - 1 84 | if dsharp is not None: 85 | if (dsharp > 0) and (sqn > 1) and (sortino > 0) and (pnl > 0): 86 | param = sqn * dsharp * pnl**0.5 * sortino 87 | else: 88 | param = 0 89 | else: 90 | param = 0 91 | # return 92 | print(f'(分析指标 SQN={sqn}, Sharp_Ratio={dsharp}, Sortino={sortino}, pnl={pnl})') 93 | return param 94 | 95 | 96 | starttime = datetime.now() 97 | opt = optunity.maximize(runstrat, num_evals=100, solver_name='particle swarm', window=[1, 200], bbdevs=[0.01, 5.0], kcdevs=[0.01, 5.0], bias_pct=[0.01, 0.20], volatile_pct=[0.01, 0.20]) 98 | # long running 99 | endtime = datetime.now() 100 | duringtime = endtime - starttime 101 | print('time cost: ', duringtime) 102 | # 得到最优参数结果 103 | optimal_pars, details, _ = opt 104 | print('Optimal Parameters:') 105 | print('策略参数 window=%s, bbdevs=%s, kcdevs=%s, bias_pct=%s, volatile_pct=%s' % (optimal_pars['window'], optimal_pars['bbdevs'], optimal_pars['kcdevs'], optimal_pars['bias_pct'], optimal_pars['volatile_pct'])) 106 | # 利用最优参数最后回测一次,作图 107 | cerebro = bt.Cerebro() 108 | cerebro.addstrategy(BBKCReverse, window=int(optimal_pars['window']), bbdevs=optimal_pars['bbdevs'], bias_pct=optimal_pars['bias_pct'], kcdevs=optimal_pars['kcdevs'], volatile_pct=optimal_pars['volatile_pct']) 109 | 110 | dmoney0 = 100.0 111 | cerebro.broker.setcash(dmoney0) 112 | dcash0 = cerebro.broker.startingcash 113 | commission = 0.0004 114 | comminfo = CommInfoFractional(commission=commission) 115 | cerebro.broker.addcommissioninfo(comminfo) 116 | cerebro.addsizer(bt.sizers.FixedSize) 117 | tframes = dict(minutes=bt.TimeFrame.Minutes, days=bt.TimeFrame.Days, weeks=bt.TimeFrame.Weeks, months=bt.TimeFrame.Months, years=bt.TimeFrame.Years) 118 | cerebro.resampledata(data, timeframe=tframes['minutes'], compression=5, name='Real') 119 | cerebro.resampledata(dataha, timeframe=tframes['minutes'], compression=5, name='Heikin') 120 | bt.observers.BuySell = MyBuySell 121 | 122 | # 设置pyfolio分析参数 123 | cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') 124 | # 125 | print('\n\t#运行cerebro') 126 | results = cerebro.run(maxcpus=True) 127 | # results = cerebro.run(runonce=False, exactbars=-2) 128 | print('\n#基本BT量化分析数据') 129 | dval9 = cerebro.broker.getvalue() 130 | dget = dval9 - dcash0 131 | kret = dget / dcash0 * 100 132 | # 最终投资组合价值 133 | strat = results[0] 134 | print('\t起始资金 Starting Portfolio Value: %.2f' % dcash0) 135 | print('\t资产总值 Final Portfolio Value: %.2f' % dval9) 136 | print('\t利润总额: %.2f,' % dget) 137 | print('\tROI投资回报率 Return on investment: %.2f %%' % kret) 138 | 139 | print('\n==================================================') 140 | print('\n quantstats专业量化分析图表\n') 141 | warnings.filterwarnings('ignore') 142 | portfolio_stats = strat.analyzers.getbyname('pyfolio') 143 | returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items() 144 | returns.index = returns.index.tz_convert(None) 145 | quantstats.reports.html(returns, output='..//data//optstats.html', title='Crypto Sentiment') 146 | # 147 | print('\n#绘制BT量化分析图形') 148 | # try: 149 | # b = Bokeh(plot_mode='single', output_mode='save', filename='..//data//optreport.html') 150 | # cerebro.plot(b) 151 | # except: 152 | cerebro.plot() 153 | 154 | # 155 | # 156 | -------------------------------------------------------------------------------- /PRODUCTION.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import backtrader as bt 5 | import datetime as dt 6 | import yaml 7 | from ccxtbt import CCXTStore 8 | from config import BINANCE, ENV, PRODUCTION, COIN_TARGET, COIN_REFER, DEBUG 9 | 10 | from dataset.dataset import CustomDataset 11 | from sizer.percent import FullMoney 12 | from strategies.BasicRSI import BasicRSI 13 | from utils import print_trade_analysis, print_sqn, send_telegram_message 14 | 15 | from PrepareCSV import prepare_data 16 | import toolkit as tk 17 | 18 | 19 | class AcctValue(bt.Observer): 20 | alias = ('Value',) 21 | lines = ('value',) 22 | 23 | plotinfo = {"plot": True, "subplot": True} 24 | 25 | def next(self): 26 | self.lines.value[0] = self._owner.broker.getvalue() # Get today's account value (cash + stocks) 27 | 28 | 29 | class AcctStats(bt.Analyzer): 30 | """A simple analyzer that gets the gain in the value of the account; should be self-explanatory""" 31 | 32 | def __init__(self): 33 | self.start_val = self.strategy.broker.get_value() 34 | self.end_val = None 35 | 36 | def stop(self): 37 | self.end_val = self.strategy.broker.get_value() 38 | 39 | def get_analysis(self): 40 | return {"start": self.start_val, "end": self.end_val, 41 | "growth": self.end_val - self.start_val, "return": self.end_val / self.start_val} 42 | 43 | 44 | def main(): 45 | cerebro = bt.Cerebro(quicknotify=True) 46 | 47 | if ENV == PRODUCTION: # Live trading with Binance 48 | broker_config = { 49 | 'apiKey': BINANCE.get("key"), 50 | 'secret': BINANCE.get("secret"), 51 | 'timeout': 5000, 52 | 'verbose': False, 53 | 'nonce': lambda: str(int(time.time() * 1000)), 54 | 'enableRateLimit': True, 55 | } 56 | 57 | store = CCXTStore(exchange='binanceusdm', currency=COIN_REFER, config=broker_config, retries=5, debug=False, 58 | sandbox=True) 59 | 60 | broker_mapping = { 61 | 'order_types': { 62 | bt.Order.Market: 'market', 63 | bt.Order.Limit: 'limit', 64 | bt.Order.Stop: 'stop-loss', 65 | bt.Order.StopLimit: 'stop limit' 66 | }, 67 | 'mappings': { 68 | 'closed_order': { 69 | 'key': 'status', 70 | 'value': 'closed' 71 | }, 72 | 'canceled_order': { 73 | 'key': 'status', 74 | 'value': 'canceled' 75 | } 76 | } 77 | } 78 | broker = store.getbroker(broker_mapping=broker_mapping) 79 | cerebro.setbroker(broker) 80 | 81 | hist_start_date = dt.datetime.utcnow() - dt.timedelta(minutes=3000) 82 | with open('dataset/symbol_config.yaml', mode='r') as f: 83 | symbol_config = f.read() 84 | symbol_config = yaml.load(symbol_config, Loader=yaml.FullLoader) 85 | for symbol in symbol_config.keys(): 86 | datakl = store.getdata( 87 | dataname=f'{symbol}', 88 | name=f'{symbol}', 89 | timeframe=bt.TimeFrame.Minutes, 90 | fromdate=hist_start_date, 91 | compression=1, 92 | ohlcv_limit=10000 93 | ) 94 | dataha = datakl.clone() 95 | dataha.addfilter(bt.filters.HeikinAshi(dataha)) 96 | # Add the feed 97 | cerebro.adddata(datakl, name=f'{symbol}_Kline') 98 | cerebro.adddata(dataha, name=f'{symbol}_Heikin') 99 | 100 | else: # Backtesting with CSV file 101 | with open('dataset/symbol_config.yaml', mode='r') as f: 102 | symbol_config = f.read() 103 | symbol_config = yaml.load(symbol_config, Loader=yaml.FullLoader) 104 | for symbol in symbol_config.keys(): 105 | t0str, t9str = '2021-09-01', '2021-10-01' 106 | fgCov = False 107 | df = prepare_data(t0str, t9str, symbol, fgCov=fgCov, prep_new=True, mode='test') 108 | datakl = tk.pools_get4df(df, t0str, t9str, fgCov=fgCov) 109 | dataha = datakl.clone() 110 | dataha.addfilter(bt.filters.HeikinAshi(dataha)) 111 | cerebro.resampledata(datakl, name=f'{symbol}_10m', timeframe=bt.TimeFrame.Minutes, compression=10) 112 | cerebro.resampledata(dataha, name=f'{symbol}_10m_Heikin', timeframe=bt.TimeFrame.Minutes, compression=10) 113 | f.close() 114 | broker = cerebro.getbroker() 115 | broker.setcommission(commission=0.0004, name=COIN_TARGET) # Simulating exchange fee 116 | broker.setcash(100000.0) 117 | cerebro.addsizer(FullMoney) 118 | 119 | # Analyzers to evaluate trades and strategies 120 | # SQN = Average( profit / risk ) / StdDev( profit / risk ) x SquareRoot( number of trades ) 121 | # cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta") 122 | # cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn") 123 | 124 | # Include Strategy 125 | strategy = [] 126 | strategy_list = [] 127 | for symbol in symbol_config.keys(): 128 | strategy_list = list(set(strategy).union(set(symbol.keys()))) 129 | for strategy in strategy_list: 130 | cerebro.addstrategy(strategy) 131 | # cerebro.broker.set_checksubmit(False) 132 | # Starting backtrader bot 133 | initial_value = cerebro.broker.getvalue() 134 | print('Starting Portfolio Value: %.2f' % initial_value) 135 | result = cerebro.run() 136 | # Print analyzers - results 137 | final_value = cerebro.broker.getvalue() 138 | print('Final Portfolio Value: %.2f' % final_value) 139 | print('Profit %.3f%%' % ((final_value - initial_value) / initial_value * 100)) 140 | print_trade_analysis(result[0].analyzers.ta.get_analysis()) 141 | print_sqn(result[0].analyzers.sqn.get_analysis()) 142 | if DEBUG: 143 | cerebro.plot() 144 | 145 | 146 | if __name__ == "__main__": 147 | try: 148 | main() 149 | except KeyboardInterrupt: 150 | print("finished.") 151 | time = dt.datetime.now().strftime("%d-%m-%y %H:%M") 152 | send_telegram_message("Bot finished by user at %s" % time) 153 | except Exception as err: 154 | send_telegram_message("Bot finished with error: %s" % err) 155 | print("Finished with error: ", err) 156 | raise 157 | -------------------------------------------------------------------------------- /PrepareCSV.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import datetime 3 | import os 4 | 5 | 6 | def prepare_data(time0, time9, symbol, fgCov=False, prep_new=True, mode='test'): 7 | path = 'D://Data//binance//futures//' 8 | df9path = f'..//data//{symbol}_1m_{mode}.csv' 9 | if prep_new: 10 | time0 = pd.to_datetime(time0) 11 | time0 = datetime.datetime.date(time0) 12 | time9 = pd.to_datetime(time9) 13 | time9 = datetime.datetime.date(time9) 14 | time5 = time0 15 | df9 = pd.DataFrame() 16 | while time5 <= time9: 17 | try: 18 | file = path + str(time5) + '//' + str(time5) + '_' + symbol + '_1m.csv' 19 | df0 = pd.read_csv(file) 20 | df0['datetime'] = [x[:19] for x in df0['candle_begin_time']] 21 | df0.set_index('datetime', drop=True, inplace=True) 22 | df0.index = pd.to_datetime(df0.index, format='%Y-%m-%d %H:%M:%S') 23 | df0.sort_index(ascending=True, inplace=True) 24 | except: 25 | time5 = time5 + datetime.timedelta(days=1) 26 | file = path + str(time5) + '//' + str(time5) + '_' + symbol + '_1m.csv' 27 | df0 = pd.read_csv(file) 28 | df0['datetime'] = [x[:19] for x in df0['candle_begin_time']] 29 | df0.set_index('datetime', drop=True, inplace=True) 30 | df0.index = pd.to_datetime(df0.index, format='%Y-%m-%d %H:%M:%S') 31 | df0.sort_index(ascending=True, inplace=True) 32 | df9 = df9.append(df0) 33 | df9.drop(columns=['candle_begin_time'], inplace=True) 34 | time5 = time5 + datetime.timedelta(days=1) 35 | # print(df9) 36 | # df9.reset_index(inplace=True) 37 | # df9['candle_begin_time'].dt.strftime('%Y-%m-%d %H:%M:%S') 38 | # as_list = df9['candle_begin_time'].tolist() 39 | # for x in as_list: 40 | # if '.000' in str(x): 41 | # idx = as_list.index(x) 42 | # as_list[idx] = idx[0:8] 43 | # df9.set_index(as_list, drop=True, inplace=True) 44 | 45 | df9.sort_index(ascending=True, inplace=True) 46 | df9.index = pd.to_datetime(df9.index, format='%Y-%m-%d %H:%M:%S') 47 | 48 | # df9 = df9.to_csv() 49 | if fgCov: 50 | df9 = df9 51 | else: 52 | df9.to_csv(df9path) 53 | df9 = df9path 54 | return df9 55 | else: 56 | df9 = df9path 57 | return df9 58 | 59 | 60 | # if __name__ == '__main__': 61 | # time0, time9, symbol = '2018-01-01', '2020-01-01', 'ETHUSDT' 62 | # prepare_data(time0, time9, symbol, fgCov=False, prep_new=False) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Binance Futures trading and backtesting framework based on rodrigo-brito's backtrader-binance-bot 2 | 3 | ### Special thanks to rodrigo-brito 4 | 5 | ### Installation 6 | 7 | Activating [Virtualenv](https://virtualenv.pypa.io/en/latest/) 8 | ``` 9 | make init 10 | source venv/bin/activate 11 | ``` 12 | 13 | Installing dependencies 14 | ``` 15 | make install 16 | ``` 17 | 18 | Start application 19 | ``` 20 | ./main.py 21 | ``` 22 | 23 | ## Results 24 | 25 | ![alt text](screenshot.png "Backtrader Simulation") 26 | 27 | 28 | ``` 29 | Starting Portfolio Value: 100000.00 30 | Final Portfolio Value: 119192.61 31 | 32 | Profit 19.193% 33 | Trade Analysis Results: 34 | Total Open Total Closed Total Won Total Lost 35 | 0 10 7 3 36 | Strike Rate Win Streak Losing Streak PnL Net 37 | 1 5 2 19192.61 38 | SQN: 1.75 39 | ``` 40 | 41 | ## To do list 42 | 43 | 多策略,多数据源,多时间周期的backtrader框架 44 | 45 | 自身带有前进式回测分析WFA 46 | 47 | 策略能够方便的加入一些仓位控制方法(简单如凯利公式) 48 | # 49 | 50 | ## 至今的所有修改进度 51 | 52 | 20211201 53 | 多策略部分除了分配持仓比例部分未写好其它部分已经完全搞定(还未debug) 54 | 55 | 目前能加入多币种多时间周期(未debug) 56 | 57 | 将原先的production.py复制了出来, 需要参照OptStrategy来要写成能够用来前进式优化的框架WFO 58 | 59 | 保留原先OptStrategy, 作为单策略优化的框架 60 | 61 | 需要将TestStrategy修改为一次能够检验多策略, 多数据源的框架 62 | 63 | Production实盘框架中的WFO功能替换为给用户发送回测任务, 手动回测优化 64 | 65 | 接触到了新的优化模块blackbox, 还未试用 66 | 67 | 当前正在完善WFO单策略优化的内容 68 | -------------------------------------------------------------------------------- /TestStrategy.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import quantstats 4 | import toolkit as tk 5 | from PrepareCSV import prepare_data 6 | import backtrader as bt 7 | from backtrader.analyzers import SQN, AnnualReturn, TimeReturn, SharpeRatio,TradeAnalyzer 8 | from backtrader_plotting import Bokeh 9 | from backtrader_plotting.schemes import Tradimo 10 | import warnings 11 | # import strategy 12 | from strategies.BBReverse import BBReverse 13 | from strategies.SimpleBollinger import SimpleBollinger 14 | from strategies.BBKCReverse import BBKCReverse 15 | from strategies.BBKCBreak import BBKCBreak 16 | 17 | 18 | class CommInfoFractional(bt.CommissionInfo): 19 | def getsize(self, price, cash): 20 | '''Returns fractional size for cash operation @price''' 21 | return self.p.leverage * (cash / price) 22 | 23 | 24 | class FixedReverser(bt.Sizer): 25 | params = (('stake', 50),) 26 | 27 | def _getsizing(self, comminfo, cash, data, isbuy): 28 | position = self.broker.getposition(data) 29 | size = self.p.stake * (1 + (position.size != 0)) 30 | return size 31 | 32 | 33 | class MyBuySell(bt.observers.BuySell): 34 | plotlines = dict( 35 | buy=dict(marker='^', markersize=4.0, color='green', fillstyle='full'), 36 | sell=dict(marker='v', markersize=4.0, color='red', fillstyle='full') 37 | # 38 | # buy=dict(marker='$++$', markersize=12.0), 39 | # sell=dict(marker='$--$', markersize=12.0) 40 | # 41 | # buy=dict(marker='$✔$', markersize=12.0), 42 | # sell=dict(marker='$✘$', markersize=12.0) 43 | ) 44 | 45 | 46 | class MyTrades(bt.observers.Trades): 47 | plotlines = dict( 48 | pnlplus=dict(_name='Positive', 49 | ls='', marker='o', color='blue', 50 | markersize=4.0, fillstyle='full'), 51 | pnlminus=dict(_name='Negative', 52 | ls='', marker='o', color='red', 53 | markersize=4.0, fillstyle='full') 54 | ) 55 | 56 | 57 | def runstrat(args=None): 58 | # setup entrance 59 | cerebro = bt.Cerebro() 60 | cerebro.addstrategy(BBKCBreak) 61 | # cerebro.addstrategy(SimpleBollinger) 62 | # prepare data 63 | t0str, t9str = '2019-01-01', '2021-10-01' 64 | symbol = 'ETHUSDT' 65 | fgCov = False 66 | df = prepare_data(t0str, t9str, symbol, fgCov=fgCov, prep_new=True, mode='test') 67 | data = tk.pools_get4df(df, t0str, t9str, fgCov=fgCov) 68 | dataha = data.clone() 69 | dataha.addfilter(bt.filters.HeikinAshi(dataha)) 70 | tframes = dict(minutes=bt.TimeFrame.Minutes, days=bt.TimeFrame.Days, weeks=bt.TimeFrame.Weeks, months=bt.TimeFrame.Months, years=bt.TimeFrame.Years) 71 | cerebro.resampledata(data, timeframe=tframes['minutes'], compression=5, name='Real') 72 | cerebro.resampledata(dataha, timeframe=tframes['minutes'], compression=5, name='Heikin') 73 | # cerebro.adddata(data) 74 | dmoney0 = 100.0 75 | cerebro.broker.setcash(dmoney0) 76 | dcash0 = cerebro.broker.startingcash 77 | commission = 0.0004 78 | comminfo = CommInfoFractional(commission=commission) 79 | cerebro.broker.addcommissioninfo(comminfo) 80 | cerebro.addsizer(bt.sizers.FixedSize) 81 | bt.observers.BuySell = MyBuySell 82 | bt.observers.Trades = MyTrades 83 | # 设置pyfolio分析参数 84 | cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') 85 | # 86 | print('\n\t#运行cerebro') 87 | results = cerebro.run(maxcpus=True) 88 | # results = cerebro.run(runonce=False, exactbars=-2) 89 | print('\n#基本BT量化分析数据') 90 | dval9 = cerebro.broker.getvalue() 91 | dget = dval9 - dcash0 92 | kret = dget / dcash0 * 100 93 | # 最终投资组合价值 94 | strat = results[0] 95 | print('\t起始资金 Starting Portfolio Value: %.2f' % dcash0) 96 | print('\t资产总值 Final Portfolio Value: %.2f' % dval9) 97 | print('\t利润总额: %.2f,' % dget) 98 | print('\tROI投资回报率 Return on investment: %.2f %%' % kret) 99 | 100 | print('\n==================================================') 101 | print('\n quantstats专业量化分析图表\n') 102 | warnings.filterwarnings('ignore') 103 | portfolio_stats = strat.analyzers.getbyname('pyfolio') 104 | returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items() 105 | returns.index = returns.index.tz_convert(None) 106 | quantstats.reports.html(returns, output='..//data//teststats.html', title='Crypto Sentiment') 107 | # 108 | print('\n#绘制BT量化分析图形') 109 | # try: 110 | # b = Bokeh(plot_mode='single', output_mode='show', filename='..//data//report.html') 111 | # cerebro.plot(b) 112 | # except: 113 | cerebro.plot(style='candlestick') 114 | 115 | 116 | if __name__ == '__main__': 117 | runstrat() 118 | -------------------------------------------------------------------------------- /WFA.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import backtrader.indicators as btind 3 | import datetime as dt 4 | import pandas as pd 5 | import pandas_datareader as web 6 | from pandas import Series, DataFrame 7 | import random 8 | from copy import deepcopy 9 | 10 | 11 | class SMAC(bt.Strategy): 12 | """A simple moving average crossover strategy; crossing of a fast and slow moving average generates buy/sell 13 | signals""" 14 | params = {"fast": 20, "slow": 50, # The windows for both fast and slow moving averages 15 | "optim": False, "optim_fs": (20, 50)} # Used for optimization; equivalent of fast and slow, but a tuple 16 | 17 | # The first number in the tuple is the fast MA's window, the 18 | # second the slow MA's window 19 | 20 | def __init__(self): 21 | """Initialize the strategy""" 22 | 23 | self.fastma = dict() 24 | self.slowma = dict() 25 | self.regime = dict() 26 | 27 | if self.params.optim: # Use a tuple during optimization 28 | self.params.fast, self.params.slow = self.params.optim_fs # fast and slow replaced by tuple's contents 29 | 30 | if self.params.fast > self.params.slow: 31 | raise ValueError( 32 | "A SMAC strategy cannot have the fast moving average's window be " + \ 33 | "greater than the slow moving average window.") 34 | 35 | for d in self.getdatanames(): 36 | # The moving averages 37 | self.fastma[d] = btind.SimpleMovingAverage(self.getdatabyname(d), # The symbol for the moving average 38 | period=self.params.fast, # Fast moving average 39 | plotname="FastMA: " + d) 40 | self.slowma[d] = btind.SimpleMovingAverage(self.getdatabyname(d), # The symbol for the moving average 41 | period=self.params.slow, # Slow moving average 42 | plotname="SlowMA: " + d) 43 | 44 | # Get the regime 45 | self.regime[d] = self.fastma[d] - self.slowma[d] # Positive when bullish 46 | 47 | def next(self): 48 | """Define what will be done in a single step, including creating and closing trades""" 49 | for d in self.getdatanames(): # Looping through all symbols 50 | pos = self.getpositionbyname(d).size or 0 51 | if pos == 0: # Are we out of the market? 52 | # Consider the possibility of entrance 53 | # Notice the indexing; [0] always mens the present bar, and [-1] the bar immediately preceding 54 | # Thus, the condition below translates to: "If today the regime is bullish (greater than 55 | # 0) and yesterday the regime was not bullish" 56 | if self.regime[d][0] > 0 and self.regime[d][-1] <= 0: # A buy signal 57 | self.buy(data=self.getdatabyname(d)) 58 | 59 | else: # We have an open position 60 | if self.regime[d][0] <= 0 and self.regime[d][-1] > 0: # A sell signal 61 | self.sell(data=self.getdatabyname(d)) 62 | 63 | 64 | class PropSizer(bt.Sizer): 65 | """A position sizer that will buy as many stocks as necessary for a certain proportion of the portfolio 66 | to be committed to the position, while allowing stocks to be bought in batches (say, 100)""" 67 | params = {"prop": 0.1, "batch": 100} 68 | 69 | def _getsizing(self, comminfo, cash, data, isbuy): 70 | """Returns the proper sizing""" 71 | 72 | if isbuy: # Buying 73 | target = self.broker.getvalue() * self.params.prop # Ideal total value of the position 74 | price = data.close[0] 75 | shares_ideal = target / price # How many shares are needed to get target 76 | batches = int(shares_ideal / self.params.batch) # How many batches is this trade? 77 | shares = batches * self.params.batch # The actual number of shares bought 78 | 79 | if shares * price > cash: 80 | return 0 # Not enough money for this trade 81 | else: 82 | return shares 83 | 84 | else: # Selling 85 | return self.broker.getposition(data).size # Clear the position 86 | 87 | 88 | class AcctValue(bt.Observer): 89 | alias = ('Value',) 90 | lines = ('value',) 91 | 92 | plotinfo = {"plot": True, "subplot": True} 93 | 94 | def next(self): 95 | self.lines.value[0] = self._owner.broker.getvalue() # Get today's account value (cash + stocks) 96 | 97 | 98 | class AcctStats(bt.Analyzer): 99 | """A simple analyzer that gets the gain in the value of the account; should be self-explanatory""" 100 | 101 | def __init__(self): 102 | self.start_val = self.strategy.broker.get_value() 103 | self.end_val = None 104 | 105 | def stop(self): 106 | self.end_val = self.strategy.broker.get_value() 107 | 108 | def get_analysis(self): 109 | return {"start": self.start_val, "end": self.end_val, 110 | "growth": self.end_val - self.start_val, "return": self.end_val / self.start_val} 111 | 112 | 113 | def createWFAReport(simParams, simulations): 114 | # Create simulation report format 115 | reportColumns = ['grossProfit', 'grossAverageProfit', 'maxProfit', 116 | 'grossLoss', 'grossAverageLoss', 'maxLoss', 117 | 'netProfit', 'averageNetProfit', 'NAR', 118 | 'recoveryFactor', 'MDDLength', 'MDD', 119 | 'wonTrade', 'lossTrade', 'tradingTime', 120 | 'averageTradeTime', 'TradeNumber', 'maxValue', 121 | 'minValue', 'totalCommission'] 122 | simulationReport = pd.DataFrame(columns=reportColumns) 123 | 124 | # Loop Simulations to create summary 125 | for simulation in simulations: 126 | '''some Calculation is done here''' 127 | return simReport 128 | 129 | 130 | def WFASplit(self, trainBy='12m', testBy='3m', loopBy='m', overlap=True): 131 | startDate = self.index[0] 132 | endDate = self.index[-1] 133 | if trainBy[-1] is 'm': 134 | trainTime = relativedelta(months=int(trainBy[:-1])) 135 | else: 136 | raise ValueError 137 | if testBy[-1] is 'm': 138 | testTime = relativedelta(months=int(testBy[:-1])) 139 | else: 140 | raise ValueError 141 | assert ((relativedelta(endDate, startDate) - trainTime).days) > 0 142 | 143 | if loopBy is 'm': 144 | test_starts = zip(rrule(MONTHLY, dtstart=startDate, until=endDate - trainTime, interval=int(testBy[:-1]))) 145 | else: 146 | raise ValueError 147 | 148 | for i in test_starts: 149 | startD = i[0] 150 | endD = i[0] + trainTime 151 | yield (self[(self.index >= startD) & (self.index < endD)], 152 | self[(self.index >= endD) & (self.index < endD + testTime)]) 153 | return None 154 | 155 | 156 | def runTrain(trainTestGenerator, _ind, stockName): 157 | WFATrainResult = [] 158 | for train, test in trainTestGenerator: 159 | logger.debug( 160 | '{} Training Data:{} to {}'.format(stockName, pd.DatetimeIndex.strftime(train.head(1).index, '%Y-%m-%d'), 161 | pd.DatetimeIndex.strftime(train.tail(1).index, '%Y-%m-%d'))) 162 | # Generate Indicator ResultSet 163 | trainer = bt.Cerebro(cheat_on_open=True, stdstats=False, optreturn=False) 164 | trainer.broker.set_cash(10000) 165 | # Add Commission 166 | IB = params['commission'](commission=0.0) 167 | trainer.broker.addcommissioninfo(IB) 168 | # Below Analyzer are used to calculate the Recovery Ratio 169 | trainer.addanalyzer(btanalyzers.TradeAnalyzer, _name='TradeAn') 170 | trainer.addanalyzer(recoveryAnalyzer, timeframe=params['analysisTimeframe'], _name='recoveryFac') 171 | trainer.addanalyzer(WFAAn, _name='WFAAna') 172 | trainer.addanalyzer(btanalyzers.TimeReturn, timeframe=bt.TimeFrame.Months, _name='TR') 173 | # SetBroker 174 | trainer.broker.set_checksubmit(False) 175 | # Copy for tester 176 | tester = deepcopy(trainer) 177 | # Optimize Strategy 178 | trainingFile = '{}/WFA' 179 | trainer.optstrategy(trainingIdea, 180 | inOrOut=(params['inOrOut'],), 181 | selfLog=(params['selfLog'],), 182 | indName=(row.indicator,), 183 | indFormula=(_ind['formula'],), 184 | entryExitPara=(_ind['entryExitParameters'],), 185 | indOutName=(_ind['indValue'],), 186 | nonOptParams=(None,), 187 | resultLocation=(params['resultLocation'],), 188 | timeString=(params['timeString'],), 189 | market=(row.market,), 190 | **optt) 191 | trainData = bt.feeds.PandasData(dataname=train) 192 | # Add a subset of data. 193 | trainer.adddata(trainData) 194 | optTable = trainer.run() 195 | final_results_list = [] 196 | for run in optTable: 197 | for x in run: 198 | x.params['res'] = x.analyzers.WFAAna.get_analysis() 199 | final_results_list.append(x.params) 200 | 201 | _bestWFA = \ 202 | pd.DataFrame.from_dict(final_results_list, orient='columns').sort_values('res', ascending=False).iloc[ 203 | 0].to_dict() 204 | bestTrainParams = {key: _bestWFA[key] for key in _bestWFA if 205 | key not in ['market', 'inOrOut', 'resultLocation', 'selfLog', 'timeString', 'res']} 206 | bestTrainParams = pd.DataFrame(bestTrainParams, index=[0]) 207 | bestTrainParams['trainStart'] = train.iloc[0].name 208 | bestTrainParams['trainEnd'] = train.iloc[-1].name 209 | bestTrainParams['testStart'] = test.iloc[0].name 210 | bestTrainParams['testEnd'] = test.iloc[-1].name 211 | WFATrainResult.append(bestTrainParams) 212 | WFATrainResult = pd.concat(WFATrainResult) 213 | return WFATrainResult 214 | 215 | 216 | def runTest(params, WFATrainResult, _ind, datafeed, stockName): 217 | # Generate Indicator ResultSet 218 | tester = bt.Cerebro(cheat_on_open=True) 219 | tester.broker.set_cash(10000) 220 | # Add Commission 221 | IB = params['commission'](commission=0.0) 222 | tester.broker.addcommissioninfo(IB) 223 | # SetBroker 224 | tester.broker.set_checksubmit(False) 225 | logger.debug('{} Start Testing'.format(stockName)) 226 | OneSimHandler = logging.FileHandler( 227 | filename='{}/simulation/{}_{}_test.log'.format(params['resultLocation'], str(stockName), str(row.indicator))) 228 | OneSimHandler.setLevel(logging.DEBUG) 229 | OneSimHandler.setFormatter(logging.Formatter("%(asctime)s:%(relativeCreated)d - %(message)s")) 230 | oneLogger.addHandler(OneSimHandler) 231 | tester.addstrategy(trainingIdea, 232 | inOrOut=params['inOrOut'], 233 | selfLog=params['selfLog'], 234 | indName=row.indicator, 235 | indFormula=_ind['formula'], 236 | entryExitPara=_ind['entryExitParameters'], 237 | indOutName=_ind['indValue'], 238 | nonOptParams=None, 239 | resultLocation=params['resultLocation'], 240 | timeString=params['timeString'], 241 | market=market, 242 | WFATestParams=WFATrainResult) 243 | data = bt.feeds.PandasData(dataname=datafeed) 244 | tester.adddata(data, name=stockName) 245 | # Add analyzers for Tester 246 | tester.addanalyzer(btanalyzers.DrawDown, _name='MDD') 247 | tester.addanalyzer(btanalyzers.TradeAnalyzer, _name='TradeAn') 248 | tester.addanalyzer(btanalyzers.SQN, _name='SQN') 249 | tester.addanalyzer(recoveryAnalyzer, timeframe=params['analysisTimeframe'], _name='recoveryFac') 250 | tester.addanalyzer(ITDDAnalyzer, _name='ITDD') 251 | tester.addanalyzer(simpleAn, _name='simpleAna') 252 | tester.addanalyzer(btanalyzers.TimeReturn, timeframe=bt.TimeFrame.Months, _name='TR') 253 | # Run and Return Cerebro 254 | cere = tester.run()[0] 255 | 256 | _report = cere.analyzers.simpleAna.writeAnalysis(bnhReturn) 257 | oneLogger.removeHandler(OneSimHandler) 258 | if params['plotGraph']: 259 | plotSimGraph(tester, params, stockName, row.indicator) 260 | return _report 261 | 262 | 263 | if __name__ == "__main__": 264 | session = 'WinsWFAProd' 265 | stockList, indicatorDict, params = jsonConfigMap(session) 266 | params['timeString'] = '0621_113833' 267 | params['topResult'] = pd.read_csv('{}{}/consoReport.csv'.format(params['resultLocation'], params['timeString']), 268 | index_col=0) 269 | params['resultLocation'] += params['timeString'] + '/WFA' 270 | simulations = [] 271 | 272 | try: 273 | # Create Folder 274 | shutil.rmtree(params['resultLocation']) 275 | except FileNotFoundError: 276 | pass 277 | except PermissionError: 278 | pass 279 | os.makedirs(params['resultLocation'], exist_ok=True) 280 | for element in ['order', 'trade', 'mr', 'ohlc', 'simulation', 'bestTrain', 'graph']: 281 | os.makedirs(params['resultLocation'] + '/' + element, exist_ok=True) 282 | # Create master Log 283 | handler = logging.FileHandler(filename='{}/Master.log'.format(params['resultLocation'])) 284 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s - %(levelname)s {}- %(message)s')) 285 | logger = logging.getLogger() 286 | logger.setLevel(logging.DEBUG) 287 | logger.addHandler(handler) 288 | # Create OptReport Log 289 | reportHandler = logging.FileHandler(filename='{}/optReport.csv'.format(params['resultLocation'])) 290 | reportHandler.setFormatter(logging.Formatter('%(message)s')) 291 | reportHandler.setLevel(logging.INFO) 292 | reportLogger = logging.getLogger('report') 293 | reportLogger.addHandler(reportHandler) 294 | simResultColumns = ['stockName', 'market', 'indicator', 295 | 'grossProfit', 'grossAverageProfit', 'maxProfit', 296 | 'grossLoss', 'grossAverageLoss', 'maxLoss', 297 | 'netProfit', 'averageNetProfit', 'NAR', 'profitFactor', 298 | 'recoveryFactor', 'MDDLength', 'MDD', 299 | 'selfMDD', 'winRate', 'tradingTimeRatio', 300 | 'averageTradingBar', 'tradeNumber', 'maxValue', 301 | 'minValue', 'initialv', 'totalCommission', 302 | 'barNumber', 'expectancy100', 'bnhReturn', 'bnhRatio'] 303 | reportLogger.info(str(simResultColumns).strip("[]").replace("'", "").replace(" ", "")) 304 | 305 | # Create Simulation Log 306 | oneLogger = logging.getLogger('oneLogger') 307 | oneLogger.propagate = False 308 | postHandler = logging.StreamHandler() 309 | postHandler.setLevel(logging.INFO) 310 | if params['selfLog']: 311 | oneLogger.setLevel(logging.DEBUG) 312 | else: 313 | oneLogger.setLevel(logging.INFO) 314 | oneLogger.addHandler(postHandler) 315 | # Record Start Time 316 | startTime = time.time() 317 | for row in params['topResult'].itertuples(): 318 | simParams = pd.DataFrame(columns=['startTime', 'endTime', 'Parameter']) 319 | indicator = indicatorDict[row.indicator] 320 | stockName = row.stockName 321 | market = row.market 322 | try: 323 | optt = eval(indicator['optParam']) 324 | except: 325 | logger.info('{}: Indicator does not have WFA parameters and skipped'.format(row.indicator)) 326 | continue 327 | datafeed = feeder(stockName, market, params) 328 | bnhReturn = round(datafeed.iloc[-1]['close'] - datafeed.iloc[0]['open'], 2) 329 | # Extract Feeder from Data to save time for multi-simulation 330 | print('Start WFA for {}-{} from {} to {}'.format(stockName, row.indicator, datafeed.iloc[0].name, 331 | datafeed.iloc[-1].name)) 332 | trainTestGenerator = WFASplit(datafeed, trainBy='8m', testBy='8m', loopBy='m') 333 | _ind = indicatorDict[row.indicator] 334 | # Training 335 | WFATrainResult = runTrain(trainTestGenerator, _ind, stockName) 336 | WFATrainResult = pd.DataFrame.from_records(WFATrainResult) 337 | WFATrainResult.to_csv('{}/bestTrain/{}_{}_train.csv'.format(params['resultLocation'], stockName, row.indicator), 338 | index=False) 339 | # TESTING 340 | _report = runTest(params, WFATrainResult, _ind, datafeed, stockName) 341 | # Consolidate T 342 | if _report[0] is not None: 343 | reportLogger.info(str(_report).strip("[]").strip(" ").replace(" ", "").replace("'", "")) 344 | simulations.append(_report) 345 | 346 | # After simulations 347 | simulations = pd.DataFrame(simulations) 348 | reportColumns = ['stockName', 'market', 'indicator', 349 | 'grossProfit', 'grossAverageProfit', 'maxProfit', 350 | 'grossLoss', 'grossAverageLoss', 'maxLoss', 351 | 'netProfit', 'averageNetProfit', 'NAR', 352 | 'profitFactor', 'recoveryFactor', 'MDDLength', 353 | 'selfMDD', 'winRate', 'tradingTimeRatio', 354 | 'averageTradingBar', 'tradeNumber', 'maxValue', 355 | 'minValue', 'initialv', 'totalCommission', 356 | 'barNumber', 'expectancy100', 'bnhReturn', 357 | 'bnhRatio'] 358 | simulations.columns = reportColumns 359 | consoResult = cr.scoring(simulations) 360 | consoResult = consoResult.sort_values(['res'], ascending=False) 361 | consoResult.sort_values('res').to_csv('{}/optReport.csv'.format(params['resultLocation']), index=False) 362 | 363 | timeRequired = time.time() - startTime 364 | print('timeRequired={:.2f}s'.format(timeRequired)) 365 | 366 | start = dt.datetime(2010, 1, 1) 367 | end = dt.datetime(2016, 10, 31) 368 | # Different stocks from past posts because of different data source (no plot for NTDOY) 369 | symbols = ["AAPL", "GOOG", "MSFT", "AMZN", "YHOO", "SNY", "VZ", "IBM", "HPQ", "QCOM", "NVDA"] 370 | datafeeds = {s: web.DataReader(s, "google", start, end) for s in symbols} 371 | for df in datafeeds.values(): 372 | df["OpenInterest"] = 0 # PandasData reader expects an OpenInterest column; 373 | # not provided by Google and we don't use it so set to 0 374 | 375 | cerebro = bt.Cerebro(stdstats=False) 376 | 377 | plot_symbols = ["AAPL", "GOOG", "NVDA"] 378 | is_first = True 379 | # plot_symbols = [] 380 | for s, df in datafeeds.items(): 381 | data = bt.feeds.PandasData(dataname=df, name=s) 382 | if s in plot_symbols: 383 | if is_first: 384 | data_main_plot = data 385 | is_first = False 386 | else: 387 | data.plotinfo.plotmaster = data_main_plot 388 | else: 389 | data.plotinfo.plot = False 390 | cerebro.adddata(data) # Give the data to cerebro 391 | 392 | cerebro.broker.setcash(1000000) 393 | cerebro.broker.setcommission(0.02) 394 | cerebro.addstrategy(SMAC) 395 | cerebro.addobserver(AcctValue) 396 | cerebro.addobservermulti(bt.observers.BuySell) # Plots up/down arrows 397 | cerebro.addsizer(PropSizer) 398 | cerebro.addanalyzer(AcctStats) 399 | 400 | cerebro.run() 401 | -------------------------------------------------------------------------------- /WalkForward/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Paul Knysh 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. 22 | -------------------------------------------------------------------------------- /WalkForward/README.md: -------------------------------------------------------------------------------- 1 | # blackbox: A Python module for parallel optimization of expensive black-box functions 2 | 3 | ## What is this? 4 | 5 | Let's say you need to find optimal parameters of some computationally intensive system (for example, time-consuming simulation). If you can construct a simple Python function, that takes a set of trial parameters, performs evaluation, and returns some scalar measure of how good chosen parameters are, then the problem becomes a mathematical optimization. However, a corresponding function is expensive (one evaluation can take hours) and is a black-box (has input-output nature). 6 | 7 | **blackbox** is a minimalistic and easy-to-use Python module that efficiently searches for a global optimum (minimum) of an expensive black-box function. User needs to provide a function, a search region (ranges of values for each input parameter) and a number of function evaluations available. A code scales well on clusters and multicore CPUs by performing all expensive function evaluations in parallel. 8 | 9 | A mathematical method behind the code is described in this arXiv note: https://arxiv.org/pdf/1605.00998.pdf 10 | 11 | Feel free to cite this note if you are using method/code in your research. 12 | 13 | ## How do I represent my objective function? 14 | 15 | It simply needs to be wrapped into a Python function. If an external application is used, it can be accessed using system call: 16 | ```python 17 | def fun(par): 18 | 19 | # running external application for given set of parameters 20 | os.system(...) 21 | 22 | # calculating output 23 | ... 24 | 25 | return output 26 | ``` 27 | `par` is a vector of input parameters (a Python list), `output` is a scalar measure to be minimized. 28 | 29 | ## How do I run the procedure? 30 | 31 | No installation is needed. Just place `blackbox.py` into your working directory. Main file should look like that: 32 | ```python 33 | import blackbox as bb 34 | 35 | 36 | def fun(par): 37 | return par[0]**2 + par[1]**2 # dummy 2D example 38 | 39 | 40 | def main(): 41 | bb.search(f=fun, # given function 42 | box=[[-10., 10.], [-10., 10.]], # range of values for each parameter (2D case) 43 | n=20, # number of function calls on initial stage (global search) 44 | m=20, # number of function calls on subsequent stage (local search) 45 | batch=4, # number of calls that will be evaluated in parallel 46 | resfile='output.csv') # text file where results will be saved 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | ``` 52 | **Important:** 53 | * All function calls are divided into batches that are evaluated in parallel. Total number of these parallel cycles is `(n+m)/batch`. 54 | * `n` must be greater than the number of parameters, `m` must be greater than 1, `batch` should not exceed the number of CPU cores available. 55 | * An optional parameter `executor=...` should be specified when calling `bb.search()` in case when code is used on a cluster with some custom parallel engine (ipyparallel, dask.distributed, pathos etc). `executor` should be an object that has a `map` method. 56 | 57 | ## How about results? 58 | 59 | Iterations are sorted by function value (best solution is in the top) and saved in a text file with the following structure: 60 | 61 | Parameter #1 | Parameter #2 | ... | Parameter #n | Function value 62 | --- | --- | --- | --- | --- 63 | +1.6355e+01 | -4.7364e+03 | ... | +6.4012e+00 | +1.1937e-04 64 | ... | ... | ... | ... | ... 65 | 66 | ## Author 67 | 68 | Paul Knysh (paul.knysh@gmail.com) 69 | 70 | I receive tons of useful feedback that helps me to improve the code. Feel free to email me if you have any questions or comments. 71 | 72 |

73 | 74 |

75 | -------------------------------------------------------------------------------- /WalkForward/blackbox.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import multiprocessing as mp 3 | import numpy as np 4 | import scipy.optimize as op 5 | 6 | 7 | def get_default_executor(): 8 | """ 9 | Provide a default executor (a context manager 10 | returning an object with a map method). 11 | 12 | This is the multiprocessing Pool object () for python3. 13 | 14 | The multiprocessing Pool in python2 does not have an __enter__ 15 | and __exit__ method, this function provides a backport of the python3 Pool 16 | context manager. 17 | 18 | Returns 19 | ------- 20 | Pool : executor-like object 21 | An object with context manager (__enter__, __exit__) and map method. 22 | """ 23 | if (sys.version_info > (3, 0)): 24 | Pool = mp.Pool 25 | return Pool 26 | else: 27 | from contextlib import contextmanager 28 | from functools import wraps 29 | 30 | @wraps(mp.Pool) 31 | @contextmanager 32 | def Pool(*args, **kwargs): 33 | pool = mp.Pool(*args, **kwargs) 34 | yield pool 35 | pool.terminate() 36 | return Pool 37 | 38 | 39 | def search(f, box, n, m, batch, resfile, 40 | rho0=0.5, p=1.0, nrand=10000, nrand_frac=0.05, 41 | executor=get_default_executor()): 42 | """ 43 | Minimize given expensive black-box function and save results into text file. 44 | 45 | Parameters 46 | ---------- 47 | f : callable 48 | The objective function to be minimized. 49 | box : list of lists 50 | List of ranges for each parameter. 51 | n : int 52 | Number of initial function calls. 53 | m : int 54 | Number of subsequent function calls. 55 | batch : int 56 | Number of function calls evaluated simultaneously (in parallel). 57 | resfile : str 58 | Text file to save results. 59 | rho0 : float, optional 60 | Initial "balls density". 61 | p : float, optional 62 | Rate of "balls density" decay (p=1 - linear, p>1 - faster, 0 1: 112 | fit_noscale = rbf(points, np.identity(d)) 113 | population = np.zeros((nrand, d+1)) 114 | population[:, 0:-1] = np.random.rand(nrand, d) 115 | population[:, -1] = list(map(fit_noscale, population[:, 0:-1])) 116 | 117 | cloud = population[population[:, -1].argsort()][0:int(nrand*nrand_frac), 0:-1] 118 | eigval, eigvec = np.linalg.eig(np.cov(np.transpose(cloud))) 119 | T = [eigvec[:, j]/np.sqrt(eigval[j]) for j in range(d)] 120 | T = T/np.linalg.norm(T) 121 | 122 | # sampling next batch of points 123 | fit = rbf(points, T) 124 | points = np.append(points, np.zeros((batch, d+1)), axis=0) 125 | 126 | for j in range(batch): 127 | r = ((rho0*((m-1.-(i*batch+j))/(m-1.))**p)/(v1*(n+i*batch+j)))**(1./d) 128 | cons = [{'type': 'ineq', 'fun': lambda x, localk=k: np.linalg.norm(np.subtract(x, points[localk, 0:-1])) - r} 129 | for k in range(n+i*batch+j)] 130 | while True: 131 | minfit = op.minimize(fit, np.random.rand(d), method='SLSQP', bounds=[[0., 1.]]*d, constraints=cons) 132 | if np.isnan(minfit.x)[0] == False: 133 | break 134 | points[n+i*batch+j, 0:-1] = np.copy(minfit.x) 135 | 136 | with executor() as e: 137 | points[n+batch*i:n+batch*(i+1), -1] = list(e.map(f, list(map(cubetobox, points[n+batch*i:n+batch*(i+1), 0:-1]))))/fmax 138 | 139 | # saving results into text file 140 | points[:, 0:-1] = list(map(cubetobox, points[:, 0:-1])) 141 | points[:, -1] = points[:, -1]*fmax 142 | points = points[points[:, -1].argsort()] 143 | 144 | labels = [' par_'+str(i+1)+(7-len(str(i+1)))*' '+',' for i in range(d)]+[' f_value '] 145 | print(points) 146 | np.savetxt(resfile, points, delimiter=',', fmt=' %+1.4e', header=''.join(labels), comments='') 147 | return points 148 | 149 | def latin(n, d): 150 | """ 151 | Build latin hypercube. 152 | 153 | Parameters 154 | ---------- 155 | n : int 156 | Number of points. 157 | d : int 158 | Size of space. 159 | 160 | Returns 161 | ------- 162 | lh : ndarray 163 | Array of points uniformly placed in d-dimensional unit cube. 164 | """ 165 | # spread function 166 | def spread(points): 167 | return sum(1./np.linalg.norm(np.subtract(points[i], points[j])) for i in range(n) for j in range(n) if i > j) 168 | 169 | # starting with diagonal shape 170 | lh = [[i/(n-1.)]*d for i in range(n)] 171 | 172 | # minimizing spread function by shuffling 173 | minspread = spread(lh) 174 | 175 | for i in range(1000): 176 | point1 = np.random.randint(n) 177 | point2 = np.random.randint(n) 178 | dim = np.random.randint(d) 179 | 180 | newlh = np.copy(lh) 181 | newlh[point1, dim], newlh[point2, dim] = newlh[point2, dim], newlh[point1, dim] 182 | newspread = spread(newlh) 183 | 184 | if newspread < minspread: 185 | lh = np.copy(newlh) 186 | minspread = newspread 187 | 188 | return lh 189 | 190 | 191 | def rbf(points, T): 192 | """ 193 | Build RBF-fit for given points (see Holmstrom, 2008 for details) using scaling matrix. 194 | 195 | Parameters 196 | ---------- 197 | points : ndarray 198 | Array of multi-d points with corresponding values [[x1, x2, .., xd, val], ...]. 199 | T : ndarray 200 | Scaling matrix. 201 | 202 | Returns 203 | ------- 204 | fit : callable 205 | Function that returns the value of the RBF-fit at a given point. 206 | """ 207 | n = len(points) 208 | d = len(points[0])-1 209 | 210 | def phi(r): 211 | return r*r*r 212 | 213 | Phi = [[phi(np.linalg.norm(np.dot(T, np.subtract(points[i, 0:-1], points[j, 0:-1])))) for j in range(n)] for i in range(n)] 214 | 215 | P = np.ones((n, d+1)) 216 | P[:, 0:-1] = points[:, 0:-1] 217 | 218 | F = points[:, -1] 219 | 220 | M = np.zeros((n+d+1, n+d+1)) 221 | M[0:n, 0:n] = Phi 222 | M[0:n, n:n+d+1] = P 223 | M[n:n+d+1, 0:n] = np.transpose(P) 224 | 225 | v = np.zeros(n+d+1) 226 | v[0:n] = F 227 | 228 | sol = np.linalg.solve(M, v) 229 | lam, b, a = sol[0:n], sol[n:n+d], sol[n+d] 230 | 231 | def fit(x): 232 | return sum(lam[i]*phi(np.linalg.norm(np.dot(T, np.subtract(x, points[i, 0:-1])))) for i in range(n)) + np.dot(b, x) + a 233 | 234 | return fit 235 | -------------------------------------------------------------------------------- /WalkForward/output.csv: -------------------------------------------------------------------------------- 1 | par_1 , par_2 , par_3 , f_value 2 | +4.3113e+02, +1.5302e+09, +1.5475e+09, -4.0472e+01 3 | +4.6000e+02, +1.5302e+09, +1.5475e+09, -2.6072e+01 4 | +4.6107e+02, +1.5302e+09, +1.5475e+09, -2.1705e+01 5 | +4.6107e+02, +1.5302e+09, +1.5475e+09, -2.1705e+01 6 | +3.6933e+02, +1.5302e+09, +1.5475e+09, -1.2487e+01 7 | +3.7000e+02, +1.5302e+09, +1.5475e+09, -1.0993e+01 8 | +1.9000e+02, +1.5302e+09, +1.5475e+09, -1.0859e+01 9 | +1.8775e+02, +1.5302e+09, +1.5475e+09, -9.7837e+00 10 | +9.1000e+02, +1.5302e+09, +1.5475e+09, -6.1101e+00 11 | +9.2178e+02, +1.5302e+09, +1.5475e+09, -1.6855e+00 12 | +5.5000e+02, +1.5302e+09, +1.5475e+09, -4.7277e-01 13 | +1.0000e+03, +1.5302e+09, +1.5475e+09, -0.0000e+00 14 | +1.0000e+03, +1.5302e+09, +1.5475e+09, -0.0000e+00 15 | +1.0000e+03, +1.5302e+09, +1.5475e+09, -0.0000e+00 16 | +1.0000e+03, +1.5302e+09, +1.5475e+09, -0.0000e+00 17 | +3.4427e+02, +1.5302e+09, +1.5475e+09, +1.8847e+00 18 | +2.8000e+02, +1.5302e+09, +1.5475e+09, +3.8054e+00 19 | +6.4000e+02, +1.5302e+09, +1.5475e+09, +4.0847e+00 20 | +1.0000e+02, +1.5302e+09, +1.5475e+09, +7.3404e+00 21 | +8.6458e+02, +1.5302e+09, +1.5475e+09, +2.5863e+01 22 | +7.3000e+02, +1.5302e+09, +1.5475e+09, +2.6446e+01 23 | +8.2000e+02, +1.5302e+09, +1.5475e+09, +3.3042e+01 24 | +1.0000e+01, +1.5302e+09, +1.5475e+09, +5.0666e+01 25 | +1.0000e+01, +1.5302e+09, +1.5475e+09, +5.0666e+01 26 | -------------------------------------------------------------------------------- /ccxtbt/__init__.py: -------------------------------------------------------------------------------- 1 | from .ccxtbroker import * 2 | from .ccxtfeed import * 3 | from .ccxtstore import * 4 | -------------------------------------------------------------------------------- /ccxtbt/ccxtbroker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2015, 2016, 2017 Daniel Rodriguez 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | from __future__ import (absolute_import, division, print_function, 22 | unicode_literals) 23 | 24 | import collections 25 | import json 26 | 27 | from backtrader import BrokerBase, OrderBase, Order 28 | from backtrader.position import Position 29 | from backtrader.utils.py3 import queue, with_metaclass 30 | 31 | from .ccxtstore import CCXTStore 32 | 33 | 34 | class CCXTOrder(OrderBase): 35 | def __init__(self, owner, data, ccxt_order): 36 | self.owner = owner 37 | self.data = data 38 | self.ccxt_order = ccxt_order 39 | self.executed_fills = [] 40 | self.ordtype = self.Buy if ccxt_order['side'] == 'buy' else self.Sell 41 | self.size = float(ccxt_order['amount']) 42 | 43 | super(CCXTOrder, self).__init__() 44 | 45 | 46 | class MetaCCXTBroker(BrokerBase.__class__): 47 | def __init__(cls, name, bases, dct): 48 | '''Class has already been created ... register''' 49 | # Initialize the class 50 | super(MetaCCXTBroker, cls).__init__(name, bases, dct) 51 | CCXTStore.BrokerCls = cls 52 | 53 | 54 | class CCXTBroker(with_metaclass(MetaCCXTBroker, BrokerBase)): 55 | '''Broker implementation for CCXT cryptocurrency trading library. 56 | This class maps the orders/positions from CCXT to the 57 | internal API of ``backtrader``. 58 | 59 | Broker mapping added as I noticed that there differences between the expected 60 | order_types and retuned status's from canceling an order 61 | 62 | Added a new mappings parameter to the script with defaults. 63 | 64 | Added a get_balance function. Manually check the account balance and update brokers 65 | self.cash and self.value. This helps alleviate rate limit issues. 66 | 67 | Added a new get_wallet_balance method. This will allow manual checking of the any coins 68 | The method will allow setting parameters. Useful for dealing with multiple assets 69 | 70 | Modified getcash() and getvalue(): 71 | Backtrader will call getcash and getvalue before and after next, slowing things down 72 | with rest calls. As such, th 73 | 74 | The broker mapping should contain a new dict for order_types and mappings like below: 75 | 76 | broker_mapping = { 77 | 'order_types': { 78 | bt.Order.Market: 'market', 79 | bt.Order.Limit: 'limit', 80 | bt.Order.Stop: 'stop-loss', #stop-loss for kraken, stop for bitmex 81 | bt.Order.StopLimit: 'stop limit' 82 | }, 83 | 'mappings':{ 84 | 'closed_order':{ 85 | 'key': 'status', 86 | 'value':'closed' 87 | }, 88 | 'canceled_order':{ 89 | 'key': 'result', 90 | 'value':1} 91 | } 92 | } 93 | 94 | Added new private_end_point method to allow using any private non-unified end point 95 | 96 | ''' 97 | 98 | order_types = {Order.Market: 'market', 99 | Order.Limit: 'limit', 100 | Order.Stop: 'stop', # stop-loss for kraken, stop for bitmex 101 | Order.StopLimit: 'stop limit'} 102 | 103 | mappings = { 104 | 'closed_order': { 105 | 'key': 'status', 106 | 'value': 'closed' 107 | }, 108 | 'canceled_order': { 109 | 'key': 'status', 110 | 'value': 'canceled'} 111 | } 112 | 113 | def __init__(self, broker_mapping=None, debug=False, **kwargs): 114 | super(CCXTBroker, self).__init__() 115 | 116 | if broker_mapping is not None: 117 | try: 118 | self.order_types = broker_mapping['order_types'] 119 | except KeyError: # Might not want to change the order types 120 | pass 121 | try: 122 | self.mappings = broker_mapping['mappings'] 123 | except KeyError: # might not want to change the mappings 124 | pass 125 | 126 | self.store = CCXTStore(**kwargs) 127 | 128 | self.currency = self.store.currency 129 | 130 | self.positions = collections.defaultdict(Position) 131 | 132 | self.debug = debug 133 | self.indent = 4 # For pretty printing dictionaries 134 | 135 | self.notifs = queue.Queue() # holds orders which are notified 136 | 137 | self.open_orders = list() 138 | 139 | self.startingcash = self.store._cash 140 | self.startingvalue = self.store._value 141 | 142 | self.use_order_params = True 143 | 144 | def get_balance(self): 145 | self.store.get_balance() 146 | self.cash = self.store._cash 147 | self.value = self.store._value 148 | return self.cash, self.value 149 | 150 | def get_wallet_balance(self, currency, params={}): 151 | balance = self.store.get_wallet_balance(currency, params=params) 152 | try: 153 | cash = balance['free'][currency] if balance['free'][currency] else 0 154 | except KeyError: # never funded or eg. all USD exchanged 155 | cash = 0 156 | try: 157 | value = balance['total'][currency] if balance['total'][currency] else 0 158 | except KeyError: # never funded or eg. all USD exchanged 159 | value = 0 160 | return cash, value 161 | 162 | def getcash(self): 163 | # Get cash seems to always be called before get value 164 | # Therefore it makes sense to add getbalance here. 165 | # return self.store.getcash(self.currency) 166 | self.cash = self.store._cash 167 | return self.cash 168 | 169 | def getvalue(self, datas=None): 170 | # return self.store.getvalue(self.currency) 171 | self.value = self.store._value 172 | return self.value 173 | 174 | def get_notification(self): 175 | try: 176 | return self.notifs.get(False) 177 | except queue.Empty: 178 | return None 179 | 180 | def notify(self, order): 181 | self.notifs.put(order) 182 | 183 | def getposition(self, data, clone=True): 184 | # return self.o.getposition(data._dataname, clone=clone) 185 | pos = self.positions[data._dataname] 186 | if clone: 187 | pos = pos.clone() 188 | return pos 189 | 190 | def next(self): 191 | if self.debug: 192 | print('Broker next() called') 193 | 194 | for o_order in list(self.open_orders): 195 | oID = o_order.ccxt_order['id'] 196 | 197 | # Print debug before fetching so we know which order is giving an 198 | # issue if it crashes 199 | if self.debug: 200 | print('Fetching Order ID: {}'.format(oID)) 201 | 202 | # Get the order 203 | ccxt_order = self.store.fetch_order(oID, o_order.data.p.dataname) 204 | 205 | # Check for new fills 206 | if 'trades' in ccxt_order and ccxt_order['trades'] is not None: 207 | for fill in ccxt_order['trades']: 208 | if fill not in o_order.executed_fills: 209 | o_order.execute(fill['datetime'], fill['amount'], fill['price'], 210 | 0, 0.0, 0.0, 211 | 0, 0.0, 0.0, 212 | 0.0, 0.0, 213 | 0, 0.0) 214 | o_order.executed_fills.append(fill['id']) 215 | 216 | if self.debug: 217 | print(json.dumps(ccxt_order, indent=self.indent)) 218 | 219 | # Check if the order is closed 220 | if ccxt_order[self.mappings['closed_order']['key']] == self.mappings['closed_order']['value']: 221 | pos = self.getposition(o_order.data, clone=False) 222 | pos.update(o_order.size, o_order.price) 223 | o_order.completed() 224 | self.notify(o_order) 225 | self.open_orders.remove(o_order) 226 | self.get_balance() 227 | 228 | # Manage case when an order is being Canceled from the Exchange 229 | # from https://github.com/juancols/bt-ccxt-store/ 230 | if ccxt_order[self.mappings['canceled_order']['key']] == self.mappings['canceled_order']['value']: 231 | self.open_orders.remove(o_order) 232 | o_order.cancel() 233 | self.notify(o_order) 234 | 235 | def _submit(self, owner, data, exectype, side, amount, price, params): 236 | if amount == 0 or price == 0: 237 | # do not allow failing orders 238 | return None 239 | order_type = self.order_types.get(exectype) if exectype else 'market' 240 | created = int(data.datetime.datetime(0).timestamp()*1000) 241 | # Extract CCXT specific params if passed to the order 242 | params = params['params'] if 'params' in params else params 243 | if not self.use_order_params: 244 | ret_ord = self.store.create_order(symbol=data.p.dataname, order_type=order_type, side=side, 245 | amount=amount, price=price, params={}) 246 | else: 247 | try: 248 | # all params are exchange specific: https://github.com/ccxt/ccxt/wiki/Manual#custom-order-params 249 | params['created'] = created # Add timestamp of order creation for backtesting 250 | ret_ord = self.store.create_order(symbol=data.p.dataname, order_type=order_type, side=side, 251 | amount=amount, price=price, params=params) 252 | except: 253 | # save some API calls after failure 254 | self.use_order_params = False 255 | return None 256 | 257 | _order = self.store.fetch_order(ret_ord['id'], data.p.dataname) 258 | 259 | order = CCXTOrder(owner, data, _order) 260 | order.price = ret_ord['price'] 261 | self.open_orders.append(order) 262 | 263 | self.notify(order) 264 | return order 265 | 266 | def buy(self, owner, data, size, price=None, plimit=None, 267 | exectype=None, valid=None, tradeid=0, oco=None, 268 | trailamount=None, trailpercent=None, 269 | **kwargs): 270 | del kwargs['parent'] 271 | del kwargs['transmit'] 272 | return self._submit(owner, data, exectype, 'buy', size, price, kwargs) 273 | 274 | def sell(self, owner, data, size, price=None, plimit=None, 275 | exectype=None, valid=None, tradeid=0, oco=None, 276 | trailamount=None, trailpercent=None, 277 | **kwargs): 278 | del kwargs['parent'] 279 | del kwargs['transmit'] 280 | return self._submit(owner, data, exectype, 'sell', size, price, kwargs) 281 | 282 | def cancel(self, order): 283 | 284 | oID = order.ccxt_order['id'] 285 | 286 | if self.debug: 287 | print('Broker cancel() called') 288 | print('Fetching Order ID: {}'.format(oID)) 289 | 290 | # check first if the order has already been filled otherwise an error 291 | # might be raised if we try to cancel an order that is not open. 292 | ccxt_order = self.store.fetch_order(oID, order.data.p.dataname) 293 | 294 | if self.debug: 295 | print(json.dumps(ccxt_order, indent=self.indent)) 296 | 297 | if ccxt_order[self.mappings['closed_order']['key']] == self.mappings['closed_order']['value']: 298 | return order 299 | 300 | ccxt_order = self.store.cancel_order(oID, order.data.p.dataname) 301 | 302 | if self.debug: 303 | print(json.dumps(ccxt_order, indent=self.indent)) 304 | print('Value Received: {}'.format(ccxt_order[self.mappings['canceled_order']['key']])) 305 | print('Value Expected: {}'.format(self.mappings['canceled_order']['value'])) 306 | 307 | if ccxt_order[self.mappings['canceled_order']['key']] == self.mappings['canceled_order']['value']: 308 | self.open_orders.remove(order) 309 | order.cancel() 310 | self.notify(order) 311 | return order 312 | 313 | def get_orders_open(self, safe=False): 314 | return self.store.fetch_open_orders() 315 | 316 | def private_end_point(self, type, endpoint, params, prefix = ""): 317 | ''' 318 | Open method to allow calls to be made to any private end point. 319 | See here: https://github.com/ccxt/ccxt/wiki/Manual#implicit-api-methods 320 | 321 | - type: String, 'Get', 'Post','Put' or 'Delete'. 322 | - endpoint = String containing the endpoint address eg. 'order/{id}/cancel' 323 | - Params: Dict: An implicit method takes a dictionary of parameters, sends 324 | the request to the exchange and returns an exchange-specific JSON 325 | result from the API as is, unparsed. 326 | - Optional prefix to be appended to the front of method_str should your 327 | exchange needs it. E.g. v2_private_xxx 328 | 329 | To get a list of all available methods with an exchange instance, 330 | including implicit methods and unified methods you can simply do the 331 | following: 332 | 333 | print(dir(ccxt.hitbtc())) 334 | ''' 335 | endpoint_str = endpoint.replace('/', '_') 336 | endpoint_str = endpoint_str.replace('{', '') 337 | endpoint_str = endpoint_str.replace('}', '') 338 | 339 | if prefix != "": 340 | method_str = prefix.lower() + '_private_' + type.lower() + endpoint_str.lower() 341 | else: 342 | method_str = 'private_' + type.lower() + endpoint_str.lower() 343 | 344 | return self.store.private_end_point(type=type, endpoint=method_str, params=params) 345 | -------------------------------------------------------------------------------- /ccxtbt/ccxtfeed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2015, 2016, 2017 Daniel Rodriguez 6 | # Copyright (C) 2017 Ed Bartosh 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | # 21 | ############################################################################### 22 | from __future__ import (absolute_import, division, print_function, 23 | unicode_literals) 24 | 25 | import time 26 | from collections import deque 27 | from datetime import datetime 28 | 29 | import backtrader as bt 30 | from backtrader.feed import DataBase 31 | from backtrader.utils.py3 import with_metaclass 32 | 33 | from .ccxtstore import CCXTStore 34 | 35 | 36 | class MetaCCXTFeed(DataBase.__class__): 37 | def __init__(cls, name, bases, dct): 38 | '''Class has already been created ... register''' 39 | # Initialize the class 40 | super(MetaCCXTFeed, cls).__init__(name, bases, dct) 41 | 42 | # Register with the store 43 | CCXTStore.DataCls = cls 44 | 45 | 46 | class CCXTFeed(with_metaclass(MetaCCXTFeed, DataBase)): 47 | """ 48 | CryptoCurrency eXchange Trading Library Data Feed. 49 | Params: 50 | - ``historical`` (default: ``False``) 51 | If set to ``True`` the data feed will stop after doing the first 52 | download of data. 53 | The standard data feed parameters ``fromdate`` and ``todate`` will be 54 | used as reference. 55 | - ``backfill_start`` (default: ``True``) 56 | Perform backfilling at the start. The maximum possible historical data 57 | will be fetched in a single request. 58 | 59 | Changes From Ed's pacakge 60 | 61 | - Added option to send some additional fetch_ohlcv_params. Some exchanges (e.g Bitmex) 62 | support sending some additional fetch parameters. 63 | - Added drop_newest option to avoid loading incomplete candles where exchanges 64 | do not support sending ohlcv params to prevent returning partial data 65 | 66 | """ 67 | 68 | params = ( 69 | ('historical', False), # only historical download 70 | ('backfill_start', False), # do backfilling at the start 71 | ('fetch_ohlcv_params', {}), 72 | ('ohlcv_limit', 20), 73 | ('drop_newest', False), 74 | ('debug', False) 75 | ) 76 | 77 | _store = CCXTStore 78 | 79 | # States for the Finite State Machine in _load 80 | _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(3) 81 | 82 | # def __init__(self, exchange, symbol, ohlcv_limit=None, config={}, retries=5): 83 | def __init__(self, **kwargs): 84 | # self.store = CCXTStore(exchange, config, retries) 85 | self.store = self._store(**kwargs) 86 | self._data = deque() # data queue for price data 87 | self._last_id = '' # last processed trade id for ohlcv 88 | self._last_ts = 0 # last processed timestamp for ohlcv 89 | 90 | def start(self, ): 91 | DataBase.start(self) 92 | 93 | if self.p.fromdate: 94 | self._state = self._ST_HISTORBACK 95 | self.put_notification(self.DELAYED) 96 | self._fetch_ohlcv(self.p.fromdate) 97 | 98 | else: 99 | self._state = self._ST_LIVE 100 | self.put_notification(self.LIVE) 101 | 102 | def _load(self): 103 | if self._state == self._ST_OVER: 104 | return False 105 | 106 | while True: 107 | if self._state == self._ST_LIVE: 108 | if self._timeframe == bt.TimeFrame.Ticks: 109 | return self._load_ticks() 110 | else: 111 | self._fetch_ohlcv() 112 | ret = self._load_ohlcv() 113 | if self.p.debug: 114 | print('---- LOAD ----') 115 | print('{} Load OHLCV Returning: {}'.format(datetime.utcnow(), ret)) 116 | return ret 117 | 118 | elif self._state == self._ST_HISTORBACK: 119 | ret = self._load_ohlcv() 120 | if ret: 121 | return ret 122 | else: 123 | # End of historical data 124 | if self.p.historical: # only historical 125 | self.put_notification(self.DISCONNECTED) 126 | self._state = self._ST_OVER 127 | return False # end of historical 128 | else: 129 | self._state = self._ST_LIVE 130 | self.put_notification(self.LIVE) 131 | continue 132 | 133 | def _fetch_ohlcv(self, fromdate=None): 134 | """Fetch OHLCV data into self._data queue""" 135 | granularity = self.store.get_granularity(self._timeframe, self._compression) 136 | 137 | if fromdate: 138 | since = int((fromdate - datetime(1970, 1, 1)).total_seconds() * 1000) 139 | else: 140 | if self._last_ts > 0: 141 | since = self._last_ts 142 | else: 143 | since = None 144 | 145 | limit = self.p.ohlcv_limit 146 | 147 | while True: 148 | dlen = len(self._data) 149 | 150 | if self.p.debug: 151 | # TESTING 152 | since_dt = datetime.utcfromtimestamp(since // 1000) if since is not None else 'NA' 153 | print('---- NEW REQUEST ----') 154 | print('{} - Requesting: Since TS {} Since date {} granularity {}, limit {}, params'.format( 155 | datetime.utcnow(), since, since_dt, granularity, limit, self.p.fetch_ohlcv_params)) 156 | data = sorted(self.store.fetch_ohlcv(self.p.dataname, timeframe=granularity, 157 | since=since, limit=limit, params=self.p.fetch_ohlcv_params)) 158 | try: 159 | for i, ohlcv in enumerate(data): 160 | tstamp, open_, high, low, close, volume = ohlcv 161 | print('{} - Data {}: {} - TS {} Time {}'.format(datetime.utcnow(), i, 162 | datetime.utcfromtimestamp(tstamp // 1000), 163 | tstamp, (time.time() * 1000))) 164 | # ------------------------------------------------------------------ 165 | except IndexError: 166 | print('Index Error: Data = {}'.format(data)) 167 | print('---- REQUEST END ----') 168 | else: 169 | 170 | data = sorted(self.store.fetch_ohlcv(self.p.dataname, timeframe=granularity, 171 | since=since, limit=limit, params=self.p.fetch_ohlcv_params)) 172 | 173 | # Check to see if dropping the latest candle will help with 174 | # exchanges which return partial data 175 | if self.p.drop_newest: 176 | del data[-1] 177 | 178 | for ohlcv in data: 179 | 180 | # for ohlcv in sorted(self.store.fetch_ohlcv(self.p.dataname, timeframe=granularity, 181 | # since=since, limit=limit, params=self.p.fetch_ohlcv_params)): 182 | 183 | if None in ohlcv: 184 | continue 185 | 186 | tstamp = ohlcv[0] 187 | 188 | # Prevent from loading incomplete data 189 | # if tstamp > (time.time() * 1000): 190 | # continue 191 | 192 | if tstamp > self._last_ts: 193 | if self.p.debug: 194 | print('Adding: {}'.format(ohlcv)) 195 | self._data.append(ohlcv) 196 | self._last_ts = tstamp 197 | 198 | if dlen == len(self._data): 199 | break 200 | 201 | def _load_ticks(self): 202 | if self._last_id is None: 203 | # first time get the latest trade only 204 | trades = [self.store.fetch_trades(self.p.dataname)[-1]] 205 | else: 206 | trades = self.store.fetch_trades(self.p.dataname) 207 | 208 | for trade in trades: 209 | trade_id = trade['id'] 210 | 211 | if trade_id > self._last_id: 212 | trade_time = datetime.strptime(trade['datetime'], '%Y-%m-%dT%H:%M:%S.%fZ') 213 | self._data.append((trade_time, float(trade['price']), float(trade['amount']))) 214 | self._last_id = trade_id 215 | 216 | try: 217 | trade = self._data.popleft() 218 | except IndexError: 219 | return None # no data in the queue 220 | 221 | trade_time, price, size = trade 222 | 223 | self.lines.datetime[0] = bt.date2num(trade_time) 224 | self.lines.open[0] = price 225 | self.lines.high[0] = price 226 | self.lines.low[0] = price 227 | self.lines.close[0] = price 228 | self.lines.volume[0] = size 229 | 230 | return True 231 | 232 | def _load_ohlcv(self): 233 | try: 234 | ohlcv = self._data.popleft() 235 | except IndexError: 236 | return None # no data in the queue 237 | 238 | tstamp, open_, high, low, close, volume = ohlcv 239 | 240 | dtime = datetime.utcfromtimestamp(tstamp // 1000) 241 | 242 | self.lines.datetime[0] = bt.date2num(dtime) 243 | self.lines.open[0] = open_ 244 | self.lines.high[0] = high 245 | self.lines.low[0] = low 246 | self.lines.close[0] = close 247 | self.lines.volume[0] = volume 248 | 249 | return True 250 | 251 | def haslivedata(self): 252 | return self._state == self._ST_LIVE and self._data 253 | 254 | def islive(self): 255 | return not self.p.historical 256 | -------------------------------------------------------------------------------- /ccxtbt/ccxtstore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2017 Ed Bartosh 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | from __future__ import (absolute_import, division, print_function, 22 | unicode_literals) 23 | 24 | import time 25 | from datetime import datetime 26 | from functools import wraps 27 | 28 | import backtrader as bt 29 | import ccxt 30 | from backtrader.metabase import MetaParams 31 | from backtrader.utils.py3 import with_metaclass 32 | from ccxt.base.errors import NetworkError, ExchangeError 33 | 34 | 35 | class MetaSingleton(MetaParams): 36 | '''Metaclass to make a metaclassed class a singleton''' 37 | 38 | def __init__(cls, name, bases, dct): 39 | super(MetaSingleton, cls).__init__(name, bases, dct) 40 | cls._singleton = None 41 | 42 | def __call__(cls, *args, **kwargs): 43 | if cls._singleton is None: 44 | cls._singleton = ( 45 | super(MetaSingleton, cls).__call__(*args, **kwargs)) 46 | 47 | return cls._singleton 48 | 49 | 50 | class CCXTStore(with_metaclass(MetaSingleton, object)): 51 | '''API provider for CCXT feed and broker classes. 52 | 53 | Added a new get_wallet_balance method. This will allow manual checking of the balance. 54 | The method will allow setting parameters. Useful for getting margin balances 55 | 56 | Added new private_end_point method to allow using any private non-unified end point 57 | 58 | ''' 59 | 60 | # Supported granularities 61 | _GRANULARITIES = { 62 | (bt.TimeFrame.Minutes, 1): '1m', 63 | (bt.TimeFrame.Minutes, 3): '3m', 64 | (bt.TimeFrame.Minutes, 5): '5m', 65 | (bt.TimeFrame.Minutes, 15): '15m', 66 | (bt.TimeFrame.Minutes, 30): '30m', 67 | (bt.TimeFrame.Minutes, 60): '1h', 68 | (bt.TimeFrame.Minutes, 90): '90m', 69 | (bt.TimeFrame.Minutes, 120): '2h', 70 | (bt.TimeFrame.Minutes, 180): '3h', 71 | (bt.TimeFrame.Minutes, 240): '4h', 72 | (bt.TimeFrame.Minutes, 360): '6h', 73 | (bt.TimeFrame.Minutes, 480): '8h', 74 | (bt.TimeFrame.Minutes, 720): '12h', 75 | (bt.TimeFrame.Days, 1): '1d', 76 | (bt.TimeFrame.Days, 3): '3d', 77 | (bt.TimeFrame.Weeks, 1): '1w', 78 | (bt.TimeFrame.Weeks, 2): '2w', 79 | (bt.TimeFrame.Months, 1): '1M', 80 | (bt.TimeFrame.Months, 3): '3M', 81 | (bt.TimeFrame.Months, 6): '6M', 82 | (bt.TimeFrame.Years, 1): '1y', 83 | } 84 | 85 | BrokerCls = None # broker class will auto register 86 | DataCls = None # data class will auto register 87 | 88 | @classmethod 89 | def getdata(cls, *args, **kwargs): 90 | '''Returns ``DataCls`` with args, kwargs''' 91 | return cls.DataCls(*args, **kwargs) 92 | 93 | @classmethod 94 | def getbroker(cls, *args, **kwargs): 95 | '''Returns broker with *args, **kwargs from registered ``BrokerCls``''' 96 | return cls.BrokerCls(*args, **kwargs) 97 | 98 | def __init__(self, exchange, currency, config, retries, debug=False, sandbox=False): 99 | self.exchange = getattr(ccxt, exchange)(config) 100 | if sandbox: 101 | self.exchange.set_sandbox_mode(True) 102 | self.currency = currency 103 | self.retries = retries 104 | self.debug = debug 105 | balance = self.exchange.fetch_balance() if 'secret' in config else 0 106 | try: 107 | if balance == 0 or not balance['free'][currency]: 108 | self._cash = 0 109 | else: 110 | self._cash = balance['free'][currency] 111 | except KeyError: # never funded or eg. all USD exchanged 112 | self._cash = 0 113 | try: 114 | if balance == 0 or not balance['total'][currency]: 115 | self._value = 0 116 | else: 117 | self._value = balance['total'][currency] 118 | except KeyError: 119 | self._value = 0 120 | 121 | def get_granularity(self, timeframe, compression): 122 | if not self.exchange.has['fetchOHLCV']: 123 | raise NotImplementedError("'%s' exchange doesn't support fetching OHLCV data" % \ 124 | self.exchange.name) 125 | 126 | granularity = self._GRANULARITIES.get((timeframe, compression)) 127 | if granularity is None: 128 | raise ValueError("backtrader CCXT module doesn't support fetching OHLCV " 129 | "data for time frame %s, comression %s" % \ 130 | (bt.TimeFrame.getname(timeframe), compression)) 131 | 132 | if self.exchange.timeframes and granularity not in self.exchange.timeframes: 133 | raise ValueError("'%s' exchange doesn't support fetching OHLCV data for " 134 | "%s time frame" % (self.exchange.name, granularity)) 135 | 136 | return granularity 137 | 138 | def retry(method): 139 | @wraps(method) 140 | def retry_method(self, *args, **kwargs): 141 | for i in range(self.retries): 142 | if self.debug: 143 | print('{} - {} - Attempt {}'.format(datetime.now(), method.__name__, i)) 144 | time.sleep(self.exchange.rateLimit / 1000) 145 | try: 146 | return method(self, *args, **kwargs) 147 | except (NetworkError, ExchangeError): 148 | if i == self.retries - 1: 149 | raise 150 | 151 | return retry_method 152 | 153 | @retry 154 | def get_wallet_balance(self, currency, params=None): 155 | balance = self.exchange.fetch_balance(params) 156 | return balance 157 | 158 | @retry 159 | def get_balance(self): 160 | balance = self.exchange.fetch_balance() 161 | 162 | cash = balance['free'][self.currency] 163 | value = balance['total'][self.currency] 164 | # Fix if None is returned 165 | self._cash = cash if cash else 0 166 | self._value = value if value else 0 167 | 168 | @retry 169 | def getposition(self): 170 | return self._value 171 | # return self.getvalue(currency) 172 | 173 | @retry 174 | def create_order(self, symbol, order_type, side, amount, price, params): 175 | # returns the order 176 | return self.exchange.create_order(symbol=symbol, type=order_type, side=side, 177 | amount=amount, price=price, params=params) 178 | 179 | @retry 180 | def cancel_order(self, order_id, symbol): 181 | return self.exchange.cancel_order(order_id, symbol) 182 | 183 | @retry 184 | def fetch_trades(self, symbol): 185 | return self.exchange.fetch_trades(symbol) 186 | 187 | @retry 188 | def fetch_ohlcv(self, symbol, timeframe, since, limit, params={}): 189 | if self.debug: 190 | print('Fetching: {}, TF: {}, Since: {}, Limit: {}'.format(symbol, timeframe, since, limit)) 191 | return self.exchange.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=limit, params=params) 192 | 193 | @retry 194 | def fetch_order(self, oid, symbol): 195 | return self.exchange.fetch_order(oid, symbol) 196 | 197 | @retry 198 | def fetch_open_orders(self, symbol=None): 199 | if symbol == None: 200 | return self.exchange.fetchOpenOrders() 201 | else: 202 | return self.exchange.fetchOpenOrders(symbol) 203 | 204 | @retry 205 | def private_end_point(self, type, endpoint, params): 206 | ''' 207 | Open method to allow calls to be made to any private end point. 208 | See here: https://github.com/ccxt/ccxt/wiki/Manual#implicit-api-methods 209 | 210 | - type: String, 'Get', 'Post','Put' or 'Delete'. 211 | - endpoint = String containing the endpoint address eg. 'order/{id}/cancel' 212 | - Params: Dict: An implicit method takes a dictionary of parameters, sends 213 | the request to the exchange and returns an exchange-specific JSON 214 | result from the API as is, unparsed. 215 | 216 | To get a list of all available methods with an exchange instance, 217 | including implicit methods and unified methods you can simply do the 218 | following: 219 | 220 | print(dir(ccxt.hitbtc())) 221 | ''' 222 | return getattr(self.exchange, endpoint)(params) 223 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PRODUCTION = "production" 4 | DEVELOPMENT = "development" 5 | 6 | COIN_TARGET = "BTC" 7 | COIN_REFER = "USDT" 8 | 9 | ENV = os.getenv("ENVIRONMENT", PRODUCTION) 10 | DEBUG = False 11 | 12 | # futures 13 | BINANCE = { 14 | "key": "9c5a3bdbe030a794a0b4920a7916bdaf0c5c650af8aac7e094fdeeef9a2eae08", 15 | "secret": "f2e9e9229556b0d9258c807930fc3e61d5937ba3db011b56929b2a0cb20274b6" 16 | } 17 | 18 | # spot 19 | # BINANCE = { 20 | # "key": "DA9Rm9HKVsBQ8hXjbj6omu1vY7ZaAPWDZ8sF01lN89ih4AfVh629KqfLQa2UO4w5", 21 | # "secret": "VHfl78kWdS6VqPhhoh7S8BXyhzcDIwZixNDoFfNdJ9U6PhpKbUeWSpsCIlTbhh9v" 22 | # } 23 | 24 | TELEGRAM = { 25 | "channel": "", 26 | "bot": "" 27 | } 28 | 29 | print("ENV = ", ENV) -------------------------------------------------------------------------------- /dataset/dataset.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | 3 | 4 | class CustomDataset(bt.feeds.GenericCSVData): 5 | params = ( 6 | ('time', -1), 7 | ('datetime', 0), 8 | ('open', 1), 9 | ('high', 2), 10 | ('low', 3), 11 | ('close', 4), 12 | ('volume', 5), 13 | ('openinterest', 6), 14 | ) 15 | -------------------------------------------------------------------------------- /dataset/queries.sql: -------------------------------------------------------------------------------- 1 | select strftime('%Y-%m-%d %H:%M:%S', datetime(start, 'unixepoch')) as start, open, high, low, close, volume, trades 2 | from candles_USDT_BTC 3 | order by start asc -------------------------------------------------------------------------------- /dataset/symbol_config.yaml: -------------------------------------------------------------------------------- 1 | BNBUSDT: 2 | BasicRSI: 3 | 5m: 4 | allocated_ratio: 0.3 5 | ema_fast_window: 10 6 | ema_slow_window: 200 7 | 10m: 8 | allocated_ratio: 0.3 9 | ema_fast_window: 10 10 | ema_slow_window: 200 11 | 20m: 12 | allocated_ratio: 0.3 13 | ema_fast_window: 10 14 | ema_slow_window: 200 15 | 40m: 16 | allocated_ratio: 0.3 17 | ema_fast_window: 10 18 | ema_slow_window: 200 19 | BTCUSDT: 20 | BasicRSI: 21 | 5m: 22 | allocated_ratio: 0.3 23 | ema_fast_window: 10 24 | ema_slow_window: 200 25 | 10m: 26 | allocated_ratio: 0.3 27 | ema_fast_window: 10 28 | ema_slow_window: 200 29 | 20m: 30 | allocated_ratio: 0.3 31 | ema_fast_window: 10 32 | ema_slow_window: 200 33 | 40m: 34 | allocated_ratio: 0.3 35 | ema_fast_window: 10 36 | ema_slow_window: 200 -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/img.png -------------------------------------------------------------------------------- /img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/img_1.png -------------------------------------------------------------------------------- /indicators/macd_hist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import backtrader as bt 4 | 5 | 6 | class MACDHistSMA(bt.Indicator): 7 | lines = ('histo',) 8 | params = ( 9 | ('period', 14), 10 | ) 11 | 12 | def __init__(self): 13 | MACD = bt.ind.MACDHisto() 14 | self.l.histo = bt.indicators.MovingAverageSimple(MACD.histo, period=self.p.period) 15 | -------------------------------------------------------------------------------- /indicators/stoch_rsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import backtrader as bt 4 | 5 | 6 | class StochRSI(bt.Indicator): 7 | lines = ('fastk', 'fastd',) 8 | 9 | params = ( 10 | ('k_period', 3), 11 | ('d_period', 3), 12 | ('period', 14), 13 | ('stoch_period', 14), 14 | ('upperband', 80.0), 15 | ('lowerband', 20.0), 16 | ) 17 | 18 | def __init__(self, base_indicator): 19 | rsi_ll = bt.ind.Lowest(base_indicator, period=self.p.period) 20 | rsi_hh = bt.ind.Highest(base_indicator, period=self.p.period) 21 | stochrsi = (base_indicator - rsi_ll) / ((rsi_hh - rsi_ll) + 0.00001) 22 | 23 | self.l.fastk = k = bt.indicators.MovingAverageSimple(100.0 * stochrsi, period=self.p.k_period) 24 | self.l.fastd = bt.indicators.MovingAverageSimple(k, period=self.p.d_period) 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backtrader>=1.9.76.123 2 | matplotlib>=3.2.0 3 | cryptocmd 4 | pandas>=1.3.3 5 | termcolor>=1.1.0 6 | PyYAML>=5.4.1 7 | git+git://github.com/Dave-Vallance/bt-ccxt-store@master#bt-ccxt-store 8 | requests>=2.26.0 9 | ccxt>=1.60.88 10 | yaml>=0.2.5 11 | arrow>=1.1.1 12 | bs4>=0.0.1 13 | beautifulsoup4>=4.10.0 14 | numexpr>=2.7.3 15 | psutil>=5.8.0 16 | numpy>=1.21.2 17 | tushare>=1.2.71 18 | pyfolio>=0.9.2 19 | h5py>=2.10.0 20 | scikit-learn>=0.24.2 21 | optunity>=1.1.1 22 | scipy>=1.7.1 23 | statsmodels>=0.12.2 24 | blackbox>=0.7.1 25 | ipython>=7.27.0 -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EarronYu/backtrader-binance-futures/fc1ba30958900439d45f6d65588676e22a2b61d9/screenshot.png -------------------------------------------------------------------------------- /sizer/percent.py: -------------------------------------------------------------------------------- 1 | from backtrader.sizers import PercentSizer 2 | 3 | 4 | class FullMoney(PercentSizer): 5 | params = ( 6 | ('percents', 99), 7 | ) 8 | -------------------------------------------------------------------------------- /strategies/BBKCBreak.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import functions.toolkit as tk 3 | from datetime import datetime 4 | import arrow 5 | 6 | 7 | class BBKCBreak(bt.Strategy): 8 | """ 9 | 简单布林线策略 10 | 价格突破上轨做多,下穿中轨平多 11 | 价格突破下轨做空,上穿中轨平空 12 | """ 13 | params = dict(window=262, bbdevs=3.7, kcdevs=1.37, bias_pct=0.398, volatile_pct=0.05) 14 | # window = 262, bbdevs = 3.702624414062499, kcdevs = 1.3728536286917288, bias_pct = 0.39861572204232243 15 | def log(self, txt, dt=None, fgPrint=False): 16 | # 增强型log记录函数,带fgPrint打印开关变量 17 | if fgPrint: 18 | dt = dt or self.datas[0].datetime.date(0) 19 | tn = tk.timNSec('', self.tim0wrk) 20 | # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 21 | print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 22 | 23 | def __init__(self): 24 | # Set some pointers / references 25 | for i, d in enumerate(self.datas): 26 | if d._name == 'Real': 27 | self.kl = d 28 | elif d._name == 'Heikin': 29 | self.ha = d 30 | 31 | self.buyprice = None 32 | self.buycomm = None 33 | self.sellprice = None 34 | self.sellcomm = None 35 | self.tim0wrk = arrow.now() 36 | self.dataclose = self.datas[0].close 37 | self.order = None 38 | 39 | bb = bt.ind.BollingerBands(self.ha, period=self.params.window, devfactor=self.params.bbdevs, movav=bt.ind.MovingAverageSimple, plot=False) 40 | ma = bt.ind.MovingAverageSimple(self.ha, period=self.p.window, plot=False) 41 | atr = self.params.kcdevs * bt.ind.ATR(self.ha, period=self.params.window, plot=False) 42 | kctop = ma + atr 43 | kcbot = ma - atr 44 | bbkctop = bt.If(bb.top > kctop, bb.top, kctop) 45 | bbkcbot = bt.If(bb.bot < kcbot, bb.bot, kcbot) 46 | 47 | volatile = abs(self.ha.open / ma - 1) 48 | low_volatile = bt.If(volatile < 0.05, 1, 0) 49 | bias = abs(self.ha.close / ma - 1) 50 | low_bias = bt.If(bias <= self.params.bias_pct, 1, 0) 51 | 52 | crossuptop = bt.ind.CrossUp(self.ha.close, bbkctop, plot=False) 53 | crossdownbot = bt.ind.CrossDown(self.ha.close, bbkcbot, plot=False) 54 | self.close_long = bt.indicators.CrossDown(self.ha.close, bb.mid, plot=False) 55 | self.close_short = bt.indicators.CrossUp(self.ha.close, bb.mid, plot=False) 56 | self.open_long = bt.And(crossuptop, low_volatile, low_bias) 57 | self.open_short = bt.And(crossdownbot, low_volatile, low_bias) 58 | 59 | def notify_order(self, order): 60 | if order.status in [order.Submitted, order.Accepted]: 61 | # 检查订单执行状态order.status: 62 | # Buy/Sell order submitted/accepted to/by broker 63 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 64 | # 正常流程,无需额外操作 65 | return 66 | 67 | # 检查订单order是否完成 68 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 69 | # 可以修改相关参数,调整进行空头交易 70 | if order.status in [order.Completed]: 71 | if order.isbuy(): 72 | self.log('买单执行BUY EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 73 | % (order.executed.price, order.executed.value, order.executed.comm)) 74 | self.buyprice = order.executed.price 75 | self.buycomm = order.executed.comm 76 | elif order.issell(): 77 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 78 | % (order.executed.price, order.executed.value, order.executed.comm)) 79 | self.sellprice = order.executed.price 80 | self.sellcomm = order.executed.comm 81 | 82 | self.bar_executed = len(self) 83 | 84 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 85 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 86 | 87 | # 检查完成,没有交易中订单(pending order) 88 | self.order = None 89 | 90 | def notify_trade(self, trade): 91 | # 检查交易trade是关闭 92 | if not trade.isclosed: 93 | return 94 | 95 | dt = self.data.datetime.date() 96 | self.log('---------------------------- TRADE ---------------------------------') 97 | self.log("1: Data Name: {}".format(trade.data._name)) 98 | self.log("2: Bar Num: {}".format(len(trade.data))) 99 | self.log("3: Current date: {}".format(dt)) 100 | self.log('4: Status: Trade Complete') 101 | self.log('5: Ref: {}'.format(trade.ref)) 102 | self.log('6: PnL: {}'.format(round(trade.pnl, 2))) 103 | self.log('--------------------------------------------------------------------') 104 | 105 | def next(self): 106 | if self.order: 107 | return 108 | pfl = self.broker.get_cash() 109 | if self.open_long and not self.position: 110 | self.order = self.order_target_percent(data=self.kl, target=0.99) 111 | if self.close_long and self.position: 112 | self.order = self.order_target_percent(data=self.kl, target=0) 113 | if self.open_short and not self.position: 114 | self.order = self.order_target_percent(data=self.kl, target=-0.99) 115 | if self.close_short and self.position: 116 | self.order = self.order_target_percent(data=self.kl, target=0) 117 | 118 | def stop(self): 119 | # 新增加一个stop策略完成函数 120 | # 用于输出执行后数据 121 | self.log(f'(策略参数 window={self.params.window}, bbdevs={self.params.bbdevs}, kcdevs={self.params.kcdevs}, bias_pct={self.params.bias_pct}) ,最终资产总值:{self.broker.getvalue()}', fgPrint=True) 122 | 123 | 124 | class BBKCBreakWF(bt.Strategy): 125 | """The SMAC strategy but in a walk-forward analysis context""" 126 | params = {"start_dates": None, # Starting days for trading periods (a list) 127 | "end_dates": None, # Ending day for trading periods (a list) 128 | "var1": None, # List of fast moving average windows, corresponding to start dates (a list) 129 | "var2": None} # Like fast, but for slow moving average window (a list) 130 | 131 | # All the above lists must be of the same length, and they all line up 132 | 133 | def __init__(self): 134 | """Initialize the strategy""" 135 | self.sma = dict() 136 | self.var1 = dict() 137 | self.var2 = dict() 138 | self.regime = dict() 139 | 140 | self.date_combos = [c for c in zip(self.p.start_dates, self.p.end_dates)] 141 | # Error checking 142 | if type(self.p.start_dates) is not list or type(self.p.end_dates) is not list or \ 143 | type(self.p.fast) is not list or type(self.p.slow) is not list: 144 | raise ValueError("Must past lists filled with numbers to params start_dates, end_dates, fast, slow.") 145 | elif len(self.p.start_dates) != len(self.p.end_dates) or \ 146 | len(self.p.fast) != len(self.p.start_dates) or len(self.p.slow) != len(self.p.start_dates): 147 | raise ValueError("All lists passed to params must have same length.") 148 | for d in self.getdatanames(): 149 | self.sma[d] = dict() 150 | self.var1[d] = dict() 151 | self.var2[d] = dict() 152 | self.regime[d] = dict() 153 | 154 | # Additional indexing, allowing for differing start/end dates 155 | for sd, ed, f, s in zip(self.p.start_dates, self.p.end_dates, self.p.var1, self.p.var2): 156 | # More error checking 157 | ''' 158 | if type(f) is not int or type(s) is not int: 159 | raise ValueError("Must include only integers in fast, slow.") 160 | elif f > s: 161 | raise ValueError("Elements in fast cannot exceed elements in slow.") 162 | elif f <= 0 or s <= 0: 163 | raise ValueError("Moving average windows must be positive.") 164 | 165 | 166 | if type(sd) is not dt.date or type(ed) is not dt.date: 167 | raise ValueError("Only datetime dates allowed in start_dates, end_dates.") 168 | elif ed - sd < dt.timedelta(0): 169 | raise ValueError("Start dates must always be before end dates.") 170 | ''' 171 | # The moving averages 172 | # Notice that different moving averages are obtained for different combinations of 173 | # start/end dates 174 | self.sma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d), 175 | period=globalparams['sma_period'], 176 | plot=False) 177 | self.var1[d][(sd, ed)] = f 178 | self.var2[d][(sd, ed)] = s 179 | ''' 180 | self.fastma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d), 181 | period=f, 182 | plot=False) 183 | self.slowma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d), 184 | period=s, 185 | plot=False) 186 | 187 | # Get the regime 188 | self.regime[d][(sd, ed)] = self.fastma[d][(sd, ed)] - self.slowma[d][(sd, ed)] 189 | # In the future, use the backtrader indicator btind.CrossOver() 190 | ''' 191 | 192 | def next(self): 193 | """Define what will be done in a single step, including creating and closing trades""" 194 | 195 | # Determine which set of moving averages to use 196 | curdate = self.datetime.date(0) 197 | dtidx = None # Will be index 198 | # Determine which period (if any) we are in 199 | for sd, ed in self.date_combos: 200 | # Debug output 201 | # print('{}: {} < {}: {}, {} < {}: {}'.format( 202 | # len(self), sd, curdate, (sd <= curdate), curdate, ed, (curdate <= ed))) 203 | if sd <= curdate and curdate <= ed: 204 | dtidx = (sd, ed) 205 | # Debug output 206 | # print('{}: the dtixdx is {}, and curdate is {};'.format(len(self), dtidx, curdate)) 207 | for d in self.getdatanames(): # Looping through all symbols 208 | pos = self.getpositionbyname(d).size or 0 209 | if dtidx is None: # Not in any window 210 | break # Don't engage in trades 211 | if pos == 0: # Are we out of the market? 212 | # Consider the possibility of entrance 213 | # Notice the indexing; [0] always mens the present bar, and [-1] the bar immediately preceding 214 | # Thus, the condition below translates to: "If today the regime is bullish (greater than 215 | # 0) and yesterday the regime was not bullish" 216 | '''if self.slowma[d][dtidx][0] > self.getdatabyname(d).close[0]: # A buy signal 217 | self.sell(data=self.getdatabyname(d), size=1000) 218 | 219 | else: # We have an open position 220 | if self.fastma[d][dtidx][0] < self.getdatabyname(d).close[0]: # A sell signal 221 | self.close(data=self.getdatabyname(d), size=1000) 222 | ''' 223 | if self.sma[d][dtidx][0] * self.var1[d][dtidx] > self.getdatabyname(d).high[0]: # A buy signal 224 | self.order_target_percent(data=self.getdatabyname(d), target=0.98) 225 | 226 | else: # We have an open position 227 | if self.getdatabyname(d).close[-1] * self.var2[d][dtidx] <= self.getdatabyname(d).high[0]: # A sell signal 228 | self.order_target_percent(data=self.getdatabyname(d), target=0) 229 | -------------------------------------------------------------------------------- /strategies/BBKCReverse.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import functions.toolkit as tk 3 | from datetime import datetime 4 | import arrow 5 | 6 | 7 | class BBKCReverse(bt.Strategy): 8 | """ 9 | 简单布林线策略 10 | 价格突破上轨做多,下穿中轨平多 11 | 价格突破下轨做空,上穿中轨平空 12 | """ 13 | params = dict(window=20, bbdevs=2, kcdevs=1.5, bias_pct=0.005, volatile_pct=0.005, stop_loss=0.02, trail=False, ) 14 | 15 | def log(self, txt, dt=None, fgPrint=False): 16 | # 增强型log记录函数,带fgPrint打印开关变量 17 | if fgPrint: 18 | dt = dt or self.datas[0].datetime.date(0) 19 | tn = tk.timNSec('', self.tim0wrk) 20 | # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 21 | print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 22 | 23 | def __init__(self): 24 | # Set some pointers / references 25 | for i, d in enumerate(self.datas): 26 | if d._name == 'Real': 27 | self.real = d 28 | elif d._name == 'Heikin': 29 | self.ha = d 30 | 31 | self.buyprice = None 32 | self.buycomm = None 33 | self.sellprice = None 34 | self.sellcomm = None 35 | self.tim0wrk = arrow.now() 36 | self.dataclose = self.datas[0].close 37 | self.order = None 38 | 39 | bb = bt.ind.BollingerBands(self.ha, period=self.params.window, devfactor=self.params.bbdevs, movav=bt.ind.MovingAverageSimple, plot=False) 40 | ma = bt.ind.MovingAverageSimple(self.ha, period=self.p.window, plot=False) 41 | atr = self.params.kcdevs * bt.ind.ATR(self.ha, period=self.params.window, plot=False) 42 | kctop = ma + atr 43 | kcbot = ma - atr 44 | bbkctop = bt.If(bb.top > kctop, bb.top, kctop) 45 | bbkcbot = bt.If(bb.bot < kcbot, bb.bot, kcbot) 46 | 47 | volatile = abs(self.ha.open / ma - 1) 48 | high_volatile = bt.If(volatile >= self.params.volatile_pct, 1, 0) 49 | bias = abs(self.ha.close / ma - 1) 50 | high_bias = bt.If(bias >= self.params.bias_pct, 1, 0) 51 | 52 | crossupbot = bt.ind.CrossUp(self.ha.close, bbkcbot, plot=False) 53 | crossdowntop = bt.ind.CrossDown(self.ha.close, bbkctop, plot=False) 54 | self.close_long = bt.indicators.CrossUp(self.ha.close, bbkctop, plot=False) 55 | self.close_short = bt.indicators.CrossDown(self.ha.close, bbkcbot, plot=False) 56 | self.open_short = bt.And(crossdowntop, high_volatile, high_bias) 57 | self.open_long = bt.And(crossupbot, high_volatile, high_bias) 58 | 59 | def notify_order(self, order): 60 | if order.status in [order.Submitted, order.Accepted]: 61 | # 检查订单执行状态order.status: 62 | # Buy/Sell order submitted/accepted to/by broker 63 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 64 | # 正常流程,无需额外操作 65 | return 66 | 67 | # 检查订单order是否完成 68 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 69 | # 可以修改相关参数,调整进行空头交易 70 | if order.status in [order.Completed]: 71 | if order.isbuy(): 72 | # if not self.p.trail: 73 | # stop_price = order.executed.price * (1.0 - self.p.stop_loss) 74 | # self.sell(exectype=bt.Order.Stop, price=stop_price) 75 | # else: 76 | # self.sell(exectype=bt.Order.StopTrail, trailamount=self.p.trail) 77 | self.log('买单执行BUY EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 78 | % (order.executed.price, order.executed.value, order.executed.comm)) 79 | self.buyprice = order.executed.price 80 | self.buycomm = order.executed.comm 81 | elif order.issell(): 82 | # if not self.p.trail: 83 | # stop_price = order.executed.price * (1.0 + self.p.stop_loss) 84 | # self.buy(exectype=bt.Order.Stop, price=stop_price) 85 | # else: 86 | # self.buy(exectype=bt.Order.StopTrail, trailamount=self.p.trail) 87 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 88 | % (order.executed.price, order.executed.value, order.executed.comm)) 89 | self.sellprice = order.executed.price 90 | self.sellcomm = order.executed.comm 91 | 92 | self.bar_executed = len(self) 93 | 94 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 95 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 96 | 97 | # 检查完成,没有交易中订单(pending order) 98 | # self.order = None 99 | 100 | def notify_trade(self, trade): 101 | # 检查交易trade是关闭 102 | if not trade.isclosed: 103 | return 104 | 105 | dt = self.data.datetime.date() 106 | self.log('---------------------------- TRADE ---------------------------------') 107 | self.log("1: Data Name: {}".format(trade.data._name)) 108 | self.log("2: Bar Num: {}".format(len(trade.data))) 109 | self.log("3: Current date: {}".format(dt)) 110 | self.log('4: Status: Trade Complete') 111 | self.log('5: Ref: {}'.format(trade.ref)) 112 | self.log('6: PnL: {}'.format(round(trade.pnl, 2))) 113 | self.log('--------------------------------------------------------------------') 114 | 115 | def next(self): 116 | if self.order: 117 | return 118 | pfl = self.broker.get_cash() 119 | if self.open_long and not self.position: 120 | self.order = self.order_target_percent(data=self.real, target=0.99) 121 | if self.close_long and self.position: 122 | self.order = self.order_target_percent(data=self.real, target=0) 123 | if self.open_short and not self.position: 124 | self.order = self.order_target_percent(data=self.real, target=-0.99) 125 | if self.close_short and self.position: 126 | self.order = self.order_target_percent(data=self.real, target=0) 127 | 128 | def stop(self): 129 | # 新增加一个stop策略完成函数 130 | # 用于输出执行后数据 131 | self.log( 132 | f'(策略参数 window={self.params.window}, bbdevs={self.params.bbdevs}, kcdevs={self.params.kcdevs}, bias_pct={self.params.bias_pct}, volatile_pct=' 133 | f'{self.params.volatile_pct}) ,最终资产总值:{self.broker.getvalue()}', 134 | fgPrint=True) 135 | -------------------------------------------------------------------------------- /strategies/BBReverse.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import functions.toolkit as tk 3 | from datetime import datetime 4 | import arrow 5 | 6 | 7 | class BBReverse(bt.Strategy): 8 | """ 9 | :param df: 10 | :param para: n, m,mean 11 | :return: 12 | # 布林线策略 13 | # 布林线中轨:n天收盘价的移动平均线 14 | # 布林线上轨:n天收盘价的移动平均线 + m * n天收盘价的标准差 15 | # 布林线上轨:n天收盘价的移动平均线 - m * n天收盘价的标准差 16 | # 当收盘价由下向上穿过上轨的时候,做多;然后由上向下穿过中轨的时候,平仓。 17 | # 当收盘价由上向下穿过下轨的时候,做空;然后由下向上穿过中轨的时候,平仓。 18 | 根据Ryan萨博的思路,计算开仓价格与中线的比值 19 | https://bbs.quantclass.cn/thread/4521 20 | 根据JIN的上下轨道使用平均差思路 21 | https://bbs.quantclass.cn/thread/4443 22 | """ 23 | params = dict(bollwindow=200, devfactor=2, bias_pct=0.05, pgoperiod=200, pgorange=3.0) 24 | 25 | def log(self, txt, dt=None, fgPrint=False): 26 | # 增强型log记录函数,带fgPrint打印开关变量 27 | if fgPrint: 28 | dt = dt or self.datas[0].datetime.date(0) 29 | tn = tk.timNSec('', self.tim0wrk) 30 | # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 31 | print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 32 | 33 | def __init__(self): 34 | self.bar_executed = len(self) 35 | self.buyprice = None 36 | self.buycomm = None 37 | self.sellprice = None 38 | self.sellcomm = None 39 | self.tim0wrk = arrow.now() 40 | self.dataclose = self.datas[0].close 41 | self.order = None 42 | 43 | midband = bt.indicators.MovingAverageSimple(self.data, period=self.params.bollwindow) 44 | std = bt.indicators.StandardDeviation(self.data, period=self.params.bollwindow, plot=False) 45 | topband = midband + self.params.devfactor * std 46 | botband = midband - self.params.devfactor * std 47 | volatile = abs(self.data.open / midband - 1) 48 | low_volatile = bt.If(volatile < 0.05, 1, 0) 49 | bias = abs(self.data.close / midband - 1) 50 | low_bias = bt.If(bias <= self.params.bias_pct, 1, 0) 51 | pgo = bt.indicators.PrettyGoodOscillator(self.data, period=self.params.pgoperiod, plot=False) 52 | # low_pgo = bt.indicators.If(abs(pgo) < self.params.pgorange, 1, 0) 53 | crossuptop = bt.indicators.CrossUp(self.data.close, topband, plot=False) 54 | self.close_long = bt.indicators.CrossDown(self.data.close, midband, plot=False) 55 | crossdownbot = bt.indicators.CrossDown(self.data.close, botband, plot=False) 56 | self.close_short = bt.indicators.CrossUp(self.data.close, midband, plot=False) 57 | self.open_long = bt.And(crossuptop, low_volatile, low_bias) 58 | self.open_short = bt.And(crossdownbot, low_volatile, low_bias) 59 | 60 | def notify_order(self, order): 61 | if order.status in [order.Submitted, order.Accepted]: 62 | # 检查订单执行状态order.status: 63 | # Buy/Sell order submitted/accepted to/by broker 64 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 65 | # 正常流程,无需额外操作 66 | return 67 | 68 | # 检查订单order是否完成 69 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 70 | # 可以修改相关参数,调整进行空头交易 71 | if order.status in [order.Completed]: 72 | if order.isbuy(): 73 | self.log('买单执行BUY EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 74 | % (order.executed.price, order.executed.value, order.executed.comm)) 75 | self.buyprice = order.executed.price 76 | self.buycomm = order.executed.comm 77 | elif order.issell(): 78 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 79 | % (order.executed.price, order.executed.value, order.executed.comm)) 80 | self.sellprice = order.executed.price 81 | self.sellcomm = order.executed.comm 82 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 83 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 84 | 85 | # 检查完成,没有交易中订单(pending order) 86 | self.order = None 87 | 88 | def notify_trade(self, trade): 89 | # 检查交易trade是关闭 90 | if not trade.isclosed: 91 | return 92 | 93 | self.log('交易操盘利润OPERATION PROFIT, 毛利GROSS %.2f, 净利NET %.2f' % 94 | (trade.pnl, trade.pnlcomm)) 95 | 96 | def next(self): 97 | if self.order: 98 | return 99 | if self.open_long and not self.position: 100 | self.log('设置多单 BUY CREATE, @ %.2f' % self.dataclose[0]) 101 | self.order = self.order_target_percent(target=0.99) 102 | if self.close_long and self.position: 103 | self.log('设置平多单 CLOSE LONG CREATE, @ %.2f' % self.dataclose[0]) 104 | self.order = self.order_target_percent(target=0.00) 105 | if self.open_short and not self.position: 106 | self.log('设置空单 BUY CREATE, @ %.2f' % self.dataclose[0]) 107 | self.order = self.order_target_percent(target=-0.99) 108 | if self.close_short and self.position: 109 | self.log('设置平空单 CLOSE LONG CREATE, @ %.2f' % self.dataclose[0]) 110 | self.order = self.order_target_percent(target=0.00) 111 | 112 | def stop(self): 113 | # 新增加一个stop策略完成函数 114 | # 用于输出执行后数据 115 | self.log(f'(策略参数 bollwindow={self.params.bollwindow}, devfactor={self.params.devfactor}, bias_pct={self.params.bias_pct}, pgoperiod={self.params.pgoperiod}, pgorange={self.params.pgorange}) ,最终资产总值:{self.broker.getvalue()}', fgPrint=True) 116 | 117 | 118 | -------------------------------------------------------------------------------- /strategies/BasicRSI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import backtrader as bt 4 | import yaml 5 | from config import DEVELOPMENT, COIN_TARGET, COIN_REFER, ENV, PRODUCTION, DEBUG 6 | 7 | from strategies.base import StrategyBase 8 | 9 | import toolkit as tk 10 | 11 | 12 | class BasicRSI(StrategyBase): 13 | 14 | def __init__(self): 15 | # 含有Heikin的数据是存储所有策略信息的 16 | StrategyBase.__init__(self) 17 | self.log("Using RSI/EMA strategy", fgprint=False) 18 | # todo 将这个策略参考SMAC和long——short进行修改 19 | 20 | self.params = dict() 21 | self.ind = dict() 22 | 23 | for d in self.getdatanames(): 24 | if 'Heikin' in d: 25 | strategy_params = self.load_params(strategy=self.__class__.__name__.rstrip('_Heikin'), data=d) 26 | # self.params[d] = dict() 27 | self.params[d]['ema_fast_window'] = strategy_params['var1'] 28 | self.params[d]['ema_slow_window'] = strategy_params['var2'] 29 | self.ind[d]['stoploss'] = strategy_params['var3'] 30 | 31 | self.ind[d] = dict() 32 | # 将指标的各项内容放到对应的数据源当中 33 | self.ind[d]['ema_fast'] = bt.indicators.EMA(self.getdatabyname(d), 34 | period=self.params[d]['ema_fast_window'], 35 | plotname="ema_fast: " + d) 36 | self.ind[d]['ema_slow'] = bt.indicators.EMA(self.getdatabyname(d), 37 | period=self.params[d]['ema_slow_window'], 38 | plotname="ema_slow: " + d) 39 | self.ind[d]['rsi'] = bt.indicators.RSI(self.getdatabyname(d)) 40 | 41 | def load_params(self, strategy, data): 42 | with open('../dataset/symbol_config.yaml', 'r') as f: 43 | symbol_config = f.read() 44 | symbol_config = yaml.load(symbol_config, Loader=yaml.FullLoader) 45 | param = data.replace('USDT', f'USDT_{strategy}') 46 | # BNBUSDT_MyStrategy_10m 47 | # param = param.remove('_Kline') 48 | param = param.split('_') 49 | strategy_params = symbol_config[param[0]][param[1]][param[2]] 50 | f.close() 51 | return strategy_params 52 | 53 | def notify_order(self, order): 54 | StrategyBase.notify_order(self, order) 55 | if order.status in [order.Completed]: 56 | # 检查订单order是否完成 57 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 58 | # 可以修改相关参数,调整进行空头交易 59 | for d in self.getdatanames(): 60 | if order.isbuy(): 61 | self.last_operation[d] = "long" 62 | self.reset_order_indicators() 63 | if ENV == DEVELOPMENT: 64 | # print(order.executed.__dict__) 65 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 66 | (order.executed.price, 67 | order.executed.value, 68 | order.executed.comm), fgprint=False) 69 | if ENV == PRODUCTION: 70 | print(order.__dict__) 71 | self.log('LONG EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 72 | (order.ccxt_order['average'], 73 | float(order.ccxt_order['info']['cumQuote']), 74 | float(order.ccxt_order['info']['cumQuote']) * 0.0004), send_telegram=False, 75 | fgprint=False) 76 | # tk.save_order(order, strategy=self.__class__.__name__, write=False) 77 | # self.buyprice = order.ccxt_order['average'] 78 | # self.buycomm = order.ccxt_order['cost'] * 0.0004 79 | if ENV == DEVELOPMENT: 80 | print('order info: ', order.__dict__) 81 | if order.issell(): 82 | self.last_operation[d] = "short" 83 | self.reset_order_indicators() 84 | if ENV == DEVELOPMENT: 85 | # print(order.executed.__dict__) 86 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 87 | (order.executed.price, 88 | order.executed.value, 89 | order.executed.comm), fgprint=False) 90 | if ENV == PRODUCTION: 91 | print(order.__dict__) 92 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 93 | (order.ccxt_order['average'], 94 | float(order.ccxt_order['info']['cumQuote']), 95 | float(order.ccxt_order['info']['cumQuote']) * 0.0004), send_telegram=False, 96 | fgprint=False) 97 | # tk.save_order(order, strategy=self.__class__.__name__, write=False) 98 | # self.sellprice = order.ccxt_order['average'] 99 | # self.sellcomm = order.ccxt_order['cost'] * 0.0004 100 | if ENV == DEVELOPMENT: 101 | print('order info: ', order.__dict__) 102 | self.bar_executed = len(self) 103 | # Sentinel to None: new orders allowed 104 | self.order = None 105 | 106 | def update_indicators(self): 107 | # self.profit = dict() 108 | for d in self.getdatanames(): 109 | if 'Heikin' in d: 110 | self.profit[d] = 0 111 | # dt, dn = self.datetime.datetime(), d.__name 112 | # pos = self.getposition(d).size 113 | # 利用真实价格来计算PNL 114 | if self.buy_price_close[d] and self.buy_price_close[d] > 0: 115 | self.profit[d] = float(self.getdatabyname(d.rstrip('_Heikin')).close[0] - self.buy_price_close[d]) / \ 116 | self.buy_price_close[d] 117 | if self.sell_price_close[d] and self.sell_price_close[d] > 0: 118 | self.profit[d] = float( 119 | self.sell_price_close[d] - self.getdatabyname(d.rstrip('_Heikin')).close[0]) / \ 120 | self.sell_price_close[d] 121 | 122 | def next(self): 123 | self.update_indicators() 124 | if self.status != "LIVE" and ENV == PRODUCTION: # waiting for live status in production 125 | return 126 | if self.order: # waiting for pending order 127 | return 128 | for d in self.getdatanames(): 129 | if 'Heikin' in d: 130 | # d = d.rstrp('_Heikin') # 以策略_周期这样干干净净的形式来作为其参数的容器,代表真实数据 131 | # 永远以真实数据来进行下单操作 132 | # dt, dn = self.datetime.datetime(), d.__name 133 | pos = self.getpositionbyname(d).size or 0 134 | # if not pos: 135 | # self.order = self.buy(d) 136 | # print(f'买买买{dt}, {dn}') 137 | if self.last_operation[d] != "long": 138 | if self.ind[d]['rsi'][0] < 30 and self.ind[d]['ema_fast'][0] > self.ind[d]['ema_slow'][0]: 139 | if not pos: 140 | self.order = self.long(data=self.getdatabyname(d.rstrip('_Heikin'))) 141 | else: 142 | self.order = self.close(data=self.getdatabyname(d.rstrip('_Heikin'))) 143 | self.order = self.long(data=self.getdatabyname(d.rstrip('_Heikin'))) 144 | if self.last_operation[d] != "short": 145 | if self.rsi > 70: 146 | if not pos: 147 | self.order = self.short(data=self.getdatabyname(d.rstrip('_Heikin'))) 148 | else: 149 | self.order = self.close(data=self.getdatabyname(d.rstrip('_Heikin'))) 150 | self.order = self.short(data=self.getdatabyname(d.rstrip('_Heikin'))) 151 | else: 152 | if pos: 153 | if -self.profit[d] > self.ind[d]['stoploss']: 154 | self.log("STOP LOSS: percentage %.3f %%" % self.profit[d], fgprint=False) 155 | if self.last_operation[d] == "long": 156 | self.order = self.close_short(data=self.getdatabyname(d.rstrip('_Heikin'))) 157 | if self.last_operation[d] == "short": 158 | self.order = self.close_short(data=self.getdatabyname(d.rstrip('_Heikin'))) 159 | # self.order = self.sell(d) 160 | # print('卖卖卖') 161 | # stop Loss 162 | 163 | 164 | class BasicRSIWalkForward(StrategyBase): # todo 1204 I'm working on this to turn it into a strategy competitive to WF method 165 | # All the above lists must be of the same length, and they all line up 166 | 167 | def __init__(self): 168 | """Initialize the strategy""" 169 | "The SMAC strategy but in a walk-forward analysis context" 170 | StrategyBase.__init__(self) 171 | self.var1 = dict() 172 | self.var2 = dict() 173 | self.var3 = dict() 174 | self.ema_fast = dict() 175 | self.ema_slow = dict() 176 | self.rsi = dict() 177 | 178 | self.date_combos = [c for c in zip(self.p.start_dates, self.p.end_dates)] 179 | 180 | "Error checking" 181 | if type(self.p.start_dates) is not list or type(self.p.end_dates) is not list or \ 182 | type(self.p.fast) is not list or type(self.p.slow) is not list: 183 | raise ValueError("Must past lists filled with numbers to params start_dates, end_dates, fast, slow.") 184 | elif len(self.p.start_dates) != len(self.p.end_dates) or \ 185 | len(self.p.fast) != len(self.p.start_dates) or len(self.p.slow) != len(self.p.start_dates): 186 | raise ValueError("All lists passed to params must have same length.") 187 | 188 | "process params and datetime period for multi datafeeds" 189 | for d in self.getdatanames(): 190 | "load params from the config file" 191 | strategy_params = self.load_params(strategy=self.__class__.__name__.rstrip('_Heikin'), data=d) 192 | # self.params[d] = dict() 193 | self.var1[d] = dict() 194 | self.var2[d] = dict() 195 | self.var3[d] = dict() 196 | self.ema_fast[d] = dict() 197 | self.ema_slow[d] = dict() 198 | self.rsi[d] = dict() 199 | 200 | if 'Heikin' in d: 201 | # Additional indexing, allowing for differing start/end dates 202 | # assign params for every period 203 | # todo turn params below to zip function and assign to f, s, t 204 | self.var1[d] = strategy_params['var1'] # ema_fast_window 205 | self.var2[d] = strategy_params['var2'] # ema_slow_window 206 | self.var3[d] = strategy_params['var3'] # stop_loss_rate 207 | for sd, ed, f, s, t in zip(self.p.start_dates, self.p.end_dates, self.var1[d], self.var2[d], self.var3[d]): 208 | # More error checking 209 | ''' 210 | if type(f) is not int or type(s) is not int: 211 | raise ValueError("Must include only integers in fast, slow.") 212 | elif f > s: 213 | raise ValueError("Elements in fast cannot exceed elements in slow.") 214 | elif f <= 0 or s <= 0: 215 | raise ValueError("Moving average windows must be positive.") 216 | 217 | 218 | if type(sd) is not dt.date or type(ed) is not dt.date: 219 | raise ValueError("Only datetime dates allowed in start_dates, end_dates.") 220 | elif ed - sd < dt.timedelta(0): 221 | raise ValueError("Start dates must always be before end dates.") 222 | ''' 223 | # The moving averages 224 | # Notice that different moving averages are obtained for different combinations of 225 | # start/end dates 226 | self.ind[d][(sd, ed)] = bt.indicators.EMA(self.getdatabyname(d), 227 | period=self.params[d]['ema_fast_window'], 228 | plotname="ema_fast: " + d) 229 | self.ind[d][(sd, ed)] = bt.indicators.EMA(self.getdatabyname(d), 230 | period=self.params[d]['ema_slow_window'], 231 | plotname="ema_slow: " + d) 232 | self.ind[d][(sd, ed)] = bt.indicators.RSI(self.getdatabyname(d)) 233 | self.ema[d][(sd, ed)] = bt.indicators.EMA(self.getdatabyname(d), 234 | period=self.params[d]['ema_fast_window'], 235 | plotname="ema_fast: " + d) 236 | self.var1[d][(sd, ed)] = f 237 | self.var2[d][(sd, ed)] = s 238 | self.var3[d][(sd, ed)] = t 239 | ''' 240 | self.fastma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d), 241 | period=f, 242 | plot=False) 243 | self.slowma[d][(sd, ed)] = btind.SimpleMovingAverage(self.getdatabyname(d), 244 | period=s, 245 | plot=False) 246 | 247 | # Get the regime 248 | self.regime[d][(sd, ed)] = self.fastma[d][(sd, ed)] - self.slowma[d][(sd, ed)] 249 | # In the future, use the backtrader indicator btind.CrossOver() 250 | ''' 251 | 252 | def load_params(self, strategy, data): 253 | with open('../dataset/symbol_config.yaml', 'r') as f: 254 | symbol_config = f.read() 255 | symbol_config = yaml.load(symbol_config, Loader=yaml.FullLoader) 256 | param = data.replace('USDT', f'USDT_{strategy}') 257 | # BNBUSDT_MyStrategy_10m 258 | # param = param.remove('_Kline') 259 | param = param.split('_') 260 | strategy_params = symbol_config[param[0]][param[1]][param[2]] 261 | f.close() 262 | return strategy_params 263 | 264 | def next(self): 265 | """Define what will be done in a single step, including creating and closing trades""" 266 | 267 | # Determine which set of moving averages to use 268 | curdate = self.datetime.date(0) 269 | dtidx = None # Will be index 270 | # Determine which period (if any) we are in 271 | for sd, ed in self.date_combos: 272 | # Debug output 273 | # print('{}: {} < {}: {}, {} < {}: {}'.format( 274 | # len(self), sd, curdate, (sd <= curdate), curdate, ed, (curdate <= ed))) 275 | if sd <= curdate and curdate <= ed: 276 | dtidx = (sd, ed) 277 | # Debug output 278 | # print('{}: the dtixdx is {}, and curdate is {};'.format(len(self), dtidx, curdate)) 279 | for d in self.getdatanames(): # Looping through all symbols 280 | pos = self.getpositionbyname(d).size or 0 281 | if dtidx is None: # Not in any window 282 | break # Don't engage in trades 283 | if pos == 0: # Are we out of the market? 284 | # Consider the possibility of entrance 285 | # Notice the indexing; [0] always mens the present bar, and [-1] the bar immediately preceding 286 | # Thus, the condition below translates to: "If today the regime is bullish (greater than 287 | # 0) and yesterday the regime was not bullish" 288 | '''if self.slowma[d][dtidx][0] > self.getdatabyname(d).close[0]: # A buy signal 289 | self.sell(data=self.getdatabyname(d), size=1000) 290 | 291 | else: # We have an open position 292 | if self.fastma[d][dtidx][0] < self.getdatabyname(d).close[0]: # A sell signal 293 | self.close(data=self.getdatabyname(d), size=1000) 294 | ''' 295 | if self.sma[d][dtidx][0] * self.var1[d][dtidx] > self.getdatabyname(d).high[0]: # A buy signal 296 | self.order_target_percent(data=self.getdatabyname(d), target=0.98) 297 | 298 | else: # We have an open position 299 | if self.getdatabyname(d).close[-1] * self.var2[d][dtidx] <= self.getdatabyname(d).high[ 300 | 0]: # A sell signal 301 | self.order_target_percent(data=self.getdatabyname(d), target=0) 302 | -------------------------------------------------------------------------------- /strategies/BollReverse.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import backtrader as bt 3 | 4 | 5 | class BollReverse(bt.Strategy): 6 | """ 7 | This is a simple mean reversion bollinger band strategy. 8 | 9 | Entry Critria: 10 | - Long: 11 | - Price closes below the lower band 12 | - Stop Order entry when price crosses back above the lower band 13 | - Short: 14 | - Price closes above the upper band 15 | - Stop order entry when price crosses back below the upper band 16 | Exit Critria 17 | - Long/Short: Price touching the median line 18 | """ 19 | params = ( 20 | ("period", int(20)), 21 | ("devfactor", int(2)), 22 | ("size", 20), 23 | ("debug", False) 24 | ) 25 | 26 | def __init__(self): 27 | self.boll = bt.indicators.BollingerBands(period=self.p.period, devfactor=self.p.devfactor) 28 | self.cross_mid = bt.indicators.CrossOver(self.data.close, self.boll.lines.mb) 29 | 30 | def next(self): 31 | orders = self.broker.get_orders_open() 32 | # Cancel open orders so we can track the median line 33 | if orders: 34 | for order in orders: 35 | self.broker.cancel(order) 36 | if not self.position: 37 | if self.data.close > self.boll.lines.top: 38 | self.sell(exectype=bt.Order.Stop, price=self.boll.lines.top[0], size=self.p.size) 39 | if self.data.close < self.boll.lines.bot: 40 | self.buy(exectype=bt.Order.Stop, price=self.boll.lines.bot[0], size=self.p.size) 41 | 42 | else: 43 | if self.position.size > 0: 44 | self.sell(exectype=bt.Order.Limit, price=self.boll.lines.mb[0], size=self.p.size) 45 | else: 46 | self.buy(exectype=bt.Order.Limit, price=self.boll.lines.mb[0], size=self.p.size) 47 | 48 | def notify_trade(self,trade): 49 | if trade.isclosed: 50 | dt = self.data.datetime.date() 51 | print('---------------------------- TRADE ---------------------------------') 52 | print("1: Data Name: {}".format(trade.data._name)) 53 | print("2: Bar Num: {}".format(len(trade.data))) 54 | print("3: Current date: {}".format(dt)) 55 | print('4: Status: Trade Complete') 56 | print('5: Ref: {}'.format(trade.ref)) 57 | print('6: PnL: {}'.format(round(trade.pnl,2))) 58 | print('--------------------------------------------------------------------') 59 | -------------------------------------------------------------------------------- /strategies/BollingBear.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import functions.toolkit as tk 3 | from datetime import datetime 4 | import arrow 5 | 6 | 7 | class BollingerBear(bt.Strategy): 8 | """ 9 | :param df: 10 | :param para: n, m,mean 11 | :return: 12 | # 布林线策略 13 | # 布林线中轨:n天收盘价的移动平均线 14 | # 布林线上轨:n天收盘价的移动平均线 + m * n天收盘价的标准差 15 | # 布林线上轨:n天收盘价的移动平均线 - m * n天收盘价的标准差 16 | # 当收盘价由下向上穿过上轨的时候,做多;然后由上向下穿过中轨的时候,平仓。 17 | # 当收盘价由上向下穿过下轨的时候,做空;然后由下向上穿过中轨的时候,平仓。 18 | 根据Ryan萨博的思路,计算开仓价格与中线的比值 19 | https://bbs.quantclass.cn/thread/4521 20 | 根据JIN的上下轨道使用平均差思路 21 | https://bbs.quantclass.cn/thread/4443 22 | """ 23 | params = dict(bollwindow=200, devfactor=2, bias_pct=0.05) 24 | 25 | def log(self, txt, dt=None, fgPrint=False): 26 | # 增强型log记录函数,带fgPrint打印开关变量 27 | if fgPrint: 28 | dt = dt or self.datas[0].datetime.date(0) 29 | tn = tk.timNSec('', self.tim0wrk) 30 | # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 31 | print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 32 | 33 | def __init__(self): 34 | self.bar_executed = len(self) 35 | self.buyprice = None 36 | self.buycomm = None 37 | self.sellprice = None 38 | self.sellcomm = None 39 | self.tim0wrk = arrow.now() 40 | self.dataclose = self.datas[0].close 41 | self.order = None 42 | 43 | midband = bt.indicators.MovingAverageSimple(self.data, period=self.params.bollwindow, plot=False) 44 | std = bt.indicators.StandardDeviation(self.data, period=self.params.bollwindow, plot=False) 45 | topband = midband + self.params.devfactor * std 46 | botband = midband - self.params.devfactor * std 47 | volatile = abs(self.data.open / midband - 1) 48 | low_volatile = bt.If(volatile < 0.05, 1, 0) 49 | bias = abs(self.data.close / midband - 1) 50 | low_bias = bt.If(bias <= self.params.bias_pct, 1, 0) 51 | crossuptop = bt.indicators.CrossUp(self.data.close, topband, plot=False) 52 | self.close_long = bt.indicators.CrossDown(self.data.close, midband, plot=False) 53 | crossdownbot = bt.indicators.CrossDown(self.data.close, botband, plot=False) 54 | self.close_short = bt.indicators.CrossUp(self.data.close, midband, plot=False) 55 | self.open_long = bt.And(crossuptop, low_volatile, low_bias) 56 | self.open_short = bt.And(crossdownbot, low_volatile, low_bias) 57 | 58 | def notify_order(self, order): 59 | if order.status in [order.Submitted, order.Accepted]: 60 | # 检查订单执行状态order.status: 61 | # Buy/Sell order submitted/accepted to/by broker 62 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 63 | # 正常流程,无需额外操作 64 | return 65 | 66 | # 检查订单order是否完成 67 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 68 | # 可以修改相关参数,调整进行空头交易 69 | if order.status in [order.Completed]: 70 | if order.isbuy(): 71 | self.log('买单执行BUY EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 72 | % (order.executed.price, order.executed.value, order.executed.comm)) 73 | self.buyprice = order.executed.price 74 | self.buycomm = order.executed.comm 75 | elif order.issell(): 76 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 77 | % (order.executed.price, order.executed.value, order.executed.comm)) 78 | self.sellprice = order.executed.price 79 | self.sellcomm = order.executed.comm 80 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 81 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 82 | 83 | # 检查完成,没有交易中订单(pending order) 84 | self.order = None 85 | 86 | def notify_trade(self, trade): 87 | # 检查交易trade是关闭 88 | if not trade.isclosed: 89 | return 90 | 91 | self.log('交易操盘利润OPERATION PROFIT, 毛利GROSS %.2f, 净利NET %.2f' % 92 | (trade.pnl, trade.pnlcomm)) 93 | 94 | def next(self): 95 | if self.order: 96 | return 97 | if self.open_long and not self.position: 98 | self.log('设置多单 BUY CREATE, @ %.2f' % self.dataclose[0]) 99 | self.order = self.order_target_percent(target=0.99) 100 | if self.close_long and self.position: 101 | self.log('设置平多单 CLOSE LONG CREATE, @ %.2f' % self.dataclose[0]) 102 | self.order = self.order_target_percent(target=0.00) 103 | if self.open_short and not self.position: 104 | self.log('设置空单 BUY CREATE, @ %.2f' % self.dataclose[0]) 105 | self.order = self.order_target_percent(target=-0.99) 106 | if self.close_short and self.position: 107 | self.log('设置平空单 CLOSE LONG CREATE, @ %.2f' % self.dataclose[0]) 108 | self.order = self.order_target_percent(target=0.00) 109 | 110 | def stop(self): 111 | # 新增加一个stop策略完成函数 112 | # 用于输出执行后数据 113 | self.log('(策略参数 bollwindow=%2d, devfactor=%.2f) ,最终资产总值: %.2f' % 114 | (self.params.bollwindow, self.params.devfactor, self.broker.getvalue()), fgPrint=True) 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /strategies/ConnorRSI.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import backtrader as bt 3 | import pandas as pd 4 | import sqlalchemy 5 | import setup_psql_environment 6 | from models import Security, SecurityPrice 7 | 8 | 9 | class Streak(bt.ind.PeriodN): 10 | ''' 11 | Keeps a counter of the current upwards/downwards/neutral streak 12 | ''' 13 | lines = ('streak',) 14 | params = dict(period=2) # need prev/cur days (2) for comparisons 15 | 16 | curstreak = 0 17 | 18 | def next(self): 19 | d0, d1 = self.data[0], self.data[-1] 20 | 21 | if d0 > d1: 22 | self.l.streak[0] = self.curstreak = max(1, self.curstreak + 1) 23 | elif d0 < d1: 24 | self.l.streak[0] = self.curstreak = min(-1, self.curstreak - 1) 25 | else: 26 | self.l.streak[0] = self.curstreak = 0 27 | 28 | 29 | class ConnorsRSI(bt.Indicator): 30 | """ 31 | Calculates the ConnorsRSI as: 32 | - (RSI(per_rsi) + RSI(Streak, per_streak) + PctRank(per_rank)) / 3 33 | """ 34 | lines = ('crsi',) 35 | params = dict(prsi=3, pstreak=2, prank=100) 36 | 37 | def __init__(self): 38 | # Calculate the components 39 | rsi = bt.ind.RSI(self.data, period=self.p.prsi) 40 | streak = Streak(self.data) 41 | rsi_streak = bt.ind.RSI(streak.data, period=self.p.pstreak) 42 | prank = bt.ind.PercentRank(self.data, period=self.p.prank) 43 | # Apply the formula 44 | self.l.crsi = (rsi + rsi_streak + prank) / 3.0 45 | 46 | 47 | class MyStrategy(bt.Strategy): 48 | def __init__(self): 49 | self.myind = ConnorsRSI() 50 | 51 | def next(self): 52 | if self.myind.crsi[0] <= 10: 53 | self.buy() 54 | elif self.myind.crsi[0] >= 90: 55 | self.sell() -------------------------------------------------------------------------------- /strategies/DonchainChannels.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import backtrader as bt 3 | 4 | 5 | class DonchianChannels(bt.Indicator): 6 | """ 7 | Params Note: 8 | - `lookback` (default: -1) 9 | If `-1`, the bars to consider will start 1 bar in the past and the 10 | current high/low may break through the channel. 11 | If `0`, the current prices will be considered for the Donchian 12 | Channel. This means that the price will **NEVER** break through the 13 | upper/lower channel bands. 14 | """ 15 | alias = ('DCH', 'DonchianChannel',) 16 | lines = ('dcm', 'dch', 'dcl',) # dc middle, dc high, dc low 17 | params = dict( 18 | period=20, 19 | lookback=-1, # consider current bar or not 20 | ) 21 | plotinfo = dict(subplot=False) # plot along with data 22 | plotlines = dict( 23 | dcm=dict(ls='--'), # dashed line 24 | dch=dict(_samecolor=True), # use same color as prev line (dcm) 25 | dcl=dict(_samecolor=True), # use same color as prev line (dch) 26 | ) 27 | 28 | def __init__(self): 29 | hi, lo = self.data.high, self.data.low 30 | if self.p.lookback: # move backwards as needed 31 | hi, lo = hi(self.p.lookback), lo(self.p.lookback) 32 | 33 | self.l.dch = bt.ind.Highest(hi, period=self.p.period) 34 | self.l.dcl = bt.ind.Lowest(lo, period=self.p.period) 35 | self.l.dcm = (self.l.dch + self.l.dcl) / 2.0 # avg of the above 36 | 37 | 38 | class MyStrategy(bt.Strategy): 39 | def __init__(self): 40 | self.myind = DonchianChannels() 41 | 42 | def next(self): 43 | if self.data[0] > self.myind.dch[0] and not self.position: 44 | self.buy() 45 | elif self.data[0] < self.myind.dcl[0] and self.position: 46 | self.sell() 47 | 48 | -------------------------------------------------------------------------------- /strategies/DualMA.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import arrow 3 | 4 | # 均线交叉策略 5 | class DualMA(bt.Strategy): 6 | params = (('nfast', 10), ('nslow', 30), ('fgPrint', False),) 7 | 8 | # def log(self, txt, dt=None, fgPrint=False): 9 | # ''' Logging function fot this strategy''' 10 | # if self.params.fgPrint or fgPrint: 11 | # dt = dt or self.datas[0].datetime.date(0) 12 | # tn = tk.timNSec('', self.tim0wrk) 13 | # # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 14 | # print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 15 | 16 | def __init__(self, vdict={}): 17 | # To keep track of pending orders and buy price/commission 18 | self.order = None 19 | self.buyprice = None 20 | self.buycomm = None 21 | self.tim0wrk = arrow.now() 22 | # 23 | # self.dataclose = self.datas[0].close 24 | # print('@@vdict',vdict) 25 | # 26 | if len(vdict) > 0: 27 | self.p.nfast = int(vdict.get('nfast')) 28 | self.p.nslow = int(vdict.get('nslow')) 29 | 30 | # 31 | sma_fast, sma_slow, self.buysig = {}, {}, {} 32 | for xc, xdat in enumerate(self.datas): 33 | sma_fast[xc] = bt.ind.SMA(xdat, period=self.p.nfast) 34 | # sma_fast[xc] =bt.ema.SMA(xdat,period=self.p.nfast) 35 | sma_slow[xc] = bt.ind.SMA(xdat, period=self.p.nslow) 36 | self.buysig[xc] = bt.ind.CrossOver(sma_fast[xc], sma_slow[xc]) 37 | # 38 | 39 | def next(self): 40 | # self.log('Close, %.2f' % self.dataclose[0]) 41 | # 42 | if self.order: 43 | return 44 | 45 | # 46 | for xc, xdat in enumerate(self.datas): 47 | xnam = xdat._name 48 | fgInx = xnam.find('inx_') >= 0 49 | # print('@n',xnam,fgInx) 50 | if not fgInx: 51 | xpos = self.getposition(xdat) 52 | # xnam=xdat.line.name 53 | xss = ' {:.02},@ : {} # {}'.format(xdat.close[0], xnam, xc) 54 | if xpos.size: 55 | # if (self.buysig[xc] < 0)and(self.buysig[0] < 0): 56 | if (self.buysig[xc] < 0): 57 | self.log('SELL' + xss) 58 | # self.log(' @@SELL CREATE, %.2f, %' % (self.datas[xc].close[0],self.data0_Name)) 59 | self.sell(data=xdat) 60 | 61 | # elif (self.buysig[xc] > 0)and(self.buysig[0] > 0): 62 | elif (self.buysig[xc] > 0): 63 | self.log('BUY' + xss) 64 | # self.log(' @@buy CREATE, %.2f' % (self.datas[xc].close[0])) 65 | # self.log(' @@buy CREATE, %.2f, %' % (self.datas[xc].close[0],self.datas[xc].Name)) 66 | self.buy(data=xdat) 67 | # 68 | # self.order = self.sell(data=xdat,exectype=bt.Order.StopTrail,trailpercent=0.0618) 69 | 70 | def stop(self): 71 | # tn=arrow.now()-self.tim0wrk 72 | self.log('策略参数 nfast= %2d, nslow= %2d,资产总值:%.2f' % 73 | (self.p.nfast, self.p.nslow, self.broker.getvalue()), fgPrint=True) 74 | -------------------------------------------------------------------------------- /strategies/DualMASign.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import backtrader as bt 3 | 4 | 5 | class DualMASign(bt.SignalStrategy): 6 | def __init__(self): 7 | sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=20) 8 | crossover = bt.ind.CrossOver(sma1, sma2) 9 | self.signal_add(bt.SIGNAL_LONG, crossover) 10 | -------------------------------------------------------------------------------- /strategies/MovingAverage.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | 3 | # 创建一个:最简单的MA均线策略类class 4 | class MovingAverage(bt.Strategy): 5 | # 定义MA均线策略的周期参数变量,默认值是15 6 | # 增加类一个log打印开关变量: fgPrint,默认自是关闭 7 | params = (('period', 15), ('fgPrint', False),) 8 | 9 | def log(self, txt, dt=None, fgPrint=False): 10 | # 增强型log记录函数,带fgPrint打印开关变量 11 | if self.params.fgPrint or fgPrint: 12 | dt = dt or self.datas[0].datetime.date(0) 13 | print('%s, %s' % (dt.isoformat(), txt)) 14 | 15 | def __init__(self, vdict={}): 16 | # 默认数据,一般使用股票池当中,下标为0的股票, 17 | # 通常使用close收盘价,作为主要分析数据字段 18 | self.dataclose = self.datas[0].close 19 | # 20 | # 21 | if len(vdict) > 0: 22 | self.p.period = int(vdict.get('period')) 23 | # 跟踪track交易中的订单(pending orders),成交价格,佣金费用 24 | self.order = None 25 | self.buyprice = None 26 | self.buycomm = None 27 | 28 | # 增加一个均线指标:indicator 29 | # 注意,参数变量period,主要在这里使用 30 | self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], period=self.params.period) 31 | 32 | def notify_order(self, order): 33 | if order.status in [order.Submitted, order.Accepted]: 34 | # 检查订单执行状态order.status: 35 | # Buy/Sell order submitted/accepted to/by broker 36 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 37 | # 正常流程,无需额外操作 38 | return 39 | # 检查订单order是否完成 40 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 41 | # 可以修改相关参数,调整进行空头交易 42 | if order.status in [order.Completed]: 43 | if order.isbuy(): 44 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 45 | % (order.executed.price, order.executed.value, order.executed.comm)) 46 | self.buyprice = order.executed.price 47 | self.buycomm = order.executed.comm 48 | elif order.issell(): 49 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 50 | % (order.executed.price, order.executed.value, order.executed.comm)) 51 | 52 | self.bar_executed = len(self) 53 | 54 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 55 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 56 | 57 | # 检查完成,没有交易中订单(pending order) 58 | self.order = None 59 | 60 | def notify_trade(self, trade): 61 | # 检查交易trade是关闭 62 | if not trade.isclosed: 63 | return 64 | 65 | self.log('交易操盘利润OPERATION PROFIT, 毛利GROSS %.2f, 净利NET %.2f' % 66 | (trade.pnl, trade.pnlcomm)) 67 | 68 | def next(self): 69 | # next函数是最重要的trade交易(运算分析)函数, 70 | # 调用log函数,输出BT回溯过程当中,工作节点数据包BAR,对应的close收盘价 71 | self.log('当前收盘价Close, %.2f' % self.dataclose[0]) 72 | # 73 | # 74 | # 检查订单执行情况,默认每次只能执行一张order订单交易,可以修改相关参数,进行调整 75 | if self.order: 76 | return 77 | # 78 | # 检查当前股票的仓位position 79 | if not self.position: 80 | # 81 | # 如果该股票仓位为0 ,可以进行BUY买入操作, 82 | # 这个仓位设置模式,也可以修改相关参数,进行调整 83 | # 84 | # 使用最简单的MA均线策略 85 | if self.dataclose[0] > self.sma[0]: 86 | # 如果当前close收盘价>当前的ma均价 87 | # ma均线策略,买入信号成立: 88 | # BUY, BUY, BUY!!!,买!买!买!使用默认参数交易:数量、佣金等 89 | self.log('设置买单 BUY CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 90 | 91 | # 采用track模式,设置order订单,回避第二张订单2nd order,连续交易问题 92 | self.order = self.buy() 93 | else: 94 | # 如果该股票仓位>0 ,可以进行SELL卖出操作, 95 | # 这个仓位设置模式,也可以修改相关参数,进行调整 96 | # 使用最简单的MA均线策略 97 | if self.dataclose[0] < self.sma[0]: 98 | # 如果当前close收盘价<当前的ma均价 99 | # ma均线策略,卖出信号成立: 100 | # 默认卖出该股票全部数额,使用默认参数交易:数量、佣金等 101 | self.log('SELL CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 102 | 103 | # 采用track模式,设置order订单,回避第二张订单2nd order,连续交易问题 104 | self.order = self.sell() 105 | 106 | def stop(self): 107 | # 新增加一个stop策略完成函数 108 | # 用于输出执行后带数据 109 | self.log('(策略参数 Period=%2d) ,最终资产总值: %.2f' % 110 | (self.params.period, self.broker.getvalue()), fgPrint=True) 111 | -------------------------------------------------------------------------------- /strategies/RSI.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | from datetime import datetime 3 | 4 | 5 | class RSI(bt.Strategy): 6 | 7 | def __init__(self): 8 | self.rsi = bt.indicators.RSI_SMA(self.data.close, period=21) 9 | 10 | def next(self): 11 | if not self.position: 12 | if self.rsi < 30: 13 | self.buy(size=100) 14 | else: 15 | if self.rsi > 70: 16 | self.sell(size=100) -------------------------------------------------------------------------------- /strategies/SimpleBollinger.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import functions.toolkit as tk 3 | from datetime import datetime 4 | import arrow 5 | 6 | 7 | class SimpleBollinger(bt.Strategy): 8 | """ 9 | 简单布林线策略 10 | 价格突破上轨做多,下穿中轨平多 11 | 价格突破下轨做空,上穿中轨平空 12 | """ 13 | params = dict(bollwindow=200, devfactor=2) 14 | 15 | def log(self, txt, dt=None, fgPrint=False): 16 | # 增强型log记录函数,带fgPrint打印开关变量 17 | if fgPrint: 18 | dt = dt or self.datas[0].datetime.date(0) 19 | tn = tk.timNSec('', self.tim0wrk) 20 | # print('%s, %s,tn:%.2f' % (dt.isoformat(), txt)) 21 | print('%s, %s,tim:%.2f s' % (dt.isoformat(), txt, tn)) 22 | 23 | def __init__(self): 24 | self.buyprice = None 25 | self.buycomm = None 26 | self.sellprice = None 27 | self.sellcomm = None 28 | self.tim0wrk = arrow.now() 29 | self.dataclose = self.datas[0].close 30 | self.order = None 31 | 32 | midband = bt.indicators.MovingAverageSimple(self.data, period=self.params.bollwindow) 33 | std = bt.indicators.StandardDeviation(self.data, period=self.params.bollwindow, plot=False) 34 | topband = midband + self.params.devfactor * std 35 | botband = midband - self.params.devfactor * std 36 | self.open_short = bt.indicators.CrossUp(self.data.close, topband, plot=False) 37 | self.close_long = bt.indicators.CrossDown(self.data.close, midband, plot=False) 38 | self.open_long = bt.indicators.CrossDown(self.data.close, botband, plot=False) 39 | self.close_short = bt.indicators.CrossUp(self.data.close, midband, plot=False) 40 | 41 | def notify_order(self, order): 42 | if order.status in [order.Submitted, order.Accepted]: 43 | # 检查订单执行状态order.status: 44 | # Buy/Sell order submitted/accepted to/by broker 45 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 46 | # 正常流程,无需额外操作 47 | return 48 | 49 | # 检查订单order是否完成 50 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 51 | # 可以修改相关参数,调整进行空头交易 52 | if order.status in [order.Completed]: 53 | if order.isbuy(): 54 | self.log('买单执行BUY EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 55 | % (order.executed.price, order.executed.value, order.executed.comm)) 56 | self.buyprice = order.executed.price 57 | self.buycomm = order.executed.comm 58 | elif order.issell(): 59 | self.log('卖单执行SELL EXECUTED,成交价: %.2f,小计 Cost: %.2f,佣金 Comm %.2f' 60 | % (order.executed.price, order.executed.value, order.executed.comm)) 61 | self.sellprice = order.executed.price 62 | self.sellcomm = order.executed.comm 63 | 64 | self.bar_executed = len(self) 65 | 66 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 67 | self.log('订单Order: 取消Canceled/保证金Margin/拒绝Rejected') 68 | 69 | # 检查完成,没有交易中订单(pending order) 70 | self.order = None 71 | 72 | def notify_trade(self, trade): 73 | # 检查交易trade是关闭 74 | if not trade.isclosed: 75 | return 76 | 77 | self.log('交易操盘利润OPERATION PROFIT, 毛利GROSS %.2f, 净利NET %.2f' % 78 | (trade.pnl, trade.pnlcomm)) 79 | 80 | def next(self): 81 | if self.order: 82 | return 83 | pfl = self.broker.get_cash() 84 | if self.open_long and not self.position: 85 | self.log('设置多单 BUY CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 86 | self.order = self.order_target_percent(target=0.99) 87 | if self.close_long and self.position: 88 | self.log('设置平多单 CLOSE LONG CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 89 | self.order = self.order_target_percent(target=0) 90 | if self.open_short and not self.position: 91 | self.log('设置空单 BUY CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 92 | self.order = self.order_target_percent(target=-0.99) 93 | if self.close_short and self.position: 94 | self.log('设置平空单 CLOSE LONG CREATE, %.2f, name : %s' % (self.dataclose[0], self.datas[0]._name)) 95 | self.order = self.order_target_percent(target=0) 96 | 97 | def stop(self): 98 | # 新增加一个stop策略完成函数 99 | # 用于输出执行后数据 100 | self.log('(策略参数 bollwindow=%2d, devfactor=%.2f) ,最终资产总值: %.2f' % 101 | (self.params.bollwindow, self.params.devfactor, self.broker.getvalue()), fgPrint=True) 102 | -------------------------------------------------------------------------------- /strategies/Swing.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | 3 | 4 | class SwingInd(bt.Indicator): 5 | ''' 6 | A Simple swing indicator that measures swings (the lowest/highest value) 7 | within a given time period. 8 | ''' 9 | lines = ('swings', 'signal') 10 | params = (('period', 7),) 11 | 12 | def __init__(self): 13 | 14 | # Set the swing range - The number of bars before and after the swing 15 | # needed to identify a swing 16 | self.swing_range = (self.p.period * 2) + 1 17 | self.addminperiod(self.swing_range) 18 | 19 | def next(self): 20 | # Get the highs/lows for the period 21 | highs = self.data.high.get(size=self.swing_range) 22 | lows = self.data.low.get(size=self.swing_range) 23 | # check the bar in the middle of the range and check if greater than rest 24 | if highs.pop(self.p.period) > max(highs): 25 | self.lines.swings[-self.p.period] = 1 # add new swing 26 | self.lines.signal[0] = 1 # give a signal 27 | elif lows.pop(self.p.period) < min(lows): 28 | self.lines.swings[-self.p.period] = -1 # add new swing 29 | self.lines.signal[0] = -1 # give a signal 30 | else: 31 | self.lines.swings[-self.p.period] = 0 32 | self.lines.signal[0] = 0 33 | -------------------------------------------------------------------------------- /strategies/TrendLine.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | from datetime import datetime 3 | import time 4 | 5 | 6 | class TrendLine(bt.Indicator): 7 | ''' 8 | This indicator shall produce a signal when price reaches a calculated trend line. 9 | 10 | The indicator requires two price points and date points to serve as X and Y 11 | values in calcuating the slope and the future expected price trend 12 | 13 | x1 = Date/Time, String in the following format "YYYY-MM-DD HH:MM:SS" of 14 | the start of the trend 15 | y1 = Float, the price (Y value) of the start of the trend. 16 | x2 = Date/Time, String in the following format "YYYY-MM-DD HH:MM:SS" of 17 | the end of the trend 18 | y2 = Float, the price (Y value) of the end of the trend. 19 | ''' 20 | 21 | lines = ('signal', 'trend') 22 | params = ( 23 | ('x1', None), 24 | ('y1', None), 25 | ('x2', None), 26 | ('y2', None) 27 | ) 28 | 29 | def __init__(self): 30 | self.p.x1 = datetime.datetime.strptime(self.p.x1, "%Y-%m-%d %H:%M:%S") 31 | self.p.x2 = datetime.datetime.strptime(self.p.x2, "%Y-%m-%d %H:%M:%S") 32 | x1_time_stamp = time.mktime(self.p.x1.timetuple()) 33 | x2_time_stamp = time.mktime(self.p.x2.timetuple()) 34 | self.m = self.get_slope(x1_time_stamp, x2_time_stamp, self.p.y1, self.p.y2) 35 | self.B = self.get_y_intercept(self.m, x1_time_stamp, self.p.y1) 36 | self.plotlines.trend._plotskip = True 37 | 38 | def next(self): 39 | date = self.data0.datetime.datetime() 40 | date_timestamp = time.mktime(date.timetuple()) 41 | Y = self.get_y(date_timestamp) 42 | self.lines.trend[0] = Y 43 | 44 | # Check if price has crossed up / down into it. 45 | if self.data0.high[-1] < Y and self.data0.high[0] > Y: 46 | self.lines.signal[0] = -1 47 | return 48 | 49 | # Check for cross downs (Into support) 50 | elif self.data0.low[-1] > Y and self.data0.low[0] < Y: 51 | self.lines.signal[0] = 1 52 | return 53 | 54 | else: 55 | self.lines.signal[0] = 0 56 | 57 | def get_slope(self, x1, x2, y1, y2): 58 | m = (y2 - y1) / (x2 - x1) 59 | return m 60 | 61 | def get_y_intercept(self, m, x1, y1): 62 | b = y1 - m * x1 63 | return b 64 | 65 | def get_y(self, ts): 66 | Y = self.m * ts + self.B 67 | return Y 68 | -------------------------------------------------------------------------------- /strategies/VolatileBoll.py: -------------------------------------------------------------------------------- 1 | import datetime # For datetime objects 2 | import os.path # To manage paths 3 | import sys # To find out the script name (in argv[0]) 4 | 5 | # Import the backtrader platform 6 | import backtrader as bt 7 | 8 | 9 | # Create a Stratey 10 | class VolatileBoll(bt.Strategy): 11 | params = (('BBandsperiod', 20),) 12 | 13 | def log(self, txt, dt=None): 14 | ''' Logging function fot this strategy''' 15 | dt = dt or self.datas[0].datetime.date(0) 16 | print('%s, %s' % (dt.isoformat(), txt)) 17 | 18 | def __init__(self): 19 | # Keep a reference to the "close" line in the data[0] dataseries 20 | self.dataclose = self.datas[0].close 21 | 22 | # To keep track of pending orders and buy price/commission 23 | self.order = None 24 | self.buyprice = None 25 | self.buycomm = None 26 | self.redline = None 27 | self.blueline = None 28 | 29 | # Add a BBand indicator 30 | self.bband = bt.indicators.BBands(self.datas[0], period=self.params.BBandsperiod) 31 | 32 | # def notify_order(self, order): 33 | # if order.status in [order.Submitted, order.Accepted]: 34 | # # Buy/Sell order submitted/accepted to/by broker - Nothing to do 35 | # return 36 | # 37 | # # Check if an order has been completed 38 | # # Attention: broker could reject order if not enougth cash 39 | # if order.status in [order.Completed, order.Canceled, order.Margin]: 40 | # if order.isbuy(): 41 | # self.log( 42 | # 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 43 | # (order.executed.price, 44 | # order.executed.value, 45 | # order.executed.comm)) 46 | # 47 | # self.buyprice = order.executed.price 48 | # self.buycomm = order.executed.comm 49 | # else: # Sell 50 | # self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 51 | # (order.executed.price, 52 | # order.executed.value, 53 | # order.executed.comm)) 54 | # 55 | # self.bar_executed = len(self) 56 | # 57 | # # Write down: no pending order 58 | # self.order = None 59 | # 60 | # def notify_trade(self, trade): 61 | # if not trade.isclosed: 62 | # return 63 | # 64 | # self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % 65 | # (trade.pnl, trade.pnlcomm)) 66 | # 67 | def next(self): 68 | # Simply log the closing price of the series from the reference 69 | # self.log('Close, %.2f' % self.dataclose[0]) 70 | 71 | # Check if an order is pending ... if yes, we cannot send a 2nd one 72 | if self.order: 73 | return 74 | 75 | if self.dataclose < self.bband.lines.bot and not self.position: 76 | self.redline = True 77 | 78 | if self.dataclose > self.bband.lines.top and self.position: 79 | self.blueline = True 80 | 81 | if self.dataclose > self.bband.lines.mb and not self.position and self.redline: 82 | # BUY, BUY, BUY!!! (with all possible default parameters) 83 | self.log('BUY CREATE, %.2f' % self.dataclose[0]) 84 | # Keep track of the created order to avoid a 2nd order 85 | self.order = self.buy() 86 | 87 | if self.dataclose > self.bband.lines.top and not self.position: 88 | # BUY, BUY, BUY!!! (with all possible default parameters) 89 | self.log('BUY CREATE, %.2f' % self.dataclose[0]) 90 | # Keep track of the created order to avoid a 2nd order 91 | self.order = self.buy() 92 | 93 | if self.dataclose < self.bband.lines.mb and self.position and self.blueline: 94 | # SELL, SELL, SELL!!! (with all possible default parameters) 95 | self.log('SELL CREATE, %.2f' % self.dataclose[0]) 96 | self.blueline = False 97 | self.redline = False 98 | # Keep track of the created order to avoid a 2nd order 99 | self.order = self.sell() 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /strategies/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | import backtrader as bt 5 | from termcolor import colored 6 | from config import DEVELOPMENT, COIN_TARGET, COIN_REFER, ENV, PRODUCTION, DEBUG 7 | from utils import send_telegram_message 8 | 9 | 10 | class StrategyBase(bt.Strategy): 11 | def __init__(self): 12 | # # Set some pointers / references 13 | # for i, d in enumerate(self.datas): 14 | # if d._name == 'Kline': 15 | # self.kl = d 16 | # elif d._name == 'Heikin': 17 | # self.ha = d 18 | 19 | self.buyprice = None 20 | self.buycomm = None 21 | self.sellprice = None 22 | self.sellcomm = None 23 | 24 | self.order = None 25 | self.last_operation = dict() 26 | self.status = "DISCONNECTED" 27 | self.bar_executed = 0 28 | 29 | self.buy_price_close = dict() 30 | self.sell_price_close = dict() 31 | 32 | self.soft_sell = False 33 | self.hard_sell = False 34 | self.soft_buy = False 35 | self.hard_buy = False 36 | 37 | self.profit = dict() 38 | 39 | self.log("Base strategy initialized", fgprint=True) 40 | 41 | def reset_order_indicators(self): 42 | self.soft_sell = False 43 | self.hard_sell = False 44 | self.buy_price_close = dict() 45 | self.sell_price_close = dict() 46 | self.soft_buy = False 47 | self.hard_buy = False 48 | 49 | def notify_data(self, data, status, *args, **kwargs): 50 | self.status = data._getstatusname(status) 51 | print(self.status) 52 | if status == data.LIVE: 53 | self.log("LIVE DATA - Ready to trade", fgprint=True) 54 | 55 | def notify_order(self, order): 56 | # StrategyBase.notify_order(self, order) 57 | if order.status in [order.Submitted, order.Accepted]: 58 | # 检查订单执行状态order.status: 59 | # Buy/Sell order submitted/accepted to/by broker 60 | # broker经纪人:submitted提交/accepted接受,Buy买单/Sell卖单 61 | # Buy/Sell order submitted/accepted to/by broker - Nothing to do 62 | self.log('ORDER ACCEPTED/SUBMITTED', fgprint=True) 63 | self.order = order 64 | return 65 | 66 | if order.status in [order.Expired]: 67 | self.log('LONG EXPIRED', send_telegram=False, fgprint=True) 68 | 69 | elif order.status in [order.Canceled, order.Margin, order.Rejected]: 70 | # return 71 | for i, d in enumerate(self.datas): 72 | self.log('Order Canceled/Margin/Rejected: Status %s - %s' % (order.Status[order.status], 73 | self.last_operation[d]), send_telegram=False, 74 | fgprint=True) 75 | 76 | def short(self, data=None): 77 | if self.last_operation[data] == "short": 78 | return 79 | if isinstance(data, str): 80 | data = self.getdatabyname(data) 81 | data = data if data is not None else self.datas[0] 82 | self.sell_price_close[data] = data.close[0] 83 | price = data.close[0] 84 | 85 | if ENV == DEVELOPMENT: 86 | self.log("open short ordered: $%.2f" % data.close[0], fgprint=True) 87 | return self.sell(data=data) 88 | 89 | cash, value = self.broker.get_wallet_balance(COIN_REFER) 90 | # print(cash, ' ', value) 91 | amount = (value / price) * 0.99 92 | self.log("open short ordered: $%.2f. Amount %.6f %s. Balance $%.2f USDT" % (data.close[0], 93 | amount, COIN_TARGET, value), 94 | send_telegram=False, fgprint=True) 95 | return self.sell(data=data, size=amount) 96 | 97 | def long(self, data=None): 98 | if self.last_operation[data] == "long": 99 | return 100 | if isinstance(data, str): 101 | data = self.getdatabyname(data) 102 | data = data if data is not None else self.datas[0] 103 | # self.log("Buy ordered: $%.2f" % self.data0.close[0], True) 104 | self.buy_price_close[data] = data.close[0] 105 | price = data.close[0] 106 | 107 | if ENV == DEVELOPMENT: 108 | self.log("open long ordered: $%.2f" % data.close[0], fgprint=True) 109 | return self.buy(data=data) 110 | 111 | cash, value = self.broker.get_wallet_balance(COIN_REFER) 112 | amount = (value / price) * 0.99 # Workaround to avoid precision issues 113 | self.log("open long ordered: $%.2f. Amount %.6f %s. Balance $%.2f USDT" % (data.close[0], 114 | amount, COIN_TARGET, value), 115 | send_telegram=False, fgprint=True) 116 | return self.buy(data=data, size=amount) 117 | 118 | def close_long(self, data=None): 119 | if isinstance(data, str): 120 | data = self.getdatabyname(data) 121 | elif data is None: 122 | data = self.data 123 | if self.last_operation[data] == "long": 124 | if ENV == DEVELOPMENT: 125 | self.log("close long ordered: $%.2f" % data.close[0], fgprint=True) 126 | return self.close(data=data) 127 | self.log("close long ordered: $%.2f" % data.close[0], send_telegram=False, fgprint=True) 128 | return self.close(data=data) 129 | 130 | def close_short(self, data=None): 131 | if isinstance(data, str): 132 | data = self.getdatabyname(data) 133 | elif data is None: 134 | data = self.data 135 | if self.last_operation[data] == "short": 136 | if ENV == DEVELOPMENT: 137 | self.log("close short ordered: $%.2f" % data.close[0], fgprint=True) 138 | return self.close(data=data) 139 | self.log("close short ordered: $%.2f" % data.close[0], send_telegram=False, fgprint=True) 140 | return self.close(data=data) 141 | 142 | def notify_trade(self, trade): 143 | if not trade.isclosed: 144 | return 145 | 146 | color = 'green' 147 | if trade.pnl < 0: 148 | color = 'red' 149 | 150 | self.log(colored('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm), color), 151 | send_telegram=False, 152 | fgprint=True) 153 | 154 | def log(self, txt, send_telegram=False, color=None, fgprint=True): 155 | if fgprint: 156 | value = datetime.now() 157 | if len(self) > 0: 158 | value = self.kl.datetime.datetime() 159 | if color: 160 | txt = colored(txt, color) 161 | print('[%s] %s' % (value.strftime("%d-%m-%y %H:%M"), txt)) 162 | if send_telegram: 163 | send_telegram_message(txt) 164 | -------------------------------------------------------------------------------- /strategies/draft.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | import backtrader as bt 4 | 5 | 6 | class BBSqueeze(bt.Indicator): 7 | """ 8 | https://www.netpicks.com/squeeze-out-the-chop/ 9 | Both indicators are symmetrical, meaning that the upper and lower bands or channel lines are the same distance from the moving average. That means that 10 | we can focus on only one side in developing our indicator. In our case, we’ll just consider the upper lines. 11 | The basic formulas we need are: 12 | Bollinger Band = Moving Average + (Number of standard deviations X Standard Deviation) 13 | Keltner Channel = Moving Average + (Number of ATR’s X ATR) 14 | Or if we translate this into pseudo-code: 15 | BBUpper = Avg(close, period) + (BBDevs X StdDev(close, period)) 16 | KCUpper = Avg(close, period) + (KCDevs X ATR(period)) 17 | The squeeze is calculated by taking the difference between these two values: 18 | Squeeze = BBUpper – KCUpper 19 | Which simplifies down to this: 20 | Squeeze = (BBDevs X StdDev(close, period)) – (KCDevs X ATR(period)) 21 | """ 22 | 23 | lines = ('squeeze',) 24 | params = (('period', 20), ('bbdevs', 2.0), ('kcdevs', 1.5), ('movav', bt.ind.MovAv.Simple),) 25 | plotinfo = dict(subplot=True) 26 | 27 | def _plotlabel(self): 28 | plabels = [self.p.period, self.p.bbdevs, self.p.kcdevs] 29 | plabels += [self.p.movav] * self.p.notdefault('movav') 30 | return plabels 31 | 32 | def __init__(self): 33 | bb = bt.ind.BollingerBands( 34 | period=self.p.period, devfactor=self.p.bbdevs, movav=self.p.movav) 35 | kc = KeltnerChannel( 36 | period=self.p.period, devfactor=self.p.kcdevs, movav=self.p.movav) 37 | self.lines.squeeze = bb.top - kc.top 38 | 39 | 40 | class KeltnerChannel(bt.Indicator): 41 | 42 | lines = ('mb', 'top', 'bot',) 43 | params = (('period', 20), ('devfactor', 1.5), 44 | ('movav', bt.ind.MovAv.Simple),) 45 | 46 | plotinfo = dict(subplot=False) 47 | plotlines = dict( 48 | mid=dict(ls='--'), 49 | top=dict(_samecolor=True), 50 | bot=dict(_samecolor=True), 51 | ) 52 | 53 | def _plotlabel(self): 54 | plabels = [self.p.period, self.p.devfactor] 55 | plabels += [self.p.movav] * self.p.notdefault('movav') 56 | return plabels 57 | 58 | def __init__(self): 59 | self.lines.mid = ma = self.p.movav(self.data, period=self.p.period) 60 | atr = self.p.devfactor * bt.ind.ATR(self.data, period=self.p.period) 61 | self.lines.top = ma + atr 62 | self.lines.bot = ma - atr -------------------------------------------------------------------------------- /strategies/longshort.py: -------------------------------------------------------------------------------- 1 | from sklearn.model_selection import TimeSeriesSplit 2 | from sklearn.utils import indexable 3 | from sklearn.utils.validation import _num_samples 4 | import numpy as np 5 | import backtrader as bt 6 | import backtrader.indicators as btind 7 | import datetime as dt 8 | import pandas as pd 9 | import pandas_datareader as web 10 | from pandas import Series, DataFrame 11 | import random 12 | from copy import deepcopy 13 | import optunity.metrics 14 | import backtrader.analyzers as btanal 15 | 16 | 17 | class long_short(bt.Strategy): 18 | """A simple moving average crossover strategy; crossing of a fast and slow moving average generates buy/sell 19 | signals""" 20 | params = dict(var1=20, var2=50) # The windows for both var1 (fast) and var2 (slow) moving averages 21 | 22 | def __init__(self): 23 | """Initialize the strategy""" 24 | 25 | self.sma = dict() 26 | self.fastma = dict() 27 | self.slowma = dict() 28 | self.regime = dict() 29 | 30 | for d in self.getdatanames(): 31 | # The moving averages 32 | self.sma[d] = btind.SMA(self.getdatabyname(d), # The symbol for the moving average 33 | period=globalparams['sma_period'], # Fast moving average 34 | plotname="SMA20: " + d) 35 | 36 | def next(self): 37 | """Define what will be done in a single step, including creating and closing trades""" 38 | for d in self.getdatanames(): # Looping through all symbols 39 | pos = self.getpositionbyname(d).size or 0 40 | if pos == 0: # Are we out of the market? 41 | # Consider the possibility of entrance 42 | # Notice the indexing; [0] always means the present bar, and [-1] the bar immediately preceding 43 | # Thus, the condition below translates to: "If today the regime is bullish (greater than 44 | # 0) and yesterday the regime was not bullish" 45 | if self.sma[d][0] * self.params.var1 > self.getdatabyname(d).high[0]: # A buy signal 46 | self.order_target_percent(data=self.getdatabyname(d), target=0.98) 47 | 48 | else: # We have an open position 49 | if self.getdatabyname(d).close[-1] * self.params.var2 <= self.getdatabyname(d).high[0]: # A sell signal 50 | self.order_target_percent(data=self.getdatabyname(d), target=0) 51 | -------------------------------------------------------------------------------- /strategies/study_strategy.py: -------------------------------------------------------------------------------- 1 | import backtrader as bt 2 | import backtrader.indicators as btind 3 | import backtrader.feeds as btfeeds 4 | import math 5 | 6 | 7 | class MyStrategy(bt.Strategy): 8 | params = dict(period1=20, period2=25, period3=10, period4=10) 9 | # tuple format 10 | # params = (('period', 20),) 11 | 12 | def __init__(self): 13 | """ 14 | The self.datas array items can be directly accessed with additional automatic member variables: 15 | self.data targets self.datas[0] 16 | self.dataX targets self.datas[X] 17 | """ 18 | sma1 = btind.SimpleMovingAverage(self.datas[0], period=self.params.period) 19 | # simplified 20 | # sma = btind.SimpleMovingAverage(period=self.params.period) 21 | # This 2nd Moving Average operates using sma1 as "data" 22 | sma2 = btind.SimpleMovingAverage(sma1, period=self.p.period2) 23 | # New data created via arithmetic operation 24 | something = sma2 - sma1 + self.data.close[0] 25 | # This 3rd Moving Average operates using something as "data" 26 | sma3 = btind.SimpleMovingAverage(something, period=self.p.period3) 27 | # Comparison operators work too ... 28 | greater = sma3 > sma1 29 | # Pointless Moving Average of True/False values but valid 30 | # This 4th Moving Average operates using greater as "data" 31 | sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4) 32 | # self.params (shorthand: self.p) 33 | self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period) 34 | # data0 is a daily data added from 'cerebro.adddata' 35 | sma4 = btind.SMA(self.data0, period=15) # 15 days sma 36 | # data1 is a weekly data 37 | sma5 = btind.SMA(self.data1, period=5) 38 | self.buysig = sma4 > sma5() 39 | # Once again a potential implementation of a SimpleMovingAverage, further broken down into steps. 40 | # Sum N period values - datasum is now a *Lines* object 41 | # that when queried with the operator [] and index 0 42 | # returns the current sum 43 | datasum = btind.SumN(self.data, period=self.params.period) 44 | # datasum (being *Lines* object although single line) can be 45 | # naturally divided by an int/float as in this case. It could 46 | # actually be divided by anothr *Lines* object. 47 | # The operation returns an object assigned to "av" which again 48 | # returns the current average at the current instant in time 49 | # when queried with [0] 50 | av = datasum / self.params.period 51 | # The av *Lines* object can be naturally assigned to the named 52 | # line this indicator delivers. Other objects using this 53 | # indicator will have direct access to the calculation 54 | self.lines.sma = av 55 | sma6 = btind.SimpleMovingAverage(self.data, period=20) 56 | sma7 = btind.SimpleMovingAverage(self.data, period=15) 57 | close_over_sma = self.data.close > sma6 58 | sma_dist_to_high = self.data.high - sma6 59 | 60 | sma_dist_small = sma_dist_to_high < 3.5 61 | # Unfortunately "and" cannot be overridden in Python being 62 | # a language construct and not an operator and thus a 63 | # function has to be provided by the platform to emulate it 64 | sell_sig = bt.And(close_over_sma, sma_dist_small) 65 | self.buysig = bt.And(sma7 > self.data.close, sma7 > self.data.high) 66 | 67 | # bt.If the value of the sma is larger than close, return low, else return high 68 | high_or_low = bt.If(sma1 > self.data.close, self.data.low, self.data.high) 69 | sma8 = btind.SMA(high_or_low, period=15) 70 | 71 | def next(self): 72 | if self.movav.lines.sma[0] > self.data.lines.close[0]: 73 | # also valid: if self.movav.lines.sma > self.data.lines.close: 74 | print('Simple Moving Average is greater than the closing price') 75 | # self.data It has a lines attribute which contains a close attribute in turn 76 | # self.movav which is a SimpleMovingAverage indicator It has a lines attribute which contains a sma attribute in turn 77 | # shorthand for accessing line: 78 | # xxx.lines can be shortened to xxx.l 79 | # xxx.lines.name can be shortened to xxx.lines_name 80 | if self.data.close[0] > 30.0: 81 | # also valid: if self.data.lines.close[0] > 30.0: 82 | ... 83 | # A SimpleMovingAverage can be calculated for the current get/set point as follows: 84 | self.line[0] = math.fsum(self.data.get(0, size=self.p.period)) / self.p.period 85 | # the current close to the previous close is a 0 vs -1 thing. 86 | if self.data.close[0] > self.data.close[-1]: 87 | print('Closing price is higher today') 88 | if self.buysig[0]: 89 | print('daily sma is greater than weekly sma1') 90 | # Although this does not seem like an "operator" it actually is 91 | # in the sense that the object is being tested for a True/False 92 | # response 93 | if self.sma6 > 30.0: 94 | print('sma is greater than 30.0') 95 | if self.sma6 > self.data.close: 96 | print('sma is above the close price') 97 | # if self.sma > 30.0: … compares self.sma[0] to 30.0 (1st line and current value) 98 | # if self.sma > self.data.close: … compares self.sma[0] to self.data.close[0] 99 | if self.sell_sig: 100 | print('sell sig is True') 101 | else: 102 | print('sell sig is False') 103 | if self.sma_dist_to_high > 5.0: 104 | print('distance from sma to hig is greater than 5.0') 105 | if self.buysig[0]: 106 | pass # do something 107 | 108 | 109 | class SimpleMovingAverage(bt.Indicator): 110 | lines = ('sma',) 111 | # self.lines[0] points to self.lines.sma 112 | # shorthand 113 | # self.line points to self.lines[0] 114 | # self.lineX point to self.lines[X] 115 | # self.line_X point to self.lines[X] 116 | # self.dataY points to self.data.lines[Y] 117 | # self.dataX_Y points to self.dataX.lines[X] which is a full shorthard version of self.datas[X].lines[Y] 118 | 119 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | { 2 | "params":, 3 | "p":, 4 | "owner":, 5 | "data":, 6 | "ccxt_order":{ 7 | "info":{ 8 | "orderId":"2870948078", 9 | "symbol":"BTCUSDT", 10 | "status":"FILLED", 11 | "clientOrderId":"x-xcKtGhcu43da53cefea1a4695cf86d", 12 | "price":"0", 13 | "avgPrice":"65725.89381", 14 | "origQty":"1.403", 15 | "executedQty":"1.403", 16 | "cumQuote":"92213.42902", 17 | "timeInForce":"GTC", 18 | "type":"MARKET", 19 | "reduceOnly":false, 20 | "closePosition":false, 21 | "side":"SELL", 22 | "positionSide":"BOTH", 23 | "stopPrice":"0", 24 | "workingType":"CONTRACT_PRICE", 25 | "priceProtect":false, 26 | "origType":"MARKET", 27 | "time":"1636951957603", 28 | "updateTime":"1636951957603" 29 | }, 30 | "id":"2870948078", 31 | "clientOrderId":"x-xcKtGhcu43da53cefea1a4695cf86d", 32 | "timestamp":1636951957603, 33 | "datetime":"2021-11-15T04:52:37.603Z", 34 | "lastTradeTimestamp":"None", 35 | "symbol":"BTC/USDT", 36 | "type":"market", 37 | "timeInForce":"GTC", 38 | "postOnly":false, 39 | "side":"sell", 40 | "price":65725.89381, 41 | "stopPrice":"None", 42 | "amount":1.403, 43 | "cost":1.084e-05, 44 | "average":65725.89381, 45 | "filled":1.403, 46 | "remaining":0.0, 47 | "status":"closed", 48 | "fee":"None", 49 | "trades":[ 50 | 51 | ], 52 | "fees":[ 53 | 54 | ] 55 | }, 56 | "executed_fills":[ 57 | 58 | ], 59 | "ordtype":1, 60 | "size":-1.403, 61 | "ref":1, 62 | "broker":"None", 63 | "info":"AutoOrderedDict()", 64 | "comminfo":"None", 65 | "triggered":false, 66 | "_active":true, 67 | "status":4, 68 | "_plimit":"None", 69 | "exectype":0, 70 | "created":, 71 | "_limitoffset":0.0, 72 | "executed":, 73 | "position":0, 74 | "dteos":738108.9999999999, 75 | "price":65725.89381 76 | } -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import backtrader as bt 4 | import yaml 5 | from config import DEVELOPMENT, COIN_TARGET, COIN_REFER, ENV, PRODUCTION, DEBUG 6 | 7 | from strategies.base import StrategyBase 8 | 9 | import toolkit as tk 10 | 11 | 12 | class BasicRSI(StrategyBase): 13 | 14 | def __init__(self): 15 | # 含有Heikin的数据是存储所有策略信息的 16 | StrategyBase.__init__(self) 17 | self.log("Using RSI/EMA strategy", fgprint=False) 18 | # todo 将这个策略参考SMAC和long——short进行修改 19 | 20 | self.params = dict() 21 | self.ind = dict() 22 | 23 | for d in self.getdatanames(): 24 | if 'Heikin' in d: 25 | strategy_params = self.load_params(strategy=self.__class__.__name__.rstrip('_Heikin'), data=d) 26 | # self.params[d] = dict() 27 | self.params[d]['ema_fast_window'] = strategy_params['var1'] 28 | self.params[d]['ema_slow_window'] = strategy_params['var2'] 29 | self.ind[d]['stoploss'] = strategy_params['var3'] 30 | 31 | self.ind[d] = dict() 32 | # 将指标的各项内容放到对应的数据源当中 33 | self.ind[d]['ema_fast'] = bt.indicators.EMA(self.getdatabyname(d), 34 | period=self.params[d]['ema_fast_window'], 35 | plotname="ema_fast: " + d) 36 | self.ind[d]['ema_slow'] = bt.indicators.EMA(self.getdatabyname(d), 37 | period=self.params[d]['ema_slow_window'], 38 | plotname="ema_slow: " + d) 39 | self.ind[d]['rsi'] = bt.indicators.RSI(self.getdatabyname(d)) 40 | 41 | def load_params(self, strategy, data): 42 | with open('../dataset/symbol_config.yaml', 'r') as f: 43 | symbol_config = f.read() 44 | symbol_config = yaml.load(symbol_config, Loader=yaml.FullLoader) 45 | param = data.replace('USDT', f'USDT_{strategy}') 46 | # BNBUSDT_MyStrategy_10m 47 | # param = param.remove('_Kline') 48 | param = param.split('_') 49 | strategy_params = symbol_config[param[0]][param[1]][param[2]] 50 | f.close() 51 | return strategy_params 52 | 53 | def notify_order(self, order): 54 | StrategyBase.notify_order(self, order) 55 | if order.status in [order.Completed]: 56 | # 检查订单order是否完成 57 | # 注意: 如果现金不足,经纪人broker会拒绝订单reject order 58 | # 可以修改相关参数,调整进行空头交易 59 | for d in self.getdatanames(): 60 | if order.isbuy(): 61 | self.last_operation[d] = "long" 62 | self.reset_order_indicators() 63 | if ENV == DEVELOPMENT: 64 | # print(order.executed.__dict__) 65 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 66 | (order.executed.price, 67 | order.executed.value, 68 | order.executed.comm), fgprint=False) 69 | if ENV == PRODUCTION: 70 | print(order.__dict__) 71 | self.log('LONG EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 72 | (order.ccxt_order['average'], 73 | float(order.ccxt_order['info']['cumQuote']), 74 | float(order.ccxt_order['info']['cumQuote']) * 0.0004), send_telegram=False, 75 | fgprint=False) 76 | # tk.save_order(order, strategy=self.__class__.__name__, write=False) 77 | # self.buyprice = order.ccxt_order['average'] 78 | # self.buycomm = order.ccxt_order['cost'] * 0.0004 79 | if ENV == DEVELOPMENT: 80 | print('order info: ', order.__dict__) 81 | if order.issell(): 82 | self.last_operation[d] = "short" 83 | self.reset_order_indicators() 84 | if ENV == DEVELOPMENT: 85 | # print(order.executed.__dict__) 86 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 87 | (order.executed.price, 88 | order.executed.value, 89 | order.executed.comm), fgprint=False) 90 | if ENV == PRODUCTION: 91 | print(order.__dict__) 92 | self.log('SHORT EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % 93 | (order.ccxt_order['average'], 94 | float(order.ccxt_order['info']['cumQuote']), 95 | float(order.ccxt_order['info']['cumQuote']) * 0.0004), send_telegram=False, 96 | fgprint=False) 97 | # tk.save_order(order, strategy=self.__class__.__name__, write=False) 98 | # self.sellprice = order.ccxt_order['average'] 99 | # self.sellcomm = order.ccxt_order['cost'] * 0.0004 100 | if ENV == DEVELOPMENT: 101 | print('order info: ', order.__dict__) 102 | self.bar_executed = len(self) 103 | # Sentinel to None: new orders allowed 104 | self.order = None 105 | 106 | def update_indicators(self): 107 | # self.profit = dict() 108 | for d in self.getdatanames(): 109 | if 'Heikin' in d: 110 | self.profit[d] = 0 111 | # dt, dn = self.datetime.datetime(), d.__name 112 | # pos = self.getposition(d).size 113 | # 利用真实价格来计算PNL 114 | if self.buy_price_close[d] and self.buy_price_close[d] > 0: 115 | self.profit[d] = float(self.getdatabyname(d.rstrip('_Heikin')).close[0] - self.buy_price_close[d]) / \ 116 | self.buy_price_close[d] 117 | if self.sell_price_close[d] and self.sell_price_close[d] > 0: 118 | self.profit[d] = float( 119 | self.sell_price_close[d] - self.getdatabyname(d.rstrip('_Heikin')).close[0]) / \ 120 | self.sell_price_close[d] 121 | 122 | def next(self): 123 | self.update_indicators() 124 | if self.status != "LIVE" and ENV == PRODUCTION: # waiting for live status in production 125 | return 126 | if self.order: # waiting for pending order 127 | return 128 | for d in self.getdatanames(): 129 | if 'Heikin' in d: 130 | # d = d.rstrp('_Heikin') # 以策略_周期这样干干净净的形式来作为其参数的容器,代表真实数据 131 | # 永远以真实数据来进行下单操作 132 | # dt, dn = self.datetime.datetime(), d.__name 133 | pos = self.getpositionbyname(d).size or 0 134 | # if not pos: 135 | # self.order = self.buy(d) 136 | # print(f'买买买{dt}, {dn}') 137 | if self.last_operation[d] != "long": 138 | if self.ind[d]['rsi'][0] < 30 and self.ind[d]['ema_fast'][0] > self.ind[d]['ema_slow'][0]: 139 | if not pos: 140 | self.order = self.long(data=self.getdatabyname(d.rstrip('_Heikin'))) 141 | else: 142 | self.order = self.close(data=self.getdatabyname(d.rstrip('_Heikin'))) 143 | self.order = self.long(data=self.getdatabyname(d.rstrip('_Heikin'))) 144 | if self.last_operation[d] != "short": 145 | if self.rsi > 70: 146 | if not pos: 147 | self.order = self.short(data=self.getdatabyname(d.rstrip('_Heikin'))) 148 | else: 149 | self.order = self.close(data=self.getdatabyname(d.rstrip('_Heikin'))) 150 | self.order = self.short(data=self.getdatabyname(d.rstrip('_Heikin'))) 151 | else: 152 | if pos: 153 | if -self.profit[d] > self.ind[d]['stoploss']: 154 | self.log("STOP LOSS: percentage %.3f %%" % self.profit[d], fgprint=False) 155 | if self.last_operation[d] == "long": 156 | self.order = self.close_short(data=self.getdatabyname(d.rstrip('_Heikin'))) 157 | if self.last_operation[d] == "short": 158 | self.order = self.close_short(data=self.getdatabyname(d.rstrip('_Heikin'))) 159 | # self.order = self.sell(d) 160 | # print('卖卖卖') 161 | # stop Loss -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from config import TELEGRAM, ENV 4 | 5 | 6 | def print_trade_analysis(analyzer): 7 | # Get the results we are interested in 8 | if not analyzer.get("total"): 9 | return 10 | 11 | total_open = analyzer.total.open 12 | total_closed = analyzer.total.closed 13 | total_won = analyzer.won.total 14 | total_lost = analyzer.lost.total 15 | win_streak = analyzer.streak.won.longest 16 | lose_streak = analyzer.streak.lost.longest 17 | pnl_net = round(analyzer.pnl.net.total, 2) 18 | strike_rate = round((total_won / total_closed) * 2) 19 | 20 | # Designate the rows 21 | h1 = ['Total Open', 'Total Closed', 'Total Won', 'Total Lost'] 22 | h2 = ['Strike Rate', 'Win Streak', 'Losing Streak', 'PnL Net'] 23 | r1 = [total_open, total_closed, total_won, total_lost] 24 | r2 = [strike_rate, win_streak, lose_streak, pnl_net] 25 | 26 | # Check which set of headers is the longest. 27 | if len(h1) > len(h2): 28 | header_length = len(h1) 29 | else: 30 | header_length = len(h2) 31 | 32 | # Print the rows 33 | print_list = [h1, r1, h2, r2] 34 | row_format = "{:<15}" * (header_length + 1) 35 | print("Trade Analysis Results:") 36 | for row in print_list: 37 | print(row_format.format('', *row)) 38 | 39 | 40 | def print_sqn(analyzer): 41 | sqn = round(analyzer.sqn, 2) 42 | print('SQN: {}'.format(sqn)) 43 | 44 | 45 | def send_telegram_message(message=""): 46 | if ENV != "production": 47 | return 48 | 49 | base_url = "https://api.telegram.org/bot%s" % TELEGRAM.get("bot") 50 | return requests.get("%s/sendMessage" % base_url, params={ 51 | 'chat_id': TELEGRAM.get("channel"), 52 | 'text': message 53 | }) 54 | -------------------------------------------------------------------------------- /walkforward.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.model_selection import TimeSeriesSplit 3 | from sklearn.utils import indexable 4 | from sklearn.utils.validation import _num_samples 5 | import backtrader as bt 6 | import backtrader.indicators as btind 7 | import datetime as dt 8 | import pandas as pd 9 | import pandas_datareader as web 10 | from pandas import Series, DataFrame 11 | import random 12 | from copy import deepcopy 13 | 14 | 15 | class TimeSeriesSplitImproved(TimeSeriesSplit): 16 | """Time Series cross-validator 17 | Provides train/test indices to split time series data samples 18 | that are observed at fixed time intervals, in train/test sets. 19 | In each split, test indices must be higher than before, and thus shuffling 20 | in cross validator is inappropriate. 21 | This cross-validation object is a variation of :class:`KFold`. 22 | In the kth split, it returns first k folds as train set and the 23 | (k+1)th fold as test set. 24 | Note that unlike standard cross-validation methods, successive 25 | training sets are supersets of those that come before them. 26 | Read more in the :ref:`User Guide `. 27 | Parameters 28 | ---------- 29 | n_splits : int, default=3 30 | Number of splits. Must be at least 1. 31 | Examples 32 | -------- 33 | >>> from sklearn.model_selection import TimeSeriesSplit 34 | >>> X = np.array([[1, 2], [3, 4], [1, 2], [3, 4]]) 35 | >>> y = np.array([1, 2, 3, 4]) 36 | >>> tscv = TimeSeriesSplit(n_splits=3) 37 | >>> print(tscv) # doctest: +NORMALIZE_WHITESPACE 38 | TimeSeriesSplit(n_splits=3) 39 | >>> for train_index, test_index in tscv.split(X): 40 | ... print("TRAIN:", train_index, "TEST:", test_index) 41 | ... X_train, X_test = X[train_index], X[test_index] 42 | ... y_train, y_test = y[train_index], y[test_index] 43 | TRAIN: [0] TEST: [1] 44 | TRAIN: [0 1] TEST: [2] 45 | TRAIN: [0 1 2] TEST: [3] 46 | >>> for train_index, test_index in tscv.split(X, fixed_length=True): 47 | ... print("TRAIN:", train_index, "TEST:", test_index) 48 | ... X_train, X_test = X[train_index], X[test_index] 49 | ... y_train, y_test = y[train_index], y[test_index] 50 | TRAIN: [0] TEST: [1] 51 | TRAIN: [1] TEST: [2] 52 | TRAIN: [2] TEST: [3] 53 | >>> for train_index, test_index in tscv.split(X, fixed_length=True, 54 | ... train_splits=2): 55 | ... print("TRAIN:", train_index, "TEST:", test_index) 56 | ... X_train, X_test = X[train_index], X[test_index] 57 | ... y_train, y_test = y[train_index], y[test_index] 58 | TRAIN: [0 1] TEST: [2] 59 | TRAIN: [1 2] TEST: [3] 60 | 61 | Notes 62 | ----- 63 | When ``fixed_length`` is ``False``, the training set has size 64 | ``i * train_splits * n_samples // (n_splits + 1) + n_samples % 65 | (n_splits + 1)`` in the ``i``th split, with a test set of size 66 | ``n_samples//(n_splits + 1) * test_splits``, where ``n_samples`` 67 | is the number of samples. If fixed_length is True, replace ``i`` 68 | in the above formulation with 1, and ignore ``n_samples % 69 | (n_splits + 1)`` except for the first training set. The number 70 | of test sets is ``n_splits + 2 - train_splits - test_splits``. 71 | """ 72 | 73 | def split(self, X, y=None, groups=None, fixed_length=False, 74 | train_splits=1, test_splits=1): 75 | """Generate indices to split data into training and test set. 76 | Parameters 77 | ---------- 78 | X : array-like, shape (n_samples, n_features) 79 | Training data, where n_samples is the number of samples 80 | and n_features is the number of features. 81 | y : array-like, shape (n_samples,) 82 | Always ignored, exists for compatibility. 83 | groups : array-like, with shape (n_samples,), optional 84 | Always ignored, exists for compatibility. 85 | fixed_length : bool, hether training sets should always have 86 | common length 87 | train_splits : positive int, for the minimum number of 88 | splits to include in training sets 89 | test_splits : positive int, for the number of splits to 90 | include in the test set 91 | Returns 92 | ------- 93 | train : ndarray 94 | The training set indices for that split. 95 | test : ndarray 96 | The testing set indices for that split. 97 | """ 98 | X, y, groups = indexable(X, y, groups) 99 | n_samples = _num_samples(X) 100 | n_splits = self.n_splits 101 | n_folds = n_splits + 1 102 | train_splits, test_splits = int(train_splits), int(test_splits) 103 | if n_folds > n_samples: 104 | raise ValueError( 105 | ("Cannot have number of folds ={0} greater" 106 | " than the number of samples: {1}.").format(n_folds, 107 | n_samples)) 108 | if (n_folds - train_splits - test_splits) < 0 and (test_splits > 0): 109 | raise ValueError( 110 | ("Both train_splits and test_splits must be positive" 111 | " integers.")) 112 | indices = np.arange(n_samples) 113 | split_size = (n_samples // n_folds) 114 | test_size = split_size * test_splits 115 | train_size = split_size * train_splits 116 | test_starts = range(train_size + n_samples % n_folds, 117 | n_samples - (test_size - split_size), 118 | split_size) 119 | if fixed_length: 120 | for i, test_start in zip(range(len(test_starts)), 121 | test_starts): 122 | rem = 0 123 | if i == 0: 124 | rem = n_samples % n_folds 125 | yield (indices[(test_start - train_size - rem):test_start], 126 | indices[test_start:test_start + test_size]) 127 | else: 128 | for test_start in test_starts: 129 | yield (indices[:test_start], 130 | indices[test_start:test_start + test_size]) 131 | 132 | 133 | class SMAC(bt.Strategy): 134 | """A simple moving average crossover strategy; crossing of a fast and slow moving average generates buy/sell 135 | signals""" 136 | params = {"fast": 20, "slow": 50, # The windows for both fast and slow moving averages 137 | "optim": False, "optim_fs": (20, 50)} # Used for optimization; equivalent of fast and slow, but a tuple 138 | 139 | # The first number in the tuple is the fast MA's window, the 140 | # second the slow MA's window 141 | 142 | def __init__(self): 143 | """Initialize the strategy""" 144 | 145 | self.fastma = dict() 146 | self.slowma = dict() 147 | self.regime = dict() 148 | 149 | if self.params.optim: # Use a tuple during optimization 150 | self.params.fast, self.params.slow = self.params.optim_fs # fast and slow replaced by tuple's contents 151 | 152 | if self.params.fast > self.params.slow: 153 | raise ValueError( 154 | "A SMAC strategy cannot have the fast moving average's window be " + \ 155 | "greater than the slow moving average window.") 156 | 157 | for d in self.getdatanames(): 158 | # The moving averages 159 | self.fastma[d] = btind.SimpleMovingAverage(self.getdatabyname(d), # The symbol for the moving average 160 | period=self.params.fast, # Fast moving average 161 | plotname="FastMA: " + d) 162 | self.slowma[d] = btind.SimpleMovingAverage(self.getdatabyname(d), # The symbol for the moving average 163 | period=self.params.slow, # Slow moving average 164 | plotname="SlowMA: " + d) 165 | 166 | # Get the regime 167 | self.regime[d] = self.fastma[d] - self.slowma[d] # Positive when bullish 168 | 169 | def next(self): 170 | """Define what will be done in a single step, including creating and closing trades""" 171 | for d in self.getdatanames(): # Looping through all symbols 172 | pos = self.getpositionbyname(d).size or 0 173 | if pos == 0: # Are we out of the market? 174 | # Consider the possibility of entrance 175 | # Notice the indexing; [0] always mens the present bar, and [-1] the bar immediately preceding 176 | # Thus, the condition below translates to: "If today the regime is bullish (greater than 177 | # 0) and yesterday the regime was not bullish" 178 | if self.regime[d][0] > 0 and self.regime[d][-1] <= 0: # A buy signal 179 | self.buy(data=self.getdatabyname(d)) 180 | 181 | else: # We have an open position 182 | if self.regime[d][0] <= 0 and self.regime[d][-1] > 0: # A sell signal 183 | self.sell(data=self.getdatabyname(d)) 184 | 185 | 186 | class PropSizer(bt.Sizer): 187 | """A position sizer that will buy as many stocks as necessary for a certain proportion of the portfolio 188 | to be committed to the position, while allowing stocks to be bought in batches (say, 100)""" 189 | params = {"prop": 0.1, "batch": 100} 190 | 191 | def _getsizing(self, comminfo, cash, data, isbuy): 192 | """Returns the proper sizing""" 193 | 194 | if isbuy: # Buying 195 | target = self.broker.getvalue() * self.params.prop # Ideal total value of the position 196 | price = data.close[0] 197 | shares_ideal = target / price # How many shares are needed to get target 198 | batches = int(shares_ideal / self.params.batch) # How many batches is this trade? 199 | shares = batches * self.params.batch # The actual number of shares bought 200 | 201 | if shares * price > cash: 202 | return 0 # Not enough money for this trade 203 | else: 204 | return shares 205 | 206 | else: # Selling 207 | return self.broker.getposition(data).size # Clear the position 208 | 209 | 210 | class AcctValue(bt.Observer): 211 | alias = ('Value',) 212 | lines = ('value',) 213 | 214 | plotinfo = {"plot": True, "subplot": True} 215 | 216 | def next(self): 217 | self.lines.value[0] = self._owner.broker.getvalue() # Get today's account value (cash + stocks) 218 | 219 | 220 | class AcctStats(bt.Analyzer): 221 | """A simple analyzer that gets the gain in the value of the account; should be self-explanatory""" 222 | 223 | def __init__(self): 224 | self.start_val = self.strategy.broker.get_value() 225 | self.end_val = None 226 | 227 | def stop(self): 228 | self.end_val = self.strategy.broker.get_value() 229 | 230 | def get_analysis(self): 231 | return {"start": self.start_val, "end": self.end_val, 232 | "growth": self.end_val - self.start_val, "return": self.end_val / self.start_val} 233 | 234 | 235 | start = dt.datetime(2018, 1, 1) 236 | end = dt.datetime(2020, 10, 31) 237 | # Different stocks from past posts because of different data source (no plot for NTDOY) 238 | symbols = ["BTC-USD", "ETH-USD", "BNB-USD"] 239 | datafeeds = {s: web.DataReader(s, "yahoo", start, end) for s in symbols} 240 | for df in datafeeds.values(): 241 | df["OpenInterest"] = 0 # PandasData reader expects an OpenInterest column; 242 | # not provided by Google and we don't use it so set to 0 243 | 244 | cerebro = bt.Cerebro(stdstats=False) 245 | 246 | plot_symbols = ["BTC-USD", "ETH-USD", "BNB-USD"] 247 | is_first = True 248 | # plot_symbols = [] 249 | for s, df in datafeeds.items(): 250 | data = bt.feeds.PandasData(dataname=df, name=s) 251 | if s in plot_symbols: 252 | if is_first: 253 | data_main_plot = data 254 | is_first = False 255 | else: 256 | data.plotinfo.plotmaster = data_main_plot 257 | else: 258 | data.plotinfo.plot = False 259 | cerebro.adddata(data) # Give the data to cerebro 260 | 261 | cerebro.broker.setcash(1000000) 262 | cerebro.broker.setcommission(0.02) 263 | cerebro.addstrategy(SMAC) 264 | cerebro.addobserver(AcctValue) 265 | cerebro.addobservermulti(bt.observers.BuySell) # Plots up/down arrows 266 | cerebro.addsizer(PropSizer) 267 | cerebro.addanalyzer(AcctStats) 268 | 269 | cerebro.run() --------------------------------------------------------------------------------