系列文章目录
第一章 神龙摆尾
前言
本章围绕一个结构模型,基于backtrader框架,从策略实现、参数调优等方面探讨量化交易实践。
一、所涉结构模型简介
本模型来自网易公开课,是一个波段上涨模型,名曰亢龙有悔,模型基本示意如下(图片截取自公开课视频)
模型要点如下:
二、策略实现
1.策略源代码
import backtrader as bt
class StrategyClass(bt.Strategy):
'''神龙摆尾'''
params = dict(
ma = 'S', ## 'S': SMA、'E':EMA、'W':WMA
period_b1 = 22, # 前箱体长度
period_b2 = 5, # 后箱体长度
boxR = 8, # 前箱体检测,60、22、10 三均线最大差值放大boxR倍仍不高于股价
breakdown = 0, # 后箱体接受几次下突破
preboxnohigh = False, # 前箱体不能高于涨停价,此参数经测试移除,详见 https://gitee.com/kuge/backtrader-study/commit/69b56932ecb62d4fb35a7e941ff8fda5c5d2eff7
needvcd = True, # 需要量均死叉
maxuplimit = -1, # 后箱体等待期涨停上限,负数表示不限制
postboxV = 2, # 后箱体规则,1:公开课版本; 2: 正课版本,3:自动切换
closeup1 = 1.3, # 目标止盈
closeup2 = 1.1, # 顶部回落止盈
closedown = 0.85, # 止损
code = None,
name = None,
log = True,
)
def log(self, txt, dt=None, force=False):
if force or self.p.log:
dt = dt or self.data.datetime.datetime()
who = f'{self.p.code} {self.p.name} ' if self.p.code else ''
print(f'{dt.isoformat()} {who}{txt}')
def notify_order(self, order):
if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
return
if order.status == order.Completed:
if order.isbuy():
buytxt = '买入价:%.2f, 量:%s, 持仓:%s' % (order.executed.price, order.executed.size, self.position.size)
self.log(buytxt, bt.num2date(order.executed.dt))
else:
selltxt = '卖出价, %.2f, 量:%s, 持仓:%s' % (
order.executed.price, order.executed.size, self.position.size)
if 'log' in order.info:
selltxt = '%s %s' % (selltxt, order.info.log)
self.log(selltxt, bt.num2date(order.executed.dt))
elif order.status in [order.Expired, order.Canceled, order.Margin]:
if 'log' not in order.info:
self.log('%s , %s' % (order.Status[order.status], order))
pass
def __init__(self):
ma = bt.ind.EMA if self.p.ma == 'E' else bt.ind.WMA if self.p.ma == 'W' else bt.ind.SMA
vma5 = ma(self.data.volume, period=5)
vma10 = ma(self.data.volume, period=10)
ma10 = ma(self.data, period=10)
ma22 = ma(self.data, period=22)
ma60 = ma(self.data, period=60)
self.xxx = (ma10, ma22, ma60)
self.yyy = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10))
self.zzz = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10)) * self.p.boxR
# 涨停
self.limitup = (self.data.close(-1) * 1.1 - 0.01 <= self.data.close)
# 前箱体最高
self.preBoxHigh = bt.ind.Highest(self.data.high, period=self.p.period_b1)
# 横盘-均线贴近
self.preBox = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10)) * self.p.boxR < self.data
# 后箱体上沿
self.boxHigh = bt.ind.Highest(self.data, period=self.p.period_b2)
# 后箱体下沿
self.boxLow = bt.ind.Lowest(self.data, period=self.p.period_b2)
# 后箱体均线交叉
self.m5c10 = bt.ind.CrossOver(vma5, vma10)
self.signal = None # [涨停柱后静默期,下突破线,下突破计数,死叉,上破线]
self.order = None
def onLimitup(self):
self.signal = [
len(self) + self.p.period_b2, # 0 静默期
self.data.close[0], # 1 箱体下突破线
self.p.breakdown, # 2 下突破计数
False, # 3 死叉
self.data.close[0], # 4 上突破线
None, # 5 买单
None, # 6 止赢单
self.data.close[-1], # 7 箱体V1下线
0, # 8 后箱体涨停计数
]
def checkBreakup(self):
curr = len(self)
close = self.data.close[0]
s = self.signal
if self.limitup[0]:
s[8] += 1
else:
if close < s[7]: # 向下突破
self.log('跌穿V1箱体,重新等待涨停')
self.signal = None
return False
if close < s[1]: # 向下突破
s[2] -= 1
if s[2] < 0 and self.p.postboxV == 2:
self.log('跌穿箱体,重新等待涨停')
self.signal = None # 重新来过
return False
if not s[3]: # 未见死叉
s[3] = self.m5c10 < 0
if curr == s[0]: # 箱体长度达标
v = self.p.postboxV
if v == 3:
v = 1 if s[2] < 0 else 2
if v == 2:
s[4] = self.boxHigh[0]
else:
s[4] = s[1]
s[1] = s[7]
elif curr > s[0] and close > s[4]: # 箱后突破
if s[3] or not self.p.needvcd: # 已有死叉
return True
else:
self.log('万事俱备,只欠死叉,继续等待')
s[4] = close
if self.p.maxuplimit >= 0 and s[8] > self.p.maxuplimit:
self.log('后箱体涨停次数超限,取消观察')
self.signal = None
return False
def next(self):
if self.signal:
if self.signal[6]:
if self.signal[6].status in [bt.Order.Canceled, bt.Order.Completed]:
self.signal = None
elif self.signal[6].status in [bt.Order.Expired, bt.Order.Margin]:
print('订单状态异常', self.signal[6].status)
elif self.data.close[0] * self.p.closeup2 < self.boxHigh[0]:
self.sell(exectype=bt.Order.Market, log=' (高位回落)止盈 ', oco=self.signal[6])
elif self.checkBreakup():
self.log('向上突破箱体,买入')
self.signal[5] = self.buy()
self.signal[6] = self.sell(exectype=bt.Order.Limit, price=self.data.close[0] * self.p.closeup1, log=' 止盈 ')
self.sell(exectype=bt.Order.Stop, price=self.signal[1], log=' 初始止损 ', oco=self.signal[6])
if self.p.closedown > 0:
self.sell(exectype=bt.Order.Stop, price=self.data.close[0] * self.p.closedown, log=' 百分比止损', oco=self.signal[6])
elif self.limitup[0]:
if self.preBox[0]:
if self.p.preboxnohigh and self.preBoxHigh[0] > self.data.close[0]:
self.log('涨停,但横盘阶段有高过今日涨停价格,不于理会')
else:
self.log('横盘后涨停,开始观察')
self.onLimitup()
else:
self.log('涨停,但横盘不足')
2.初步参数调优-选股参数
先测选股部分,重点关注机会数量,也顺便初步观察收益
import argparse
import backtrader as bt
from datetime import datetime as dt
from time import time
from multiprocessing import cpu_count, Process
import os
from os.path import join, exists, dirname
if __name__ == '__main__':
import sys
sys.path.append(dirname(dirname(__file__)))
# 此即上文策略实现
from strategies.netEasyTailwhip import StrategyClass
# 以下涉及完整代码参见 https://gitee.com/kuge/backtrader-study
from utils.localdata import onebyone, download
ps = dict(
boxR = [7,7.4,7.8,8,8.2,8.6,9], # 前箱体检测,60、22、10 三均线最大差值放大boxR倍仍不高于股价
breakdown = [0,1,2], # 后箱体接受几次下突破
preboxnohigh = [True,False], # 前箱体不能高于涨停价
needvcd = [True,False], # 需要量均死叉
maxuplimit = [-1,0,1], # 后箱体等待期涨停上限,负数表示不限制
postboxV = [1,2], # 后箱体规则,1:公开课版本; 2: 正课版本,3:自动切换
)
ps2 = None
def pps(p={}, ks=list(ps.keys()), ps=ps, ps2=ps2):
if len(ks) > 0:
for x in ps[ks[0]]:
yield from pps(dict({f'{ks[0]}': x}, **p), ks[1:], ps, ps2)
elif 'afterdown' in p and p['afterdown'] and ps2:
yield from pps(p, list(ps2.keys()), ps2, None)
else:
yield p
print(len([x for x in pps()]))
def main(num, pss, StrategyClass=StrategyClass):
logfile = join('logs', f's{num:03d}_{dt.now().isoformat()}_{os.getpid()}.log')
def log(text, write=False, file=logfile, cache=dict(a=[])):
lines = cache['a']
lines.append(f'{dt.now().isoformat()} {text}')
if write:
with open(file, 'a', encoding='utf8') as f:
f.write('\n'.join(lines))
cache['a'] = ['\n']
commparams = dict(log=False, closeup1=1.11, closeup2=1.06, closedown=0.8)
import re
replace = re.compile(r'0{4}0+1$')
for p in pss:
params = dict(commparams, **p)
total = [0, 0, 0, 0, 0] # 有交易的证券数,交易总笔数,盈利总笔数,损失总笔数,盈利总和
# for daily_data, code, name in [d for d in onebyone(klen=300) if d[1] in ['sh.600378', 'sh.603333']]:
for daily_data, code, name in onebyone(klen=300):
params['code'] = code
params['name'] = name
cerebro = bt.Cerebro()
# 设置启动资金
cerebro.broker.setcash(10000000.0)
# 设置佣金
cerebro.broker.setcommission(commission=0.001)
cerebro.addstrategy(StrategyClass, **params)
data = bt.feeds.PandasData(dataname=daily_data)
cerebro.adddata(data)
cerebro.addsizer(bt.sizers.FixedSize, stake=100) # 每笔交易使用固定交易量,stake代表交易量
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='ta')
result = cerebro.run(plot=False)
r = result[0].analyzers.ta.get_analysis()
if r.total and r.total.total > 0:
s = r.total.total
l, w = 0, 0
if r.total.total - r.total.open > 0:
l, w, x = r.lost.total, r.won.total, r.pnl.gross.total
else:
x = cerebro.broker.getvalue() - 10000000
log(f'{code} {name} {s} {w} {l} {x:.2f} ')
total = [a+b for a,b in zip(total, [1,s,w,l,x])]
total[-1] = round(total[-1], 2)
log(replace.sub('', f'={total}={p}={dt.now()}'), True)
def parse_args():
parser = argparse.ArgumentParser(
description='神龙摆尾策略参数搜索-多进程')
parser.add_argument('-n', default=cpu_count()-1, required=False, type=int, help='进程数')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
download()
params = [x for x in pps()]
size = (len(params) // (args.n - 1)) if args.n > 1 else len(params)
for i in range(args.n):
Process(target=main,kwargs={"num": i, "pss": params[size * i:size * (i + 1)]}).start()
print(dt.now().isoformat(), f"主进程ID: {os.getpid()} 进程数量:{args.n} 参数组数量: {len(params)} 每个进程搜索参数组数量:{size}")
测试发现
1、preboxnohigh参数纯属画蛇添足,遂移除。
2、最大机会数量(共4314次交易机会,胜率37%,涉及2145只证券)
2154 4314 1599 2636 3589.04 {'postboxV':1,'maxuplimit':-1,'needvcd':False,'breakdown':0,'boxR':7}
2154 4314 1599 2636 3589.04 {'postboxV':1,'maxuplimit':-1,'needvcd':False,'breakdown':1,'boxR':7}
2154 4314 1599 2636 3589.04 {'postboxV':1,'maxuplimit':-1,'needvcd':False,'breakdown':2,'boxR':7}
3、最高收益(22042.67)
1760 2746 1024 1658 19236.8 {'postboxV':1,'maxuplimit':0,'needvcd':False,'breakdown':0,'boxR':7.4}
1760 2746 1024 1658 19236.8 {'postboxV':1,'maxuplimit':0,'needvcd':False,'breakdown':1,'boxR':7.4}
1760 2746 1024 1658 19236.8 {'postboxV':1,'maxuplimit':0,'needvcd':False,'breakdown':2,'boxR':7.4}
907 1128 422 690 19609.18 {'postboxV':2,'maxuplimit':1,'needvcd':True,'breakdown':2,'boxR':7.4}
865 1063 395 652 20718.98 {'postboxV':2,'maxuplimit':1,'needvcd':True,'breakdown':2,'boxR':8.2}
1118 1500 544 936 20830.49 {'postboxV':2,'maxuplimit':1,'needvcd':False,'breakdown':2,'boxR':7}
874 1076 404 656 21041.59 {'postboxV':2,'maxuplimit':1,'needvcd':True,'breakdown':2,'boxR':8}
934 1167 442 708 22042.67 {'postboxV':2,'maxuplimit':1,'needvcd':True,'breakdown':2,'boxR':7}
3.进一步参数调优-卖出参数
选定 {'postboxV':1,'maxuplimit':-1,'needvcd':False,'breakdown':2,'boxR':7} ,期待通过止盈止损参数提高收益
ps = dict(
closeup1 = [x/100 for x in range(105, 118)], # 目标止盈
closeup2 = [x/100 for x in range(103, 110)], # 顶部回落止盈
closedown = [x/100 for x in range(72,90,2)], # 止损
)
进一步测试发现,如下参数组合可获得显著的性能提升
2149 4340 2705 1550 2925 31900.29 {'closedown':0.72,'closeup2':1.09,'closeup1':1.05}
2149 4340 2705 1550 2925 31900.29 {'closedown':0.74,'closeup2':1.09,'closeup1':1.05}
2149 4340 2705 1550 2925 31900.29 {'closedown':0.76,'closeup2':1.09,'closeup1':1.05}
其中:
- 胜率 从37%提升到 62.3%,提升了 68%
- 收益 从22042.67提升到31900.29,提升了 44.7%
另外,三个参数中有两个处于测试范围的边界,下一步可考虑更换范围继续搜索。
总结
本文通过源代码简单演示了如何使用backtrader来实现一个交易策略,并简单介绍了如何通过回测搜索来对策略参数进行调优。
所涉策略仅用于技术探讨,不代表作者认同该模型有效性。
对量化技术感兴趣可加微信交流