BackTrader:性能优化之多股策略速度优化

前言:

谈及BackTrader的回测速度优化,最常见的说法是从底层使用numpy等计算库来替换,但这种优化无疑非常新手不友好。因此本文着眼于如何最简单的优化多股情况下回测慢这一情况。考虑测试效率,本文使用100支股票回测。经过测试,优化后策略执行速度提升59%(93->38.4)。

策略描述:

前一天非一字涨停的股票进入候选池。

第二天10~11点若涨幅大于4%买入。

持仓股在14:30时若未涨停卖出。

V1策略及运行时间:

v1代码设计思路:

使用5分数据进行交易,而使用日线数据进行候选池判断及涨幅判断。添加定时器只在每天15:00点筛选候选池,然后在next中根据时间与涨幅判断是否需要买入或卖出。策略部分代码如下:

class MyStrategy(bt.Strategy):
    params = dict(
        when=bt.timer.SESSION_START,
        end=bt.timer.SESSION_END,
        timer=True,
        cheat=False,
        offset=timedelta(),
        repeat=timedelta(),
        weekdays=[],
        period=3,
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.datetime(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        self.order = None

        self.add_timer(
                when=time(15, 0),
                offset=self.p.offset,
                repeat=self.p.repeat,
                weekdays=self.p.weekdays,
        )

        s_m = []
        for i, d in enumerate(self.datas):
            if not d._name.endswith('_day'):
                s_m.append([d._name, i, None])
        self.st_df = pd.DataFrame(data=s_m, columns=['code', 'min', 'day'])
        for i, d in enumerate(self.datas):
            if d._name.endswith('_day'):
                n = d._name.split('_')[0]
                self.st_df.loc[self.st_df.code == n, 'day'] = i
        #         self.stock_names.append(d._name)
        # self.min_stocks = self.datas[:int(len(self.datas)/2)]
        # self.day_stocks = self.datas[-int(len(self.datas)/2):]

        self.zt_list = []
        self.last_hold = []
        self.new_hold = []
        self.zt_num = 0

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        idx = self.st_df.loc[self.st_df.code==order.data._name].index.values[0]
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.new_hold.append(idx)
                self.zt_list.remove(idx)
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))
                self.last_hold.remove(idx)
        elif order.status in [order.Canceled, order.Expired, order.Margin, order.Rejected]:
            self.log('Order Canceled/Expired/Margin/Rejected')
            self.new_hold.remove(idx)

        # Write down: no pending order
        self.order = None

    def next(self):
        t = self.datetime.time(0)
        #1.每天早上10:05至11:05买入
        len_for_new = 10 - len(self.last_hold) - len(self.new_hold)
        if len(self.zt_list) > 0 and len_for_new > 0:
            if t >= time(9,40) and t <= time(14,30):
                for i in self.zt_list:
                    if i in self.last_hold:
                        continue
                    d = self.datas[self.st_df.loc[i, 'min']]
                    if len_for_new <= 0:
                        break
                    last_close = self.datas[self.st_df.loc[i, 'day']].close[0]
                    if 1.045 * last_close < d.close[0] < 1.09 * last_close:
                        len_for_new -= 1
                        targetvalue = 0.1 * self.broker.getvalue()
                        size = targetvalue/(last_close*1.09)//100*100
                        self.buy(data=d, size=size, price=last_close*1.09, exectype=bt.Order.Limit,
                                 valid=self.datetime.datetime(0)+timedelta(minutes=5))

        #2.每天14:35卖出
        if len(self.last_hold) > 0:
            if t == time(14, 35):
                for i in self.last_hold:
                    m = self.datas[self.st_df.loc[i, 'min']]
                    d = self.datas[self.st_df.loc[i, 'day']]
                    if m.close[0] < d.high_limit[0]: #14:30时 day bar最新是昨天的
                        print('sell 平仓', m._name, self.getposition(m).size)
                        self.close(data=m)

    
    def notify_timer(self, timer, when, *args, **kwargs):
        # 2.合并买入卖出结果
        self.last_hold += self.new_hold
        self.new_hold = []
        # 1.根据涨停预选股票池
        self.zt_list = []
        for i, row in self.st_df.iterrows():
            d = self.datas[row['day']]
            if d.close[0] > d.low[0] and d.pctChg[0] > 9.9:
                self.log('zhangting ' + str(d.close[0]) + d._name)
                self.zt_list.append(i)
        # 3.删除已买入
        self.zt_list = list(set(self.zt_list)-set(self.last_hold))
        self.zt_num += len(self.zt_list)
        #print('平均涨停数', self.zt_num/len(self.data0))

运行时间:

总时间:72秒

读取csvcerebro.adddata执行完成
5.8462

可以看到耗时主要集中在cerebro添加数据完成到执行完成,[3]中所提及的优化数据读取的方式便不适用。而根据[2]中提出,Observers和Analyzers耗时能达到执行的一半,去掉以后重新运行得到总时间:71秒,没有明显提升,可能是本例中添加的Observers和Analyzers都比较简单。

V2策略及运行时间:

v2代码改进思路:

为了提高运行效率,考虑尽量减少next中的判断,将其放到cerebro之外,同时将信号直接附加到5min数据上,不再传入日数据。代码如下:

class PandasDataExtendInd(bt.feeds.PandasData):
    # 增加线
    lines = ('ind','high_limit','buy_ind', 'sell_ind',)
    params = (('ind', -1),('high_limit', -1),('buy_ind', -1),('sell_ind', -1), )  # 机构持股数量合计


class MyStrategy(bt.Strategy):
    params = dict(
        when=bt.timer.SESSION_START,
        end=bt.timer.SESSION_END,
        timer=True,
        cheat=False,
        offset=timedelta(),
        repeat=timedelta(),
        weekdays=[],
        period=3,
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.datetime(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        self.order = None

        self.add_timer(
                when=time(15, 0),
                offset=self.p.offset,
                repeat=self.p.repeat,
                weekdays=self.p.weekdays,
        )

        self.zt_list = []
        self.last_hold = []
        self.new_hold = []
        self.zt_num = 0

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        #idx = self.st_df.loc[self.st_df.code==order.data._name].index.values[0]
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.new_hold.append(order.data)
                self.zt_list.remove(self.datas.index(order.data))
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))
                self.last_hold.remove(order.data)
        elif order.status in [order.Canceled, order.Expired, order.Margin, order.Rejected]:
            self.log('Order Canceled/Expired/Margin/Rejected')
            self.new_hold.remove(order.data)

        # Write down: no pending order
        self.order = None

    def next(self):
        t = self.datetime.time(0)
        #1.每天早上10:05至11:05买入
        len_for_new = 10 - len(self.last_hold) - len(self.new_hold)
        if len(self.zt_list) > 0 and len_for_new > 0:
            if t >= time(9,40) and t <= time(14,30):
                for i in self.zt_list:
                    if i in self.last_hold:
                        continue
                    d = self.datas[i]
                    if len_for_new <= 0:
                        break
                    if d.buy_ind:
                        len_for_new -= 1
                        targetvalue = 0.1 * self.broker.getvalue()
                        size = targetvalue/(d.high_limit*0.99)//100*100
                        self.buy(data=d, size=size, price=d.high_limit*0.99, exectype=bt.Order.Limit,
                                 valid=self.datetime.datetime(0)+timedelta(minutes=5))

        #2.每天14:35卖出
        if len(self.last_hold) > 0:
            if t == time(14, 35):
                for m in self.last_hold:
                    if m.sell_ind: #14:30时 day bar最新是昨天的
                        print('sell 平仓', m._name, self.getposition(m).size)
                        self.close(data=m)

    
    def notify_timer(self, timer, when, *args, **kwargs):
        # 2.合并买入卖出结果
        self.last_hold += self.new_hold
        self.new_hold = []
        # 1.根据涨停预选股票池
        self.zt_list = []
        for i, d in enumerate(self.datas):
            if d.ind[0]:
                self.zt_list.append(i)
        # 3.删除已买入
        self.zt_list = list(set(self.zt_list)-set(self.last_hold))
        self.zt_num += len(self.zt_list)
        #print('平均涨停数', self.zt_num/len(self.data0))

运行时间:

总时间:103秒

读取csvcerebro.adddata执行完成
5.84.593

反向优化效果显著,也就是next中的比较操作+少传入日数据的效果远远小于传入了复杂的5分钟数据。详细打印运行时间,可以看到next第一次开始时为80秒,中间接近70秒的时间是cerebro进行各种初始化。

V3最终优化

优化思路:

 详细分析代码后可以得到其中最耗时的部分为:

# cerebro.py -> runstrategies()
for data in self.datas:
    data.preload()

# feed.py -> preload()
def preload(self):
    while self.load():
        pass
    self._last()
    self.home()

preload本身不好优化,但是对于runstrategies可以采用多线程执行进行优化,采用cerebro本身使用的Multiprocessing.Pool完成。

运行时间:

总时间:49秒

读取csvcerebro.adddata执行完成
5.94.338.4

数据读取、载入耗时不变,执行速度大幅提升。

结论

利用多线程可以大幅提升策略回测速度,同时修改难度较低。

电脑参数:

i7-10510U 2.30GHz, 4核8线程

15G内存

win10

参考:

[1] https://zhuanlan.zhihu.com/p/345815425

[2] https://www.zhihu.com/question/440467223

[3] https://community.backtrader.com/topic/2263/which-line-code-function-consume-more-time-when-doing-a-backtest/13

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
backtrader是一个功能强大的开源框架,可以用于构建和测试量化交易策略。它提供了参数优化的功能,可以帮助改进策略的性能。下面是使用backtrader进行参数优化的步骤: 1. 定义策略类:首先,你需要定义一个继承自backtrader.Strategy的策略类。在这个类中,你可以定义策略的逻辑和需要优化的参数。 2. 定义参数:在策略类中,你可以使用backtrader的Params类来定义需要优化的参数。你可以指定参数的名称、取值范围和步长。 3. 创建Cerebro对象:接下来,你需要创建一个Cerebro对象,它是backtrader的主要组件,用于管理策略和执行回测。 4. 添加数据:在Cerebro对象中,你需要添加数据源。backtrader支持多种数据源,包括CSV文件、Pandas DataFrame、实时数据等。 5. 添加策略:在Cerebro对象中,你需要添加之前定义的策略类。 6. 添加参数优化:使用Cerebro对象的addanalyzer方法,你可以添加参数优化的分析器。backtrader提供了多种分析器,包括参数优化分析器。 7. 运行回测:调用Cerebro对象的run方法,可以运行回测并进行参数优化。回测结果将包含每个参数组合的性能指标。 8. 获取最佳参数:通过分析回测结果,你可以获取最佳参数组合。backtrader提供了一些方法来帮助你分析和选择最佳参数。 下面是一个使用backtrader进行参数优化的示例代码: ```python import backtrader as bt class MyStrategy(bt.Strategy): params = ( ('param1', 10), ('param2', 20), ) def __init__(self): # 策略初始化逻辑 def next(self): # 策略每个时间步的逻辑 cerebro = bt.Cerebro() # 添加数据源 data = bt.feeds.YourDataFeed() cerebro.adddata(data) # 添加策略 cerebro.addstrategy(MyStrategy) # 添加参数优化分析器 cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe') # 运行回测 results = cerebro.run() # 获取最佳参数 best_params = results[0].analyzers.sharpe.get_analysis() print("Best params:", best_params) ``` 这是一个基本的示例,你可以根据自己的需求和策略进行修改和扩展。希望对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值