在vnpy中使用命令行进行回测

vnpy station支持图形化的操作界面,然而我们在进行模板调试的时候更多地需要在命令行下进行debug,为此需要在python运行环境中编制程序,调用vnpy回测模块。

一、真相只有一个

在一个稍微大一些的面向对象程序中,总会有俄罗斯套娃似的多层封装,找到真正的功能代码并不总是那么容易。

在Qt图形界面,开启回测的命令是在/vnpy/app/ui/widget.py的start_backtesting函数中启动的,在这个函数中,调用了对应的engine的start_backtesting函数,相关文件在/vnpy/app/cta_backtester/engine.py。

我们找到BacktesterEigine类的start_backtesting方法,这个方法中,启用一个新的线程来运行同类的run_backtesting方法:

 self.thread = Thread(
            target=self.run_backtesting,
            args=(
                class_name,
                vt_symbol,
                interval,
                 ……
                 )
             )

而run_backtesting方法仍然是一个壳,它调用了另外一个类/vnpy/app/cta_strategy/BacktestingEngine,我们继续寻找。终于在/cnpy/app/cta_strategy/backtesting.py中找到了函数run_backtesting:

def run_backtesting(self):
    """"""
    if self.mode == BacktestingMode.BAR:
        func = self.new_bar
    else:
        func = self.new_tick

    self.strategy.on_init()

    # Use the first [days] of history data for initializin
    day_count = 0
    ix = 0

    for ix, data in enumerate(self.history_data):
        if self.datetime and data.datetime.day != self.dat
            day_count += 1
            if day_count >= self.days:
                break

        self.datetime = data.datetime
        self.callback(data)

    self.strategy.inited = True
    self.output("策略初始化完成")

    self.strategy.on_start()
    self.strategy.trading = True
    self.output("开始回放历史数据")

    # Use the rest of history data for running backtesting
    for data in self.history_data[ix:]:
        func(data)

    self.output("历史数据回放结束")

从这里就可看得出回测原理,实际上是吧策略重新跑了一遍。func函数根据回测的数据类别(bar或是tick)而不同。这里的new_bar也好,new_tick也好都是重新实例化一个数据信号,跟实盘没有什么不同。

小技巧:为了不把函数前面的空格复制进来,可以用vim的块复制命令,光标移动到def run_backtesting的“d”那里,按下Ctrl+V,通过键盘移动光标到要复制的末尾,按下y。疯狂按Page down,到文件末尾,按p进行粘贴。最后再按i进入插入模式,重新选择那些行复制。

二、为调用做好准备

engine与widget是一个完整地前后台关系。从/vnpy/app/cta_backtester/ui/widget/py图形界面中我们可以找到运行run_backtesting的代码:

result = self.backtester_engine.start_backtesting(
         class_name, #策略名
         vt_symbol,  #品种
         interval,   
         start,
         end,
         rate,
         slippage,
         size,
         pricetick,
         capital,
         inverse,
         new_setting  
      )

这里需要特殊注意的是new_setting这个参数,它是一个dict。指的是一个策略的具体参数,例如在双均线策略中,它是快慢两条均线的窗口大小。在图形界面中,它是在对话框中获得的:

old_setting = self.settings[class_name]
dialog = BacktestingSettingEditor(class_name, old_setting)
new_setting = dialog.get_setting()

这里的self是一个widget。看来settings是一个狠角色,它保存了每个策略的参数。我们不得不继续追问,最初的settings是从哪里来的呢。

整个widget.py文件,维护的就是一个叫做BacktesterManager的类。在这个类里,首先就是定义了成员变量setting_filename:

setting_filename = "cta_backtester_setting.json"

然而,这个json文件存储的是对于回测的设置,也就是class_name,vt_symbol,interval这些默认的配置,如下图。

真正的对于每个策略的参数设置是在构造函数中被调用的init_strategy_settings函数:

def init_strategy_settings(self):
""""""
self.class_names = self.backtester_engine.get_strategy_class_names()

for class_name in self.class_names:
    setting = self.backtester_engine.get_default_setting(class_name)
    self.settings[class_name] = setting

self.class_combo.addItems(self.class_names)

三、正向调用

经过了前面两节的大量分析,我们终于开始着手调用start_backtesting了。首先需要从json文件中获取回测参数,之后使用get_default_setting函数获取策略参数。

3.1回测参数

关于回测的参数,照猫画虎,程序中写入:

strategy_setting = back_engine.get_default_setting('AtrRsiStrategy')

相应的终端打印出来输出:

{'class_name': 'AtrRsiStrategy', 'vt_symbol': 'rb1910.SHFE', 'interval': 'd', 'rate': 2.5e-05, 'slippage': 5.0, 'size': 10.0, 'pricetick': 0.2, 'capital': 1000000.0, 'inverse': False}

3.2策略参数

策略参数需要调用函数

strategy_setting = back_engine.get_default_setting('AtrRsiStrategy')

终端打印输出:

{'atr_length': 21, 'atr_ma_length': 10, 'rsi_length': 5, 'rsi_entry': 16, 'trailing_percent': 0.8, 'fixed_size': 1}

3.3启动回测

终于可以开始调试回测了,好激动:

back_engine.start_backtesting(
    back_setting['class_name'],
    back_setting['vt_symbol'],
    back_setting['interval'],
    datetime(2019,6,1),
    datetime(2019,8,1),
    back_setting['rate'],
    back_setting['slippage'],
    back_setting['size'],
    back_setting['pricetick'],
    back_setting['capital'],
    back_setting['inverse'],
    strategy_setting
    )

我们对程序进行debug,程序能够正确地进行读取数据(在sqlite数据库已经有数据的前提下,如果没有请去前文《vnpy不使用rqdata,尝试tushare》准备数据)

我们选用的是AtrRsiStrategy这个策略,起始日期是2019年3月1日,结束日期是2019年6月1日,回测的合约是rb1910.SHFE,经过调试在5月23日出现了交易信号:

之前的文章中,经常会出现命令行无法退出的情况,今天实在忍无可忍,故去寻找原因:main_engine没有退出,可以通过下面的方法轻松解决:

main_engine.close()

附录:完整代码

from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine
from vnpy.app.cta_backtester import CtaBacktesterApp
from datetime import datetime
from vnpy.trader.utility import load_json

def main():

    event_engine = EventEngine()
    main_engine = MainEngine(event_engine)
    app = CtaBacktesterApp
    main_engine.add_app(app)
    print(main_engine.apps)
    back_engine = main_engine.get_engine("CtaBacktester")
    print(back_engine)
    back_engine.init_engine()

    vt_symbol = "rb1909.SHFE"  #实际上下载的数据要跟回测匹配,
    interval = "d"             #因为我已经提前下好了,所以这里就可以把这一部分注释
    start = datetime(2019,5,1)
    end = datetime(2019,8,1)
    back_engine.start_downloading(vt_symbol,interval,start,end)

    back_setting = load_json("cta_backtester_setting.json")
    print(back_setting)
    strategy_setting = back_engine.get_default_setting('AtrRsiStrategy')
    print(strategy_setting)
    back_engine.start_backtesting(
        back_setting['class_name'],
        back_setting['vt_symbol'],
        back_setting['interval'],
        datetime(2019,3,1),
        datetime(2019,6,1),
        back_setting['rate'],
        back_setting['slippage'],
        back_setting['size'],
        back_setting['pricetick'],
        back_setting['capital'],
        back_setting['inverse'],
        strategy_setting
        )
    main_engine.close()
    print('all over')
    return

if __name__ == "__main__":
    main()

发表评论