量化交易-回测调参-神龙摆尾

系列文章目录

第一章 神龙摆尾


前言

本章围绕一个结构模型,基于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来实现一个交易策略,并简单介绍了如何通过回测搜索来对策略参数进行调优。

所涉策略仅用于技术探讨,不代表作者认同该模型有效性。

对量化技术感兴趣可加微信交流

 

《从编程小白到量化宗师之路》系列课程是一套综合性实战课程,涵盖股票,期货,虚拟货币等的交易方法和策略手段。《基于BackTrader开发一套WorkForward前向分析框架》是本系列的第二个中级课程。课程宗旨是缩短个人或小型投资者与大型机构投资者之间的的差距。目前市场上的所有量化策略编写系统,都是从获取一段时间的数据开始,利用指标或者各种模型,进行订单的买卖操作,直到跑完这段时间的数据,运行出结果,并给出各种各样的统计分析,就结束了!?然而实际上,这远没有结束,我们就以指标为例,不同时间不同的行情,指标的效果有很大的差别,更别说不同的年份有不同的行情,只使用一段时间测试怎么足够?一次性用所有数据,又是一种极端过拟合,更何况,你不能使用2019年测试好的策略,用在2018年之前的任何时间,这些限制,正是金融时间序列数据的不同之处。为了解决这个问题,就应该使用WorkForward前向分析,也就是通常意义上的“边走边看,走一步看一步”。这本应该是最基础的功能,然而市面上大多数的量化分析系统,完全没有提到或者提供这项功能,让初步入门的量化学习者还要自己组装这一基础功能。本课程基于backtrader,实现了一个默认支持workforward分析的框架,用户只需要设定需要的产品数据,比如股票和期货,然后设定训练时间,测试时间,预热时间(课程会讲到),编写策略后, 就可以运行WorkForward前向分析功能。用户以后只需要专注于策略编写,大大减轻了使用量化交易系统的负担。课程内容从讲解机器学习中用到的交叉验证和为什么金融时序要使用前向分析(WorkForward)开始,详细讲解了前向分析框架的每一个函数,每一个参数的用途,并使用边实际运行代码边讲解的方法,通透的讲述了前向分析框架使用到的各个部分,为同学们透彻理解前向分析框架的代码提供了十分方便的途径。 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值