backtrader回测中,发现买和卖同时发生在一根K线上,请问开发者可以介入控制吗?

最近用backtrader框架编写一个策略(马丁策略)。框架如下:
def next(self):
...
self.buy(size = self.c_base_order_qty)
...
self.buy(exectype = bt.Order.Limit, price = _next_so_price, size = _next_so_qty)

非常不巧的是买入订单和卖出订单都发生在了未来的一根K线上,导致后续逻辑崩盘。

img


请问像这种情况,开发者可以介入控制吗?(比如控制买卖顺序)

  • 这篇文章:49、【backtrader股票策略】如何实现跨周期调用技术指标的策略? 也许有你想要的答案,你可以看看
  • 除此之外, 这篇博客: backtrader程序介绍-策略回测用法中的 三、进行交易策略的回测 部分也许能够解决你的问题, 你可以仔细阅读以下内容或跳转源博客中阅读:
  • 进行策略回测前,需要自定义自己的交易策略:

    # 一个新的策略,如果价格三连跌的话,买入
    class TestStrategy(bt.Strategy):
        def log(self,txt,dt=None): #此策略的日志记录功能
            dt=dt or self.datas[0].datetime.date(0)
            print('%s,%s'%(dt.isoformat(),txt))
            
        def __init__(self):  #保留对 data[0] 数据系列中“close”行的引用
            self.dataclose= self.datas[0].close
            
        def next(self): #只需从参考记录该系列的收盘价
            self.log('Close,%.2f'% self.dataclose[0])
            
            if self.dataclose[0]<self.dataclose[-1]: #当前收盘价低于上一个收盘价
                if self.dataclose[-1]<self.dataclose[-2]: #当前收盘价低于上一个收盘价
                    self.log('BUY CREATE,%.2f'%self.dataclose[0]) #创建买单
                    self.buy()  
    

    进行回测:

    if __name__== '__main__':
        cerebro=bt.Cerebro()
        
        # Add a strategy
        cerebro.addstrategy(TestStrategy)
    
    #     引入数据
        modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
        datapath=os.path.join(modpath,'C:/Users/gws/Desktop/OPTIONS_DATA/600519.csv')
    #     将数据放入Data Feeds,默认的数据集是yahoo的股票数据
    # 载入自己的数据也是可以的,只不过你需要设定每个列的含义
        data=bt.feeds.GenericCSVData(
        dataname=datapath,
        datetime=0,
        open=1,
        high=3,
        low=4,
        close=2,
        volume=5,
        dtformat=('%Y%m%d'),
    #         使用datetime来过滤价格数据 的起止范围
        fromdate=datetime.datetime(2010,1,1), #不传递此日期之前的数值
        todate=datetime.datetime(2021,12,1)  #不传递此日期之后的数值
        )
        # 将数据 Feed到Cerebro
        cerebro.adddata(data)
    #     设置投资金额
        cerebro.broker.setcash(100000.0)
        print(f'组合初始价值:%.2f'%cerebro.broker.getvalue())
    #     运行broker
        cerebro.run()
        print(f'组合期末价值:%.2f'%cerebro.broker.getvalue())
        cerebro.plot()
    

    回测文字结果:

    组合初始价值:100000.00
    2010-01-04,Close,104.59
    2010-01-05,Close,104.29
    2010-01-05,BUY CREATE,104.29
    2010-01-06,Close,102.64
    2010-01-06,BUY CREATE,102.64
    2010-01-07,Close,100.77
    2010-01-07,BUY CREATE,100.77
    2010-01-08,Close,99.71
    2010-01-08,BUY CREATE,99.71
    2010-11-01,Close,99.27
    2010-11-01,BUY CREATE,99.27
    2010-11-02,Close,101.95
    2010-11-03,Close,99.36
    ......
    2021-11-05,BUY CREATE,2062.58
    2021-11-08,Close,2043.76
    2021-11-08,BUY CREATE,2043.76
    2021-11-09,Close,1990.67
    2021-11-09,BUY CREATE,1990.67
    2021-01-20,Close,2021.60
    组合期末价值:948494.55
    
    

    回测结果图
    在这里插入图片描述
    可以看出,这个策略是盈利客观的,但是策略过于简单,在后几年的大涨行情中并没有触发。

    继续进行策略的回测

    # 不止买入,还要卖出
    # Strategy 类有一个变量 position 保存当前持有的资产数量
    # buy() 和 sell() 会返回 被创建的订单 (还未被执行的)
    # 订单状态改变后将会通知 Strategy 实例的 notify() 方法
    # 5个柱之后(在第6个时候执行)不管涨跌都卖
    # 请注意,这里没有指定具体时间,而是指定的柱的数量。一个柱可能代表1分钟、1小时、1天、1星期等等,这取决于你价格数据文件里一条数据代表的周期。
    
    # 创建一个新策略
    class TestStrategy(bt.Strategy):
        
        def log(self,txt,dt=None):
            dt=dt or self.datas[0].datetime.date(0)
            print('%s,%s'%(dt.isoformat(),txt))
            
        def __init__(self):
            self.dataclose=self.datas[0].close
            self.order=None  #跟踪挂单
            self.buyprice = None
            self.buycomm = None #加入手续费
            
        def notify_order(self,order):
            if order.status in [order.Submitted,order.Accepted]: #经纪商提交/接受/接受的买入/卖出订单 
                return
            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.buyprice = order.executed.price
                    self.buycomm = order.executed.comm
                             
                else:
                     self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                              (order.executed.price,order.executed.value,order.executed.comm))
                          
                self.bar_executed=len(self)
                
            elif order.status in [order.Canceled,order.Margin,order.Rejected]:
                self.log('Order Canceled/Margin/Rejected')
                
            self.order=None #写下:无挂单
        def next(self):
            self.log('Close,%.2f'%self.dataclose[0])
            if self.order: ## 检查订单是否处于待处理状态...如果是,我们不能发送第二个
                return
            if not self.position: #检查我们是否在市场上
                if self.dataclose[0]<self.dataclose[-1]:
                    if self.dataclose[-1]<self.dataclose[-2]:
                        self.log('BUY CREATE,%.2f'%self.dataclose[0])
                        self.order=self.buy() #跟踪创建的订单以避免第二个订单
            else:
                if len(self)>=(self.bar_executed+5):
                    self.log('SELL CREATE,%.2f'%self.dataclose[0])
                    self.order=self.sell()
    
    if __name__== '__main__':
        cerebro=bt.Cerebro()
        
        # Add a strategy
        cerebro.addstrategy(TestStrategy)
    
    #     引入数据
        modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
        datapath=os.path.join(modpath,'C:/Users/gws/Desktop/OPTIONS_DATA/600519.csv')
    #     将数据放入Data Feeds,默认的数据集是yahoo的股票数据
    # 载入自己的数据也是可以的,只不过你需要设定每个列的含义
        data=bt.feeds.GenericCSVData(
        dataname=datapath,
        datetime=0,
        open=1,
        high=3,
        low=4,
        close=2,
        volume=5,
        dtformat=('%Y%m%d'),
    #         使用datetime来过滤价格数据 的起止范围
        fromdate=datetime.datetime(2010,1,1), #不传递此日期之前的数值
        todate=datetime.datetime(2021,12,1)  #不传递此日期之后的数值
        )
        # 将数据 Feed到Cerebro
        cerebro.adddata(data)
    #     设置投资金额
        cerebro.broker.setcash(100000.0)
        
    #     加入手续费
        cerebro.broker.setcommission(commission=0.001) # 0.001 即是 0.1%
        print(f'组合初始价值:%.2f'%cerebro.broker.getvalue())
    #     运行broker
        cerebro.run()
        print(f'组合期末价值:%.2f'%cerebro.broker.getvalue())
        cerebro.plot()
    

    回测结果

    2021-01-08,Close,2070.51
    2021-11-01,Close,2080.14
    2021-11-02,Close,2140.74
    2021-11-03,Close,2143.82
    2021-11-04,Close,2114.09
    2021-11-05,Close,2062.58
    2021-11-05,BUY CREATE,2062.58
    2021-11-08,BUY EXECUTED, Price: 2041.84, Cost: 2041.84, Comm 2.04
    2021-11-08,Close,2043.76
    2021-11-09,Close,1990.67
    2021-01-20,Close,2021.60
    组合期末价值:100912.05
    

    在这里插入图片描述
    可以看出,策略盈利不多,而且没有第一种简单策略的收益高。

    加入技术指标进行回测

    # 添加技术指标
    # 收盘价高于平均价的时候,以市价买入
    
    # 持有仓位的时候,如果收盘价低于平均价,卖出
    
    # 只有一个待执行的订单
    
    # Create a Stratey
    class TestStrategy(bt.Strategy):
        params = (
            ('maperiod', 15),
        )
        def log(self, txt, dt=None):
            ''' Logging function fot this strategy'''
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))
            
        def __init__(self):
            # Keep a reference to the "close" line in the data[0] dataseries
            self.dataclose = self.datas[0].close
            self.order = None
            self.buyprice = None
            self.buycomm = None
     # 添加技术指标
            self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], period=self.params.maperiod)
        # 绘图显示的指标
            bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
            bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
                                                subplot=True)
            bt.indicators.StochasticSlow(self.datas[0])
            bt.indicators.MACDHisto(self.datas[0])
            rsi = bt.indicators.RSI(self.datas[0])
            bt.indicators.SmoothedMovingAverage(rsi, period=10)
            bt.indicators.ATR(self.datas[0], plot=False)
        
        def notify_order(self,order):
            if order.status in [order.Submitted,order.Accepted]: #经纪商提交/接受/接受的买入/卖出订单 
                return
            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.buyprice = order.executed.price
                    self.buycomm = order.executed.comm
                             
                else:
                     self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                              (order.executed.price,order.executed.value,order.executed.comm))
                          
                self.bar_executed=len(self)
                
            elif order.status in [order.Canceled,order.Margin,order.Rejected]:
                self.log('Order Canceled/Margin/Rejected')
                
            self.order=None #写下:无挂单
            
        
        def notify_trade(self, trade):
            if not trade.isclosed:
                return
            self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                     (trade.pnl, trade.pnlcomm))
    
        
            
        def next(self):
            self.log('Close,%.2f'%self.dataclose[0])
            if self.order: ## 检查订单是否处于待处理状态...如果是,我们不能发送第二个
                return
            if not self.position: #检查我们是否在市场上
                if self.dataclose[0] > self.sma[0]:
                
                    self.log('BUY CREATE,%.2f'%self.dataclose[0])
                    self.order=self.buy() #跟踪创建的订单以避免第二个订单
            else:
                if self.dataclose[0] < self.sma[0]:
                    self.log('SELL CREATE,%.2f'%self.dataclose[0])
                    self.order=self.sell()
    
    
    if __name__== '__main__':
        cerebro=bt.Cerebro()
        
        # Add a strategy
        cerebro.addstrategy(TestStrategy)
    
    #     引入数据
        modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
        datapath=os.path.join(modpath,'C:/Users/gws/Desktop/OPTIONS_DATA/600519.csv')
    #     将数据放入Data Feeds,默认的数据集是yahoo的股票数据
    # 载入自己的数据也是可以的,只不过你需要设定每个列的含义
        data=bt.feeds.GenericCSVData(
        dataname=datapath,
        datetime=0,
        open=1,
        high=3,
        low=4,
        close=2,
        volume=5,
        dtformat=('%Y%m%d'),
    #         使用datetime来过滤价格数据 的起止范围
        fromdate=datetime.datetime(2010,1,1), #不传递此日期之前的数值
        todate=datetime.datetime(2021,12,1)  #不传递此日期之后的数值
        )
        # 将数据 Feed到Cerebro
        cerebro.adddata(data)
    #     设置投资金额
        cerebro.broker.setcash(100000.0)
        
    #     加入手续费
        cerebro.broker.setcommission(commission=0.001) # 0.001 即是 0.1%
        print(f'组合初始价值:%.2f'%cerebro.broker.getvalue())
    #     运行broker
        cerebro.run()
        print(f'组合期末价值:%.2f'%cerebro.broker.getvalue())
        
        
        cerebro.plot()
    

    回测结果

    2021-11-05, Close,2062.58
    2021-11-08, Close,2043.76
    2021-11-09, Close,1990.67
    2021-11-09, SELL CREATE,1990.67
    2021-01-20, Close,2021.60
    组合期末价值:100998.60
    

    在这里插入图片描述
    回测后的收益不高。

    加入手续费后,后两者策略的表现都不好,从2020年到2021年的收益率还不如余额宝收益率,看来后两者策略在该标的上的表现不如意。