基于backtrader的唐奇安结合ADX策略实现(自动多参数调优和回测)
from datetime import datetime,timedelta
import backtrader as bt
import tushare as ts
import pandas as pd
import talib as ta
import numpy as np
import matplotlib.pyplot as plt
import mplfinance as mpf
import pyfolio as pf
import optunity
import optunity.metrics
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
plt.rcParams['figure.figsize']=[10, 8]
plt.rcParams['figure.dpi']=200
plt.rcParams['figure.facecolor']='w'
plt.rcParams['figure.edgecolor']='k'
import requests
import json
import pandas as pd
import datetime as dt
def get_binance_bars(symbol, interval, startTime, endTime):
url = "https://api.binance.com/api/v3/klines"
startTime = str(int(startTime.timestamp() * 1000))
endTime = str(int(endTime.timestamp() * 1000))
limit = '50'
req_params = {"symbol" : symbol, 'interval' : interval, 'startTime' : startTime, 'endTime' : endTime, 'limit' : limit}
df = pd.DataFrame(json.loads(requests.get(url, params = req_params).text))
if (len(df.index) == 0):
return None
df = df.iloc[:, 0:6]
df.columns = ['datetime', 'open', 'high', 'low', 'close', 'volume']
df.open = df.open.astype("float")
df.high = df.high.astype("float")
df.low = df.low.astype("float")
df.close = df.close.astype("float")
df.volume = df.volume.astype("float")
df.index = [dt.datetime.fromtimestamp(x / 1000.0) for x in df.datetime]
return df
df_list = []
last_datetime = dt.datetime(2022,1,1)
while True:
new_df = get_binance_bars('ETHUSDT', '4h', last_datetime, dt.datetime(2022,7,10))
if new_df is None:
break
df_list.append(new_df)
last_datetime = max(new_df.index) + dt.timedelta(0, 5)
dataframe=pd.concat(df_list)
dataframe['openinterest']=0
dataframe=dataframe[['open','high','low','close','volume','openinterest']]
print(dataframe.shape)
print(dataframe.tail())
dataframe.head()
(1099, 6)
open high low close volume \
2022-07-09 08:00:00 1214.03 1221.79 1204.67 1217.01 147773.1735
2022-07-09 12:00:00 1217.01 1227.40 1210.69 1225.02 79680.3095
2022-07-09 16:00:00 1225.01 1228.25 1205.27 1210.28 80972.9123
2022-07-09 20:00:00 1210.28 1222.61 1206.52 1215.24 83406.7132
2022-07-10 00:00:00 1215.23 1235.40 1209.33 1218.61 89320.9760
openinterest
2022-07-09 08:00:00 0
2022-07-09 12:00:00 0
2022-07-09 16:00:00 0
2022-07-09 20:00:00 0
2022-07-10 00:00:00 0
| open | high | low | close | volume | openinterest |
---|
2022-01-01 00:00:00 | 3784.64 | 3788.45 | 3623.00 | 3626.27 | 54604.9027 | 0 |
---|
2022-01-01 04:00:00 | 3626.21 | 3712.50 | 3622.29 | 3676.23 | 39496.5993 | 0 |
---|
2022-01-01 08:00:00 | 3676.22 | 3748.45 | 3676.22 | 3723.96 | 26592.7693 | 0 |
---|
2022-01-01 12:00:00 | 3723.96 | 3765.27 | 3701.00 | 3715.31 | 27251.1401 | 0 |
---|
2022-01-01 16:00:00 | 3715.32 | 3733.84 | 3673.46 | 3693.37 | 23727.9949 | 0 |
---|
class D_ADX_TradingStrategy(bt.Strategy):
params = dict(
N1= 40,
N2=30,
N3=30,
N4=20,
printlog=False,
)
def log(self, txt, dt=None,doprint=False):
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()},{txt}')
def __init__(self):
self.order = None
self.buy_count = 0
self.last_price = 0
self.close = self.datas[0].close
self.high = self.datas[0].high
self.low = self.datas[0].low
self.DonchianH = bt.ind.Highest(self.high(-1), period=int(self.p.N1), subplot=True)
self.DonchianL = bt.ind.Lowest(self.low(-1), period=int(self.p.N2), subplot=True)
self.DonchianM= (self.DonchianH+self.DonchianL)/2
self.CrossoverH = bt.ind.CrossOver(self.close(0), self.DonchianH, subplot=False)
self.CrossoverL = bt.ind.CrossOver(self.close(0), self.DonchianL, subplot=False)
self.CrossoverM = bt.ind.CrossOver(self.close(0), self.DonchianM, subplot=False)
self.ADX = bt.talib.ADX(self.high, self.low, self.close,timeperiod=int(self.p.N4),subplot=True)
def next(self):
if self.order:
return
if self.position.size > 0 :
if self.CrossoverM<0 or self.ADX[0]<self.ADX[-1]:
self.order = self.sell(size=abs(self.position.size))
self.buy_count = 0
elif self.position.size < 0 :
if self.CrossoverM>0 or self.ADX[0]<self.ADX[-1]:
self.order = self.buy(size=abs(self.position.size))
self.buy_count = 0
else:
if self.CrossoverH > 0 and self.buy_count == 0 and self.ADX[0]>self.ADX[-1] and self.ADX[-1]>int(self.p.N3):
self.buy_unit = int(self.broker.getvalue()/self.close[0]/4)
self.order = self.buy(size=self.buy_unit)
self.last_price = self.position.price
self.buy_count = 1
elif self.CrossoverL < 0 and self.buy_count == 0 and self.ADX[0]>self.ADX[-1] and self.ADX[-1]>int(self.p.N3):
self.buy_unit = int(self.broker.getvalue()/self.close[0]/4)
self.buy_unit=500
self.order = self.sell(size=self.buy_unit)
self.last_price = self.position.price
self.buy_count = 1
def notify_order(self, order):
order_status = ['Created','Submitted','Accepted','Partial',
'Completed','Canceled','Expired','Margin','Rejected']
if order.status in [order.Submitted, order.Accepted]:
self.log('ref:%.0f, name: %s, Order: %s'% (order.ref,
order.data._name,
order_status[order.status]))
return
if order.status in [order.Partial, order.Completed]:
if order.isbuy():
self.log(
'BUY EXECUTED, status: %s, ref:%.0f, name: %s, Size: %.2f, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order_status[order.status],
order.ref,
order.data._name,
order.executed.size,
order.executed.price,
order.executed.value,
order.executed.comm))
else:
self.log('SELL EXECUTED, status: %s, ref:%.0f, name: %s, Size: %.2f, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order_status[order.status],
order.ref,
order.data._name,
order.executed.size,
order.executed.price,
order.executed.value,
order.executed.comm))
elif order.status in [order.Canceled, order.Margin, order.Rejected, order.Expired]:
self.log('ref:%.0f, name: %s, status: %s'% (
order.ref, order.data._name, order_status[order.status]))
self.order = None
def notify_trade(self, trade):
if trade.justopened:
self.log('Trade Opened, name: %s, Size: %.2f,Price: %.2f' % (
trade.getdataname(), trade.size, trade.price))
elif trade.isclosed:
self.log('Trade Closed, name: %s, GROSS %.2f, NET %.2f, Comm %.2f' %(
trade.getdataname(), trade.pnl, trade.pnlcomm, trade.commission))
else:
self.log('Trade Updated, name: %s, Size: %.2f,Price: %.2f' % (
trade.getdataname(), trade.size, trade.price))
def stop(self):
self.log(f'(组合线:{self.p.N1},{self.p.N2},{self.p.N3},{self.p.N4}); 期末总资金: {self.broker.getvalue():.2f}', doprint=False)
def main(N1,N2,N3,N4,para_opt=True,startcash=100000,com=0.02):
if para_opt==True:
cerebro = bt.Cerebro()
cerebro.addstrategy(D_ADX_TradingStrategy,N1=N1,N2=N2,N3=N3,N4=N4)
data = bt.feeds.PandasData(dataname=dataframe)
cerebro.adddata(data)
cerebro.broker.setcash(startcash)
cerebro.broker.setcommission(commission=com)
cerebro.run(maxcpus=2)
value = cerebro.broker.getvalue()
return value
else:
cerebro = bt.Cerebro()
cerebro.addstrategy(D_ADX_TradingStrategy,N1=N1,N2=N2,N3=N3,N4=N4)
data = bt.feeds.PandasData(dataname=dataframe)
cerebro.adddata(data)
cerebro.broker.setcash(startcash)
cerebro.broker.setcommission(commission=com)
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
print('期初总资金: %.2f' % cerebro.broker.getvalue())
results=cerebro.run(maxcpus=2)
cerebro.plot(iplot=False)
result = results[0]
pyfolio = result.analyzers.pyfolio
returns, positions, transactions, gross_lev = pyfolio.get_pf_items()
pf.create_full_tear_sheet(returns)
'''backtrader内置的策略参数优化方法是权利搜索方法,也就是遍历每个参数组合值。在参数很多,每个参数取值变化范围大的情况下,优化效率是很低的。
可以采用智能优化算法,比如粒子群优化等进行大规模参数优化。下面,我们用python开源算法库optunity来对backtrader策略参数进行优化。 '''
'''
执行10次回测,设置两个参数sma1、sma2的取值范围
num_evals: 执行次数
Available solvers: particle swarm, tpe, sobol, nelder-mead, random search, cma-es, grid search
sma1、sma2 确定参数的取值范围
'''
opt = optunity.maximize(
f=main,
num_evals=100,
solver_name='particle swarm',
N1=[40,100],
N2=[40,100],
N3=[20,50],
N4=[8,20]
)
optimal_pars, details, _ = opt
print(optimal_pars)
{'N1': 76.64719168590987, 'N2': 83.65052762407552, 'N3': 25.247355935345496, 'N4': 15.344248021285086}
main(N1=optimal_pars['N1'],N2=optimal_pars['N2'],N3=optimal_pars['N3'],N4=optimal_pars['N4'],para_opt=False)
期初总资金: 100000.00
Start date | 2022-01-01 |
---|
End date | 2022-07-10 |
---|
Total months | 9 |
---|
| Backtest |
---|
Annual return | 276.335% |
---|
Cumulative returns | 173.054% |
---|
Annual volatility | 3386.631% |
---|
Sharpe ratio | -0.12 |
---|
Calmar ratio | 2.35 |
---|
Stability | NaN |
---|
Max drawdown | -117.486% |
---|
Omega ratio | 0.90 |
---|
Sortino ratio | -0.15 |
---|
Skew | -3.82 |
---|
Kurtosis | 92.83 |
---|
Tail ratio | 0.60 |
---|
Daily value at risk | -428.304% |
---|
Worst drawdown periods | Net drawdown in % | Peak date | Valley date | Recovery date | Duration |
---|
0 | 117.49 | 2022-01-22 | 2022-05-04 | 2022-06-13 | 101 |
---|
1 | 25.22 | 2022-01-20 | 2022-01-21 | 2022-01-22 | 2 |
---|
2 | 23.40 | 2022-06-15 | 2022-06-19 | NaT | NaN |
---|
3 | 3.43 | 2022-06-13 | 2022-06-14 | 2022-06-15 | 3 |
---|
4 | NaN | NaT | NaT | NaT | NaN |
---|