Cerebro核心模块

一、元类

  • Python中,实例由类生成,类由type生成,所有类的最顶层基类是object,所有的类都继承自object,object类是type的实例,type是Python用来创建所有类的元类。元类是用来创建类的类

  • 普通类和元类创建类的区别:

    • 普通类实例化时,先用__new__构造新的空对象,然后调用__init__方法去初始化空对象。如果要调用__call__,需要使用实例后加一个括号显式调用。
    • 元类创建类实例化时,首先调用元类的__call__,然后才是__new____init__。由于要先调用__call__,可以控制类的生成,比如可以控制生成类的参数。
  • backtrader中,基本上所有类从元类继承。MetaBase定义如下:

    class MetaBase(type):
        def doprenew(cls, *args, **kwargs):
            return cls, args, kwargs
    
        def donew(cls, *args, **kwargs):
            _obj = cls.__new__(cls, *args, **kwargs)s
            return _obj, args, kwargs
    
        def dopreinit(cls, _obj, *args, **kwargs):
            return _obj, args, kwargs
    
        def doinit(cls, _obj, *args, **kwargs):
            _obj.__init__(*args, **kwargs)
            return _obj, args, kwargs
    
        def dopostinit(cls, _obj, *args, **kwargs):
            return _obj, args, kwargs
    
        def __call__(cls, *args, **kwargs):
            cls, args, kwargs = cls.doprenew(*args, **kwargs)
            _obj, args, kwargs = cls.donew(*args, **kwargs)
            _obj, args, kwargs = cls.dopreinit(_obj, *args, **kwargs)
            _obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
            _obj, args, kwargs = cls.dopostinit(_obj, *args, **kwargs)
            return _obj
    
    
    • MetaBase类定义了doprenew/donew/dopreinit/doinit/dopostinit方法,其中donew中调用类的__new__创建对象,doinit函数调用类的__init__方法对对象进行初始化。而__call__函数直接完成预处理、实例化、初始化、后处理一系列动作,并且不需要显式调用,在类进行实例化时就会完成。

    • 所有MetaBase(或者其子类)创建的类都会先调用__call__函数,包括Cerebro、lineseries、strategies、broker、indicator等。

  • 某些功能类并没有继承MetaBase,而是继承MetaBase的子类MetaParams,MetaParams定义如下:

    class MetaParams(MetaBase):
        def __new__(meta, name, bases, dct):
            # Remove params from class definition to avoid inheritance
            # (and hence "repetition")
            newparams = dct.pop('params', ())
    
            packs = 'packages'
            newpackages = tuple(dct.pop(packs, ()))  # remove before creation
    
            fpacks = 'frompackages'
            fnewpackages = tuple(dct.pop(fpacks, ()))  # remove before creation
    
            # Create the new class - this pulls predefined "params"
            cls = super(MetaParams, meta).__new__(meta, name, bases, dct)
    
            # Pulls the param class out of it - default is the empty class
            params = getattr(cls, 'params', AutoInfoClass)
    
            # Pulls the packages class out of it - default is the empty class
            packages = tuple(getattr(cls, packs, ()))
            fpackages = tuple(getattr(cls, fpacks, ()))
    
            # get extra (to the right) base classes which have a param attribute
            morebasesparams = [x.params for x in bases[1:] if hasattr(x, 'params')]
    
            # Get extra packages, add them to the packages and put all in the class
            for y in [x.packages for x in bases[1:] if hasattr(x, packs)]:
                packages += tuple(y)
    
            for y in [x.frompackages for x in bases[1:] if hasattr(x, fpacks)]:
                fpackages += tuple(y)
    
            cls.packages = packages + newpackages
            cls.frompackages = fpackages + fnewpackages
    
            # Subclass and store the newly derived params class
            cls.params = params._derive(name, newparams, morebasesparams)
    
            return cls
    
        def donew(cls, *args, **kwargs):
            clsmod = sys.modules[cls.__module__]
            # import specified packages
            for p in cls.packages:
                if isinstance(p, (tuple, list)):
                    p, palias = p
                else:
                    palias = p
    
                pmod = __import__(p)
    
                plevels = p.split('.')
                if p == palias and len(plevels) > 1:  # 'os.path' not aliased
                    setattr(clsmod, pmod.__name__, pmod)  # set 'os' in module
    
                else:  # aliased and/or dots
                    for plevel in plevels[1:]:  # recurse down the mod
                        pmod = getattr(pmod, plevel)
    
                    setattr(clsmod, palias, pmod)
    
            # import from specified packages - the 2nd part is a string or iterable
            for p, frompackage in cls.frompackages:
                if isinstance(frompackage, string_types):
                    frompackage = (frompackage,)  # make it a tuple
    
                for fp in frompackage:
                    if isinstance(fp, (tuple, list)):
                        fp, falias = fp
                    else:
                        fp, falias = fp, fp  # assumed is string
    
                    # complain "not string" without fp (unicode vs bytes)
                    pmod = __import__(p, fromlist=[str(fp)])
                    pattr = getattr(pmod, fp)
                    setattr(clsmod, falias, pattr)
                    for basecls in cls.__bases__:
                        setattr(sys.modules[basecls.__module__], falias, pattr)
    
            # Create params and set the values from the kwargs
            params = cls.params()
            for pname, pdef in cls.params._getitems():
                setattr(params, pname, kwargs.pop(pname, pdef))
    
            # Create the object and set the params in place
            _obj, args, kwargs = super(MetaParams, cls).donew(*args, **kwargs)
            _obj.params = params
            _obj.p = params  # shorter alias
    
            # Parameter values have now been set before __init__
            return _obj, args, kwargs
    
    • donew函数分别提取package、frompackage和params并赋值,params用于提取参数,对于backtrader很关键。
  • Cerebro类中,参数定义为元组,通过setattr直接赋值到具体独立参数,方便进行访问。

    params = (
            ('preload', True),
            ('runonce', True),
            ('maxcpus', None),
            ('stdstats', True),
            ('oldbuysell', False),
            ('oldtrades', False),
            ('lookahead', 0),
            ('exactbars', False),
            ('optdatas', True),
            ('optreturn', True),
            ('objcache', False),
            ('live', False),
            ('writer', False),
            ('tradehistory', False),
            ('oldsync', False),
            ('tz', None),
            ('cheat_on_open', False),
            ('broker_coo', True),
            ('quicknotify', False),
        )
    
  • 元组在backtrader中的应用:

    • 所有类实例化时通过元类的__call__完成,不同的类可以进行一些特殊化的处理。
    • 参数通过donew完成映射。
  • 元类在backtrader用于处理模板化工作,让具体功能类专注于功能实现。

二、Cerebro

1、Cerebro简介

  • Cerebro是backtrader的核心控制系统,构建了策略回测的基础框架,具体细节则在组件内部实现。Cerebro主要工作包括:

    • 收集所有的输入(data feeds),执行者(strategies),观测者(Observers)、评价者(Analyzers)以及文档(Writers),保证系统在任何时候都正常运行。
    • 执行回测或者实时数据输入以及交易。
    • 返回结果。
    • 画图。
  • Cerebro类继承自MetaParams,代码如下:

    class Cerebro(with_metaclass(MetaParams, object)):
        params = (
            ('preload', True),
            ('runonce', True),
            ('maxcpus', None),
            ('stdstats', True),
            ('oldbuysell', False),
            ('oldtrades', False),
            ('lookahead', 0),
            ('exactbars', False),
            ('optdatas', True),
            ('optreturn', True),
            ('objcache', False),
            ('live', False),
            ('writer', False),
            ('tradehistory', False),
            ('oldsync', False),
            ('tz', None),
            ('cheat_on_open', False),
            ('broker_coo', True),
            ('quicknotify', False),
        )
    
        def __init__(self):
            self._dolive = False
            self._doreplay = False
            self._dooptimize = False
            self.stores = list()
            self.feeds = list()
            self.datas = list()
            self.datasbyname = collections.OrderedDict()
            self.strats = list()
            self.optcbs = list()  # holds a list of callbacks for opt strategies
            self.observers = list()
            self.analyzers = list()
            self.indicators = list()
            self.sizers = dict()
            self.writers = list()
            self.storecbs = list()
            self.datacbs = list()
            self.signals = list()
            self._signal_strat = (None, None, None)
            self._signal_concurrent = False
            self._signal_accumulate = False
    
            self._dataid = itertools.count(1)
    
            self._broker = BackBroker()
            self._broker.cerebro = self
    
            self._tradingcal = None  # TradingCalendar()
    
            self._pretimers = list()
            self._ohistory = list()
            self._fhistory = None
    
        @staticmethod
        def iterize(iterable):
    
        def set_fund_history(self, fund):
    
        def add_order_history(self, orders, notify=True):
    
        def notify_timer(self, timer, when, *args, **kwargs):
    
        def add_timer(self, when,
                      offset=datetime.timedelta(), repeat=datetime.timedelta(),
                      weekdays=[], weekcarry=False,
                      monthdays=[], monthcarry=True,
                      allow=None,
                      tzdata=None, strats=False, cheat=False,
                      *args, **kwargs):
    
        def addtz(self, tz):
    
        def addcalendar(self, cal):
    
        def add_signal(self, sigtype, sigcls, *sigargs, **sigkwargs):
    
        def signal_strategy(self, stratcls, *args, **kwargs):
    
        def signal_concurrent(self, onoff):
    
        def signal_accumulate(self, onoff):
    
        def addstore(self, store):
    
        def addwriter(self, wrtcls, *args, **kwargs):
    
        def addsizer(self, sizercls, *args, **kwargs):
    
        def addsizer_byidx(self, idx, sizercls, *args, **kwargs):
    
        def addindicator(self, indcls, *args, **kwargs):
    
        def addanalyzer(self, ancls, *args, **kwargs):
    
        def addobserver(self, obscls, *args, **kwargs):
    
        def addobservermulti(self, obscls, *args, **kwargs):
    
        def addstorecb(self, callback):
    
        def notify_store(self, msg, *args, **kwargs):
    
        def adddatacb(self, callback):
    
        def notify_data(self, data, status, *args, **kwargs):
    
        def adddata(self, data, name=None):
    
        def chaindata(self, *args, **kwargs):
    
        def rolloverdata(self, *args, **kwargs):
    
        def replaydata(self, dataname, name=None, **kwargs):
    
        def resampledata(self, dataname, name=None, **kwargs):
    
        def optcallback(self, cb):
    
        def optstrategy(self, strategy, *args, **kwargs):
    
        def addstrategy(self, strategy, *args, **kwargs):
    
        def setbroker(self, broker):
    
        def getbroker(self):
    
        broker = property(getbroker, setbroker)
    
        def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None,
                 width=16, height=9, dpi=300, tight=True, use=None,
                 **kwargs):
    
        def __call__(self, iterstrat):
    
        def __getstate__(self):
    
        def runstop(self):
    
        def run(self, **kwargs):
    
        def runstrategies(self, iterstrat, predata=False):
    
        def stop_writers(self, runstrats):
    

2、Cerebro初始化

  • Cerebro通过如下代码进行初始化:

    cerebro = bt.Cerebro(**kwargs)
    
    • 首先调用Cerebro类的__init__函数初始化类的属性。

      def __init__(self):
              self._dolive = False
              self._doreplay = False
              self._dooptimize = False
              self.stores = list()
              self.feeds = list()
              self.datas = list()
              self.datasbyname = collections.OrderedDict()
              self.strats = list()
              self.optcbs = list()  # holds a list of callbacks for opt strategies
              self.observers = list()
              self.analyzers = list()
              self.indicators = list()
              self.sizers = dict()
              self.writers = list()
              self.storecbs = list()
              self.datacbs = list()
              self.signals = list()
              self._signal_strat = (None, None, None)
              self._signal_concurrent = False
              self._signal_accumulate = False
              self._dataid = itertools.count(1)
              self._broker = BackBroker()#初始化一个缺省的broker实例。
              self._broker.cerebro = self
              self._tradingcal = None  # TradingCalendar()
              self._pretimers = list()
              self._ohistory = list()
              self._fhistory = None
      
      名称定义说明
      storeslist用于存储接收到的其它组件(例如data)消息。
      feedslist用于存储数据源(data Feeds)的基类。
      dataslist用于存储数据源。feeds是data的基类。
      datasbynameOrderedDict存储数据源的名称,存储的方式是有序的字典。
      stratslist用于存储策略(strategies)类
      optcbslist存储优化策略的回调。对执行完的优化策略进行处理的回调。
      observerslist保存observers类
      analyzerslist保存analyzers类
      indicatorslist保存indicators类
      writerslist保存writers类
      storecbslist保存对notify_store消息处理的回调.对于收到存储在store消息中可以定制处理过程。
      datacbslist保存对data类消息处理的回调
      signalslist保存信号,用于通过signal strategy进行回测的机制。
      sizersdict保存sizers类
    • Cerebro类还支持一系列的参数(在元类中统一进行处理),实例化的时候通过**kwargs传递,参数以元组的元组形式定义,如下:

      params = (
              ('preload', True),
              ('runonce', True),
              ('maxcpus', None),
              ('stdstats', True),
              ('oldbuysell', False),
              ('oldtrades', False),
              ('lookahead', 0),
              ('exactbars', False),
              ('optdatas', True),
              ('optreturn', True),
              ('objcache', False),
              ('live', False),
              ('writer', False),
              ('tradehistory', False),
              ('oldsync', False),
              ('tz', None),
              ('cheat_on_open', False),
              ('broker_coo', True),
              ('quicknotify', False),
          )
      
      参数取值范围缺省值含义
      preloadTrue FalseTRUE是否为Strategies预加载传递给Cerebro的不同数据源。
      runonceTrue FalseTRUErunonce是indicators类对数据访问方法的优化以加快速度,使用矢量化模式来提高算法的运行速度。strategies和Observers通常是基于事件对数据进行处理。
      maxcpusNone->运行系统可用的核None指定可以用于优化的CPU核
      stdstatsTrue FalseTrue如果为True,创建缺省Observers,包括Broker、Buysell和Trades。
      oldbuysellTrue FalseFALSE创建Observers时,使用新buysell还是旧版本实现,没有特殊需求,使用新代码。
      oldtradesTrue FalseFALSE创建Observers时,使用新buysell还是旧版本实现,没有特殊需求,使用新代码。
      lookahead数值0用于扩展数据时,扩大数据的缓存的大小。通常设置缺省值即可。
      exactbarsFalse,数值FALSE用于指示如何缓存数据以节约内存。False是Lines对象数据都会加入到内存以方便计算。如果采用其它值,会有一些特殊处理减少内存,可能对画图、runonce等机制造成影响。缺省False。
      optdatasTrue FalseTrue速度处理的优化机制,主要是数据预装载、runonce等,可以提高效率。
      optreturnTrue FalseTrue对不同类的优化机制,可以提高效率,设置为缺省值即可。
      objcacheTrue FalseFalse实验性质的方法,没啥用,缺省值
      liveTrue FalseFalse是否实时输入数据。
      writerTrue FalseFalse设置为True,为自动创建一个writer,缺省输出日志到stdout。
      tradehistoryTrue FalseFalse设置为True,会记录所有策略的每一次交易信息。也可以在strategies里面设置
      oldsyncTrue FalseFalse新版本后(1.9.0.99之后)提供新来的datas同步机制。如果要用老的同步机制,可以设置为True。
      tzNone,StringNone记录时区信息,缺省None,使用的就是UTC,对于中国是’UTC+8’。对于回测,时区并不重要。
      cheat_on_openTrue FalseFalse设置为True,会调用strategies的next_open方法,在next前调用,主要用于在对订单的评估,可以基于前一天的open价发起一个订单。
      broker_cooTrue FalseTrue设置为True,broker调用set_coo开启’cheat_on_open’,此时cheat_on_open也必须打开。
      quicknotifyTrue FalseFalse在next前发起broker的通知。只有实时数据可以快速通知

3、Cerebro添加数据源

  • 增加数据源最常见的方式就是cerebro.adddata(data),data必须是已经实例化的data feed。

    data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)
    
  • 也可以通过resample和replay添加数据源:

    cerebro.resampledata(data,timeframe=bt.TimeFrame.Weeks) 
    cerebro.replaydatadata(data, timeframe=bt.TimeFrame.Days)
    
  • backtrader可以接受任何数量的data feeds,包括普通数据以及resample/replay的数据。

  • adddata代码如下:

    def adddata(self, data, name=None):
        '''
            Adds a ``Data Feed`` instance to the mix.
            If ``name`` is not None it will be put into ``data._name`` which is
            meant for decoration/plotting purposes.
            '''
        if name is not None:
            data._name = name
    
            data._id = next(self._dataid)
            data.setenvironment(self)
    
            self.datas.append(data)
            self.datasbyname[data._name] = data
            feed = data.getfeed()
            if feed and feed not in self.feeds:
                self.feeds.append(feed)
    
                if data.islive():
                    self._dolive = True
    
                    return data
    
    • 记录下data的名称
    • 分配data的ID,从1开始。
    • Cerebro实例和data建立关联关系:data调用setenvironment记录自己现在归当前Cerebro管,同时Cerebro将data加入到datas列表中。
    • 获取data的feed,如果feed有效,加入到feeds列表中。feed是data的基类。
    • 记录是否实时数据。
  • resampdata的代码如下:

    def resampledata(self, dataname, name=None, **kwargs):
            '''
            Adds a ``Data Feed`` to be resample by the system
    
            If ``name`` is not None it will be put into ``data._name`` which is
            meant for decoration/plotting purposes.
    
            Any other kwargs like ``timeframe``, ``compression``, ``todate`` which
            are supported by the resample filter will be passed transparently
            '''
            if any(dataname is x for x in self.datas):
                dataname = dataname.clone()
    
            dataname.resample(**kwargs)
            self.adddata(dataname, name=name)
            self._doreplay = True
    
            return dataname
    
    • 判断resample目的dataname(data对象实例)是否已经保存在datas列表中。如果已经保存,那么不能直接resample,需要clone一个新的data对象。
    • 调用data feeds的resample函数对数据进行处理。
    • 直接调用adddata函数处理,同时记录._doreplay标记。
  • replaydata的代码:

    def replaydata(self, dataname, name=None, **kwargs):
            '''
            Adds a ``Data Feed`` to be replayed by the system
    
            If ``name`` is not None it will be put into ``data._name`` which is
            meant for decoration/plotting purposes.
    
            Any other kwargs like ``timeframe``, ``compression``, ``todate`` which
            are supported by the replay filter will be passed transparently
            '''
            if any(dataname is x for x in self.datas):
                dataname = dataname.clone()
    
            dataname.replay(**kwargs)
            self.adddata(dataname, name=name)
            self._doreplay = True
    
            return dataname
    

4、Cerebro添加策略

  • Cerebro添加策略代码如下:

    cerebro.addstrategy(TestStrategy)
    cerebro.optstrategy(TestStrategy, maperiod=range(3,15))
    
  • addstrategy代码实现:

    def addstrategy(self, strategy, *args, **kwargs):
        '''
            Adds a ``Strategy`` class to the mix for a single pass run.
            Instantiation will happen during ``run`` time.
    
            args and kwargs will be passed to the strategy as they are during
            instantiation.
    
            Returns the index with which addition of other objects (like sizers)
            can be referenced
            '''
        self.strats.append([(strategy, args, kwargs)])
        return len(self.strats) - 1
    
    • 将strategy类及其参数加入到Cerebro中用于存储策略的容器中(strats),返回索引。strategies只有在run时才会实例化。
  • optstrategy实现代码如下:

        def optstrategy(self, strategy, *args, **kwargs):
            '''
            Adds a ``Strategy`` class to the mix for optimization. Instantiation
            will happen during ``run`` time.
    
            args and kwargs MUST BE iterables which hold the values to check.
    
            Example: if a Strategy accepts a parameter ``period``, for optimization
            purposes the call to ``optstrategy`` looks like:
    
              - cerebro.optstrategy(MyStrategy, period=(15, 25))
    
            This will execute an optimization for values 15 and 25. Whereas
    
              - cerebro.optstrategy(MyStrategy, period=range(15, 25))
    
            will execute MyStrategy with ``period`` values 15 -> 25 (25 not
            included, because ranges are semi-open in Python)
    
            If a parameter is passed but shall not be optimized the call looks
            like:
    
              - cerebro.optstrategy(MyStrategy, period=(15,))
    
            Notice that ``period`` is still passed as an iterable ... of just 1
            element
    
            ``backtrader`` will anyhow try to identify situations like:
    
              - cerebro.optstrategy(MyStrategy, period=15)
    
            and will create an internal pseudo-iterable if possible
            '''
            self._dooptimize = True
            args = self.iterize(args)
            optargs = itertools.product(*args)
    
            optkeys = list(kwargs)
    
            vals = self.iterize(kwargs.values())
            optvals = itertools.product(*vals)
    
            okwargs1 = map(zip, itertools.repeat(optkeys), optvals)
    
            optkwargs = map(dict, okwargs1)
    
            it = itertools.product([strategy], optargs, optkwargs)
            self.strats.append(it)
    

5、Cerebro更改Broker

  • Cerebro初始化时实例化了一个缺省broker,如果需要提供自己的broker,可以通过如下方法更改:

    broker = MyBroker()
    cerebro.broker = broker
    
  • Cerebro代码实现中提供了property装饰器broker = property(getbroker, setbroker),等同通过setbroker/getbroker设置/读取。

6、Cerebro添加评价指标

  • Cerebro添加评价指标代码如下:

    tframes = dict(
        days=bt.TimeFrame.Days,
        weeks=bt.TimeFrame.Weeks,
        months=bt.TimeFrame.Months,
        years=bt.TimeFrame.Years)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='SharpeRatio')
    cerebro.addanalyzer(bt.analyzers.DrawDown,_name = 'DrawDown')
    cerebro.addanalyzer(bt.analyzers.AnnualReturn,_name = 'AnnualReturn')
    cerebro.addanalyzer(bt.analyzers.SQN,_name = 'SQN')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer,_name = 'TradeAnalyzer')
    cerebro.addanalyzer(bt.analyzers.PositionsValue,_name = 'PositionsValue')
    cerebro.addanalyzer(bt.analyzers.Returns,_name = 'Returns')
    cerebro.addanalyzer(bt.analyzers.LogReturnsRolling,timeframe=tframes['years'],_name = 'LogReturnsRolling')
    cerebro.addanalyzer(bt.analyzers.Transactions, _name='Transactions')
    
  • addanalyzer实现代码如下:

    def addanalyzer(self, ancls, *args, **kwargs):
        '''
            Adds an ``Analyzer`` class to the mix. Instantiation will be done at
            ``run`` time
            '''
        self.analyzers.append((ancls, args, kwargs))
    

7、Cerebro添加观测器

  • Cerebro添加观测器代码如下:

    cerebro.addobserver(bt.observers.Broker) 
    cerebro.addobserver(bt.observers.Trades) 
    cerebro.addobserver(bt.observers.BuySell) 
    
  • addobserver实现代码:

        def addobserver(self, obscls, *args, **kwargs):
            '''
                Adds an ``Observer`` class to the mix. Instantiation will be done at
                ``run`` time
                '''
            self.observers.append((False, obscls, args, kwargs))
    
        def addobservermulti(self, obscls, *args, **kwargs):
            '''
            Adds an ``Observer`` class to the mix. Instantiation will be done at
            ``run`` time
    
            It will be added once per "data" in the system. A use case is a
            buy/sell observer which observes individual datas.
    
            A counter-example is the CashValue, which observes system-wide values
            '''
            self.observers.append((True, obscls, args, kwargs))
    

8、Cerebro运行

  • 运行代码如下:

    results = cerebro.run(**kwargs)
    
  • Cerebro运行参数如下:

    • preload:是否为Strategies预加载传递给Cerebro的不同数据源,默认为False。

    • runonce:是否以矢量化模式运行Indicators以加速整个系统,默认为True。

    • live:是否为实时数据,默认为False,是回测数据。实时数据时会停用preload和runonce,但对内存节省方案没有影响。

    • maxcpus:同时使用多少个内核进行优化,默认值为None,表示所有可用CPU。

    • stdstats:是否添加默认观测器:Broker、Trade和BuySell,默认值为True。

    • objcache:是否为line对象的缓存,默认为False。

    • writer:是否将标准信息的输出生成一个默认文件,默认为False。

    • tradehistory:是否将所有策略的每笔交易中激活更新事件记录log,默认False。

    • optdatas:是否进行数据优化,数据预加载将在主进程中只进行一次,以节省时间和资源,默认为True,但需要preload和runonce也为True。

    • optreturn:是否只优化params属性和analyzers,可以在一定程度优化速度,默认为True。

    • oldsync:是否使用旧版本数据同步,默认为False。从版本1.9.0.99开始,多个数据(相同或不同时间范围)的同步已更改为允许不同长度的数据。

    • tz:为策略添加全球时区,默认为None。

      • None:策略显示日期时间将采用UTC。

      • pytz 实例:将UTC时间转换为所选时区

      • string:将尝试实例化pytz实例。

    • cheat_on_open:是否使用cheat_on_open订单,默认为False。当为True时next_open调用发生在next方法调用前,此时指标尚未重新计算,允许发送一个考虑前一天指标但使用open价格计算的订单。

    • broker_coo:是否自动调用set_coo代理的方法来激活cheat_on_open执行,默认为True。

    • quicknotify:Broker通知在下一个价格交付前交付 。对于回溯测试,没有任何影响;对于实盘Broker,可以在bar交付前很久就发出通知。

  • run代码实现如下:

        def run(self, **kwargs):
            '''The core method to perform backtesting. Any ``kwargs`` passed to it
            will affect the value of the standard parameters ``Cerebro`` was
            instantiated with.
    
            If ``cerebro`` has not datas the method will immediately bail out.
    
            It has different return values:
    
              - For No Optimization: a list contanining instances of the Strategy
                classes added with ``addstrategy``
    
              - For Optimization: a list of lists which contain instances of the
                Strategy classes added with ``addstrategy``
            '''
            self._event_stop = False  # Stop is requested
    
            if not self.datas:
                return []  # nothing can be run
    
            pkeys = self.params._getkeys()
            for key, val in kwargs.items():
                if key in pkeys:
                    setattr(self.params, key, val)
    
            # Manage activate/deactivate object cache
            linebuffer.LineActions.cleancache()  # clean cache
            indicator.Indicator.cleancache()  # clean cache
    
            linebuffer.LineActions.usecache(self.p.objcache)
            indicator.Indicator.usecache(self.p.objcache)
    
            self._dorunonce = self.p.runonce
            self._dopreload = self.p.preload
            self._exactbars = int(self.p.exactbars)
    
            if self._exactbars:
                self._dorunonce = False  # something is saving memory, no runonce
                self._dopreload = self._dopreload and self._exactbars < 1
    
            self._doreplay = self._doreplay or any(x.replaying for x in self.datas)
            if self._doreplay:
                # preloading is not supported with replay. full timeframe bars
                # are constructed in realtime
                self._dopreload = False
    
            if self._dolive or self.p.live:
                # in this case both preload and runonce must be off
                self._dorunonce = False
                self._dopreload = False
    
            self.runwriters = list()
    
            # Add the system default writer if requested
            if self.p.writer is True:
                wr = WriterFile()
                self.runwriters.append(wr)
    
            # Instantiate any other writers
            for wrcls, wrargs, wrkwargs in self.writers:
                wr = wrcls(*wrargs, **wrkwargs)
                self.runwriters.append(wr)
    
            # Write down if any writer wants the full csv output
            self.writers_csv = any(map(lambda x: x.p.csv, self.runwriters))
    
            self.runstrats = list()
    
            if self.signals:  # allow processing of signals
                signalst, sargs, skwargs = self._signal_strat
                if signalst is None:
                    # Try to see if the 1st regular strategy is a signal strategy
                    try:
                        signalst, sargs, skwargs = self.strats.pop(0)
                    except IndexError:
                        pass  # Nothing there
                    else:
                        if not isinstance(signalst, SignalStrategy):
                            # no signal ... reinsert at the beginning
                            self.strats.insert(0, (signalst, sargs, skwargs))
                            signalst = None  # flag as not presetn
    
                if signalst is None:  # recheck
                    # Still None, create a default one
                    signalst, sargs, skwargs = SignalStrategy, tuple(), dict()
    
                # Add the signal strategy
                self.addstrategy(signalst,
                                 _accumulate=self._signal_accumulate,
                                 _concurrent=self._signal_concurrent,
                                 signals=self.signals,
                                 *sargs,
                                 **skwargs)
    
            if not self.strats:  # Datas are present, add a strategy
                self.addstrategy(Strategy)
    
            iterstrats = itertools.product(*self.strats)
            if not self._dooptimize or self.p.maxcpus == 1:
                # If no optimmization is wished ... or 1 core is to be used
                # let's skip process "spawning"
                for iterstrat in iterstrats:
                    runstrat = self.runstrategies(iterstrat)
                    self.runstrats.append(runstrat)
                    if self._dooptimize:
                        for cb in self.optcbs:
                            cb(runstrat)  # callback receives finished strategy
            else:
                if self.p.optdatas and self._dopreload and self._dorunonce:
                    for data in self.datas:
                        data.reset()
                        if self._exactbars < 1:  # datas can be full length
                            data.extend(size=self.params.lookahead)
                        data._start()
                        if self._dopreload:
                            data.preload()
    
                pool = multiprocessing.Pool(self.p.maxcpus or None)
                for r in pool.imap(self, iterstrats):
                    self.runstrats.append(r)
                    for cb in self.optcbs:
                        cb(r)  # callback receives finished strategy
    
                pool.close()
    
                if self.p.optdatas and self._dopreload and self._dorunonce:
                    for data in self.datas:
                        data.stop()
    
            if not self._dooptimize:
                # avoid a list of list for regular cases
                return self.runstrats[0]
    
            return self.runstrats
    
    • 记录_event_stop为False,标识开始启动,需要通过stop来停止。
    • 检查是否有数据源(datas),没有则退出。
    • 如果run函数携带参数是否在Cerebro的参数表中,更新Cerebro实例的参数。Cerebro参数在初始化时可以设置,在run时也可以设置。
    • 处理参数。根据参数设置一些标记,清空缓存,为系统运行做好准备。
    • 如果参数标识要求提供writers(记录日志),有则实例化一个缺省writers。同时检查是否还有通过addwriter添加的其它的writers,有则实例化。将writers加入到runwriters容器,然后检查writers有没有参数要求输出csv,任意writers要求输出CSV,则记录到writers_csv标识。
    • 初始化runstrats策略运行容器。
    • 查看_signal_strat容器中有没有记录信号策略类(SignalStrategy,通过signal_strategy加入)。如果没有,则查看普通strategies容器strats第一个策略是不是SignalStrategy类;如果不是,返回SignalStrategy,创建一个空的signal_strategy。用户自己通过signal_strategy函数添加的SignalStrategy,普通strategy容器中的SignalStrategy以及缺省创建的SignalStrategy,三个按优先顺序,必须插入一个信号策略类,保存在普通strategies容器strats中,只是参数设置为signal。如果要使用信号回测方式,那么一定要加一个信号策略类
    • 如果用户没有设定策略,Cerebro添加一个缺省strategies类。
    • 使用product将所有的strategies类存放到iterstrats迭代器中。
    • 如果不进行性能优化或者参数中maxcpus为1,那么直接按循序调用runstrategies执行策略。如果是优化策略(通过optstrategy添加的策略),策略执行完成还可以调用回调(回调可通过optcallback增加)进行处理。
    • 如果需要进行性能优化处理:首先对数据进行reset/start/preload处理,然后根据maxcpus参数启用多进程,在多进程中调用runstrategies进行策略运行。对于优化策略,策略执行完毕时进行回调处理。策略执行完毕时调用data的stop方法。

9、策略运行机制

  • runstrategies函数代码如下:

        def runstrategies(self, iterstrat, predata=False):
            '''
            Internal method invoked by ``run```to run a set of strategies
            '''
            self._init_stcount()
    
            self.runningstrats = runstrats = list()
            for store in self.stores:
                store.start()
    
            if self.p.cheat_on_open and self.p.broker_coo:
                # try to activate in broker
                if hasattr(self._broker, 'set_coo'):
                    self._broker.set_coo(True)
    
            if self._fhistory is not None:
                self._broker.set_fund_history(self._fhistory)
    
            for orders, onotify in self._ohistory:
                self._broker.add_order_history(orders, onotify)
    
            self._broker.start()
    
            for feed in self.feeds:
                feed.start()
    
            if self.writers_csv:
                wheaders = list()
                for data in self.datas:
                    if data.csv:
                        wheaders.extend(data.getwriterheaders())
    
                for writer in self.runwriters:
                    if writer.p.csv:
                        writer.addheaders(wheaders)
    
            # self._plotfillers = [list() for d in self.datas]
            # self._plotfillers2 = [list() for d in self.datas]
    
            if not predata:
                for data in self.datas:
                    data.reset()
                    if self._exactbars < 1:  # datas can be full length
                        data.extend(size=self.params.lookahead)
                    data._start()
                    if self._dopreload:
                        data.preload()
    
            for stratcls, sargs, skwargs in iterstrat:
                sargs = self.datas + list(sargs)
                try:
                    strat = stratcls(*sargs, **skwargs)
                except bt.errors.StrategySkipError:
                    continue  # do not add strategy to the mix
    
                if self.p.oldsync:
                    strat._oldsync = True  # tell strategy to use old clock update
                if self.p.tradehistory:
                    strat.set_tradehistory()
                runstrats.append(strat)
    
            tz = self.p.tz
            if isinstance(tz, integer_types):
                tz = self.datas[tz]._tz
            else:
                tz = tzparse(tz)
    
            if runstrats:
                # loop separated for clarity
                defaultsizer = self.sizers.get(None, (None, None, None))
                for idx, strat in enumerate(runstrats):
                    if self.p.stdstats:
                        strat._addobserver(False, observers.Broker)
                        if self.p.oldbuysell:
                            strat._addobserver(True, observers.BuySell)
                        else:
                            strat._addobserver(True, observers.BuySell,
                                               barplot=True)
    
                        if self.p.oldtrades or len(self.datas) == 1:
                            strat._addobserver(False, observers.Trades)
                        else:
                            strat._addobserver(False, observers.DataTrades)
    
                    for multi, obscls, obsargs, obskwargs in self.observers:
                        strat._addobserver(multi, obscls, *obsargs, **obskwargs)
    
                    for indcls, indargs, indkwargs in self.indicators:
                        strat._addindicator(indcls, *indargs, **indkwargs)
    
                    for ancls, anargs, ankwargs in self.analyzers:
                        strat._addanalyzer(ancls, *anargs, **ankwargs)
    
                    sizer, sargs, skwargs = self.sizers.get(idx, defaultsizer)
                    if sizer is not None:
                        strat._addsizer(sizer, *sargs, **skwargs)
    
                    strat._settz(tz)
                    strat._start()
    
                    for writer in self.runwriters:
                        if writer.p.csv:
                            writer.addheaders(strat.getwriterheaders())
    
                if not predata:
                    for strat in runstrats:
                        strat.qbuffer(self._exactbars, replaying=self._doreplay)
    
                for writer in self.runwriters:
                    writer.start()
    
                # Prepare timers
                self._timers = []
                self._timerscheat = []
                for timer in self._pretimers:
                    # preprocess tzdata if needed
                    timer.start(self.datas[0])
    
                    if timer.params.cheat:
                        self._timerscheat.append(timer)
                    else:
                        self._timers.append(timer)
    
                if self._dopreload and self._dorunonce:
                    if self.p.oldsync:
                        self._runonce_old(runstrats)
                    else:
                        self._runonce(runstrats)
                else:
                    if self.p.oldsync:
                        self._runnext_old(runstrats)
                    else:
                        self._runnext(runstrats)
    
                for strat in runstrats:
                    strat._stop()
    
            self._broker.stop()
    
            if not predata:
                for data in self.datas:
                    data.stop()
    
            for feed in self.feeds:
                feed.stop()
    
            for store in self.stores:
                store.stop()
    
            self.stop_writers(runstrats)
    
            if self._dooptimize and self.p.optreturn:
                # Results can be optimized
                results = list()
                for strat in runstrats:
                    for a in strat.analyzers:
                        a.strategy = None
                        a._parent = None
                        for attrname in dir(a):
                            if attrname.startswith('data'):
                                setattr(a, attrname, None)
    
                    oreturn = OptReturn(strat.params, analyzers=strat.analyzers, strategycls=type(strat))
                    results.append(oreturn)
    
                return results
    
            return runstrats
    
    • 策略运行计数。

    • 初始化runningstrats容器,用于存储正在运行的策略。

    • 对组件参数设置和初始化:启动信号存储(store);broker参数设置和启动;启动feed;设置Writers参数。

    • 如果predata为False,调用preload预加载数据。部分性能优化情况下,在run时就执行preload。

    • 从iterstrat迭代器(run调用runningstrats函数携带参数)提取每一个strategy和参数,进行strategy的实例化和初始化,并返回正在执行的strategy的实例,记录到runstrats容器中。策略对象实例化使用元类执行:

      strat = stratcls(*sargs, **skwargs)
      
    • 记录下时区,用datas类存储的时区或者参数输入时区。

    • 针对strategy实例分别加入Observers、indicators、analyzer以及sizer类,并调用start函数开始启动。

    • 针对writers(存储在runwriters中,run时实例化),设置表头,并启动。

    • 设定定时器,并对strategies执行_runonce/_runonce_old/_runnext_old/_runnexth函数。

    • 停止各个组件。对于优化策略,需要对运行结果进行分析。

  • _runnext函数如下:

        def _runnext(self, runstrats):
            '''
            Actual implementation of run in full next mode. All objects have its
            ``next`` method invoke on each data arrival
            '''
            datas = sorted(self.datas,
                           key=lambda x: (x._timeframe, x._compression))
            datas1 = datas[1:]
            data0 = datas[0]
            d0ret = True
    
            rs = [i for i, x in enumerate(datas) if x.resampling]
            rp = [i for i, x in enumerate(datas) if x.replaying]
            rsonly = [i for i, x in enumerate(datas)
                      if x.resampling and not x.replaying]
            onlyresample = len(datas) == len(rsonly)
            noresample = not rsonly
    
            clonecount = sum(d._clone for d in datas)
            ldatas = len(datas)
            ldatas_noclones = ldatas - clonecount
            lastqcheck = False
            dt0 = date2num(datetime.datetime.max) - 2  # default at max
            while d0ret or d0ret is None:
                # if any has live data in the buffer, no data will wait anything
                newqcheck = not any(d.haslivedata() for d in datas)
                if not newqcheck:
                    # If no data has reached the live status or all, wait for
                    # the next incoming data
                    livecount = sum(d._laststatus == d.LIVE for d in datas)
                    newqcheck = not livecount or livecount == ldatas_noclones
    
                lastret = False
                # Notify anything from the store even before moving datas
                # because datas may not move due to an error reported by the store
                self._storenotify()
                if self._event_stop:  # stop if requested
                    return
                self._datanotify()
                if self._event_stop:  # stop if requested
                    return
    
                # record starting time and tell feeds to discount the elapsed time
                # from the qcheck value
                drets = []
                qstart = datetime.datetime.utcnow()
                for d in datas:
                    qlapse = datetime.datetime.utcnow() - qstart
                    d.do_qcheck(newqcheck, qlapse.total_seconds())
                    drets.append(d.next(ticks=False))
    
                d0ret = any((dret for dret in drets))
                if not d0ret and any((dret is None for dret in drets)):
                    d0ret = None
    
                if d0ret:
                    dts = []
                    for i, ret in enumerate(drets):
                        dts.append(datas[i].datetime[0] if ret else None)
    
                    # Get index to minimum datetime
                    if onlyresample or noresample:
                        dt0 = min((d for d in dts if d is not None))
                    else:
                        dt0 = min((d for i, d in enumerate(dts)
                                   if d is not None and i not in rsonly))
    
                    dmaster = datas[dts.index(dt0)]  # and timemaster
                    self._dtmaster = dmaster.num2date(dt0)
                    self._udtmaster = num2date(dt0)
    
                    # slen = len(runstrats[0])
                    # Try to get something for those that didn't return
                    for i, ret in enumerate(drets):
                        if ret:  # dts already contains a valid datetime for this i
                            continue
    
                        # try to get a data by checking with a master
                        d = datas[i]
                        d._check(forcedata=dmaster)  # check to force output
                        if d.next(datamaster=dmaster, ticks=False):  # retry
                            dts[i] = d.datetime[0]  # good -> store
                            # self._plotfillers2[i].append(slen)  # mark as fill
                        else:
                            # self._plotfillers[i].append(slen)  # mark as empty
                            pass
    
                    # make sure only those at dmaster level end up delivering
                    for i, dti in enumerate(dts):
                        if dti is not None:
                            di = datas[i]
                            rpi = False and di.replaying   # to check behavior
                            if dti > dt0:
                                if not rpi:  # must see all ticks ...
                                    di.rewind()  # cannot deliver yet
                                # self._plotfillers[i].append(slen)
                            elif not di.replaying:
                                # Replay forces tick fill, else force here
                                di._tick_fill(force=True)
    
                            # self._plotfillers2[i].append(slen)  # mark as fill
    
                elif d0ret is None:
                    # meant for things like live feeds which may not produce a bar
                    # at the moment but need the loop to run for notifications and
                    # getting resample and others to produce timely bars
                    for data in datas:
                        data._check()
                else:
                    lastret = data0._last()
                    for data in datas1:
                        lastret += data._last(datamaster=data0)
    
                    if not lastret:
                        # Only go extra round if something was changed by "lasts"
                        break
    
                # Datas may have generated a new notification after next
                self._datanotify()
                if self._event_stop:  # stop if requested
                    return
    
                if d0ret or lastret:  # if any bar, check timers before broker
                    self._check_timers(runstrats, dt0, cheat=True)
                    if self.p.cheat_on_open:
                        for strat in runstrats:
                            strat._next_open()
                            if self._event_stop:  # stop if requested
                                return
    
                self._brokernotify()
                if self._event_stop:  # stop if requested
                    return
    
                if d0ret or lastret:  # bars produced by data or filters
                    self._check_timers(runstrats, dt0, cheat=False)
                    for strat in runstrats:
                        strat._next()
                        if self._event_stop:  # stop if requested
                            return
    
                        self._next_writers(runstrats)
    
            # Last notification chance before stopping
            self._datanotify()
            if self._event_stop:  # stop if requested
                return
            self._storenotify()
            if self._event_stop:  # stop if requested
                return
    
    • 按照周期大小排序datas,将具有最小周期的data作为data0,其它放到datas1容器中。
    • 将resample和replay的数据索引单独保存,做好标记。计算clone数据(加入resample和replay数据时都会clone)的个数、非clone数据的个数以及数据总的数量。
    • dt0初始化最大值为最大日期(9999年12月31日)的多边格里高利度序数(每一日期对应一个独一无二的数,方便程序处理)。
    • 如果所有数据是live或者没有新数据,需要等待。
    • 识别有没有收到stop消息(从store容器中读取),在等待数据的时候,可能因为失败没有进一步的数据。
    • 所有的data都会调用do_qcheck以及next。
    • 后面一段确实比较难以理解,其总体思路是寻找主data(dmaster),通常应该是周期最短的数据。然后其他数据以此数据作为基准。(目前理解有限,后续在解读Data相关类的时候针对此场景再补充分析)
      在此过程中,需要调用_brokernotify和_datanotify检测是否有停止信号。
    • 数据完备(bar成功产生之后),就调用策略的_next方法,这样就进入到我们定制策略next中进行处理了。
  • _runonce函数实现:

        def _runonce(self, runstrats):
            '''
            Actual implementation of run in vector mode.
    
            Strategies are still invoked on a pseudo-event mode in which ``next``
            is called for each data arrival
            '''
            for strat in runstrats:
                strat._once()
                strat.reset()  # strat called next by next - reset lines
    
            # The default once for strategies does nothing and therefore
            # has not moved forward all datas/indicators/observers that
            # were homed before calling once, Hence no "need" to do it
            # here again, because pointers are at 0
            datas = sorted(self.datas,
                           key=lambda x: (x._timeframe, x._compression))
    
            while True:
                # Check next incoming date in the datas
                dts = [d.advance_peek() for d in datas]
                dt0 = min(dts)
                if dt0 == float('inf'):
                    break  # no data delivers anything
    
                # Timemaster if needed be
                # dmaster = datas[dts.index(dt0)]  # and timemaster
                slen = len(runstrats[0])
                for i, dti in enumerate(dts):
                    if dti <= dt0:
                        datas[i].advance()
                        # self._plotfillers2[i].append(slen)  # mark as fill
                    else:
                        # self._plotfillers[i].append(slen)
                        pass
    
                self._check_timers(runstrats, dt0, cheat=True)
    
                if self.p.cheat_on_open:
                    for strat in runstrats:
                        strat._oncepost_open()
                        if self._event_stop:  # stop if requested
                            return
    
                self._brokernotify()
                if self._event_stop:  # stop if requested
                    return
    
                self._check_timers(runstrats, dt0, cheat=False)
    
                for strat in runstrats:
                    strat._oncepost(dt0)
                    if self._event_stop:  # stop if requested
                        return
    
                    self._next_writers(runstrats)
    
    • 针对每个strategy,运行_once方法并执行reset,将数据索引指向0。
    • 将数据按照周期(timeframe)大小排序。
    • 调用advance_peek看下一个时间(索引为1),最近时间记为dt0。
    • 对最近时间的data调用advance处理。
    • 如果设置cheat_on_open标识,在策略的next前调用next_open方法。
    • 检测是否收到broker的stop消息。
    • 调用策略_oncepost方法,会调用到strategy的next方法。

10、可视化

  • 调用plot方法进行可视化输出,plot方法实现:

    def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None,
             width=16, height=9, dpi=300, tight=True, use=None,
             **kwargs):
        '''
            Plots the strategies inside cerebro
    
            If ``plotter`` is None a default ``Plot`` instance is created and
            ``kwargs`` are passed to it during instantiation.
    
            ``numfigs`` split the plot in the indicated number of charts reducing
            chart density if wished
    
            ``iplot``: if ``True`` and running in a ``notebook`` the charts will be
            displayed inline
    
            ``use``: set it to the name of the desired matplotlib backend. It will
            take precedence over ``iplot``
    
            ``start``: An index to the datetime line array of the strategy or a
            ``datetime.date``, ``datetime.datetime`` instance indicating the start
            of the plot
    
            ``end``: An index to the datetime line array of the strategy or a
            ``datetime.date``, ``datetime.datetime`` instance indicating the end
            of the plot
    
            ``width``: in inches of the saved figure
    
            ``height``: in inches of the saved figure
    
            ``dpi``: quality in dots per inches of the saved figure
    
            ``tight``: only save actual content and not the frame of the figure
            '''
        if self._exactbars > 0:
            return
    
        if not plotter:
            from . import plot
            if self.p.oldsync:
                plotter = plot.Plot_OldSync(**kwargs)
                else:
                    plotter = plot.Plot(**kwargs)
    
                    # pfillers = {self.datas[i]: self._plotfillers[i]
                    # for i, x in enumerate(self._plotfillers)}
    
                    # pfillers2 = {self.datas[i]: self._plotfillers2[i]
                    # for i, x in enumerate(self._plotfillers2)}
    
                    figs = []
                    for stratlist in self.runstrats:
                        for si, strat in enumerate(stratlist):
                            rfig = plotter.plot(strat, figid=si * 100,
                                                numfigs=numfigs, iplot=iplot,
                                                start=start, end=end, use=use)
                            # pfillers=pfillers2)
    
                            figs.append(rfig)
    
                            plotter.show()
    
                            return figs
    
  • 引入plotter模块,针对每一个运行的strategy进行绘图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值