├── .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 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 | 
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()
--------------------------------------------------------------------------------