当前位置:
首页 > Python基础教程 >
-
vn.py源码解读(七、回测代码解析)
原本想开始讲策略类的编写,后来觉得,结合回测代码其实能够更好的理解,所以先解读一下vnpy回测的代码吧,后续自己也想把vnpy回测的部分优化一下,毕竟我觉得可视化和回测结果方提高还有很多空间。
我们解读的代码从runbacktesting.py开始。首先,和实盘中一样导入了一个策略。
from vnpy.trader.app.ctaStrategy.strategy.strategyDoubleMa import DoubleMaStrategy
紧接着,就获得了一个回测引擎的对象:
engine = BacktestingEngine()
我们先不管这个引擎,继续往下看,发现后面与engine有关的操作有这些:
# 设置引擎的回测模式为K线
engine.setBacktestingMode(engine.BAR_MODE)
# 设置回测用的数据起始日期
engine.setStartDate('20120101')
# 设置产品相关参数
engine.setSlippage(0.2) # 股指1跳
engine.setRate(0.3/10000) # 万0.3
engine.setSize(300) # 股指合约大小
engine.setPriceTick(0.2) # 股指最小价格变动
# 设置使用的历史数据库
engine.setDatabase(MINUTE_DB_NAME, 'IF0000')
# 在引擎中创建策略对象
d = {}
engine.initStrategy(DoubleMaStrategy, d)
# 开始跑回测
engine.runBacktesting()
# 显示回测结果
engine.showBacktestingResult()
那么我们就一个一个来看吧。
1.回测模式
我们进入BacktestingEngine类的定义,然后把和mode有关的全部看一下:
def setBacktestingMode(self, mode):
"""设置回测模式"""
self.mode = mode
首先,在外面调用的就是这个函数,然后,我们可以注意到,其实这个函数就是修改了一下mode变量。那么我们继续追杀这个变量出现过的地方。
在初始化函数当中,对这个变量做了初始化设置。
self.mode = self.BAR_MODE # 回测模式,默认为K线
然后是在loadHistoryData函数汇总,根据mode变量来判断。
# 首先根据回测模式,确认要使用的数据类
if self.mode == self.BAR_MODE:
dataClass = VtBarData
func = self.newBar
else:
dataClass = VtTickData
func = self.newTick
也就是,根据不同的回测模式,tick回测还是bar回测,我们在从数据库读取数据的时候,需要不同的数据的类。
在runBacktesting函数中,也有这段代码。个人觉得,这里略微啰嗦了,为什么不用工厂模式来生产呢?
然后是crossLimitOrder函数,也是就限价单撮合函数中,根据回测类型判断成交价格。
# 先确定会撮合成交的价格,这里和限价单规则相反
if self.mode == self.BAR_MODE:
buyCrossPrice = self.bar.high # 若买入方向停止单价格低于该价格,则会成交
sellCrossPrice = self.bar.low # 若卖出方向限价单价格高于该价格,则会成交
bestCrossPrice = self.bar.open # 最优成交价,买入停止单不能低于,卖出停止单不能高于
else:
buyCrossPrice = self.tick.lastPrice
sellCrossPrice = self.tick.lastPrice
bestCrossPrice = self.tick.lastPrice
在crossStopOrder中也是。此外,在计算回测结果的时候,calculateBacktestingResult函数中,到最后交易日,会用最后的价格平仓了解。这里最后平仓价格的确定也回测模式有关:
# 到最后交易日尚未平仓的交易,则以最后价格平仓
if self.mode == self.BAR_MODE:
endPrice = self.bar.close
else:
endPrice = self.tick.lastPrice
还有的是参数优化函数里面,这一部分暂时不考虑。
2.回测设置
# 设置回测用的数据起始日期
engine.setStartDate('20120101')
# 设置产品相关参数
engine.setSlippage(0.2) # 股指1跳
engine.setRate(0.3/10000) # 万0.3
engine.setSize(300) # 股指合约大小
engine.setPriceTick(0.2) # 股指最小价格变动
我们看一下这个函数的构成:
def setStartDate(self, startDate='20100416', initDays=10):
"""设置回测的启动日期"""
self.startDate = startDate
self.initDays = initDays
self.dataStartDate = datetime.strptime(startDate, '%Y%m%d')
initTimeDelta = timedelta(initDays)
self.strategyStartDate = self.dataStartDate + initTimeDelta
我们可以发现,设置的startDate是数据启动的日子,因为被变成了dataStartDate,而前面有一个initDays,这个就很奇怪了,这个是用于计算指标的吗?比如我们的策略是20日均线,那么就应该需要20个交易日的数据启动时间。那么这个20的参数应该是策略类所有,而且是20个交易日。但是这里的initDays明显就是日历日。那么,我们就分别追杀一下dataStartDate和strategyStartDate的使用的地方吧。
在init初始化函数中,有这样的注释:
self.dataStartDate = None # 回测数据开始日期,datetime对象
self.dataEndDate = None # 回测数据结束日期,datetime对象
self.strategyStartDate = None # 策略启动日期(即前面的数据用于初始化),datetime对象
明确了dataStartDate是回测数据开始的时间,而strategyStartDate和dataStartDate之前的差距是用于数据初始化的时间,也就是策略启动时间是在数据启动之后的。那么疑问就是,中间这段数据初始化时间是应该在这个地方被确定的吗?我们继续追杀。
在loadHistoryData方法里面,我们又碰到了这个strategyStartDate变量,我们来看一下:
# 载入初始化需要用的数据
if self.hdsClient:
initCursor = self.hdsClient.loadHistoryData(self.dbName,
self.symbol,
self.dataStartDate,
self.strategyStartDate)
else:
flt = {'datetime':{'$gte':self.dataStartDate,
'$lt':self.strategyStartDate}}
initCursor = collection.find(flt).sort('datetime')
self.hdsClient = None # 历史数据服务器客户端
其中,初始化函数中的定义其实已经告诉我们了,hdsClient是一个数据来源的标志符,显然我们的数据来自于本地的Mongodb,而不是什么亚马逊这种的云服务器之类的,所以我们会使用下面的if分支语句。
我们看到,传递进去的是dataStrategyDate和strategyStartDate。
3.数据库部分
后面就涉及到一点mongodb数据库python读取的知识了,简单介绍一下。
首先,我们通过robo可视化工具看一下现在mongodb数据库里面的数据情况。
我们在这个loadHistoryData方法的一开始,先获得了数据库的连接:
self.dbClient = pymongo.MongoClient(globalSetting['mongoHost'], globalSetting['mongoPort'])
collection = self.dbClient[self.dbName][self.symbol]
前面一行是获取一个连接,globalSetting就是VT_setting配置文件里面的内容:
"mongoHost": "localhost",
"mongoPort": 27017,
"mongoLogging": true,
然后是通过连接获得一个集合。获得集合首先需要数据库的名称,然后需要具体collection的名称。数据库的名称在一开始的
engine.setDatabase(MINUTE_DB_NAME, 'IF0000')
其中,MINUTE_DB_NAME = 'VnTrader_1Min_Db'
#----------------------------------------------------------------------
def setDatabase(self, dbName, symbol):
"""设置历史数据所用的数据库"""
self.dbName = dbName
self.symbol = symbol
我们发现,就是这个detDatabase方法设置了数据库获取的参数。
那么,根据我们前面看到的mongodb数据库的结构,我们就知道了,这里其实就是获得了IF0000的数据集合。我们来看一下具体从mongodb中获取数据的代码:
# 载入初始化需要用的数据
if self.hdsClient:
initCursor = self.hdsClient.loadHistoryData(self.dbName,
self.symbol,
self.dataStartDate,
self.strategyStartDate)
else:
flt = {'datetime':{'$gte':self.dataStartDate,
'$lt':self.strategyStartDate}}
initCursor = collection.find(flt).sort('datetime')
# 将数据从查询指针中读取出,并生成列表
self.initData = [] # 清空initData列表
for d in initCursor:
data = dataClass()
data.__dict__ = d
self.initData.append(data)
上面的代码中,我们发现flt语句,在mongodb中,$gte是great than equal的意思,也就是大于等于,lt是less than的意思,也就是小于。所以,上面的initCursor指的是获得大于等于dataStartDate,同时小于strategyStartDate的行情数据。后面的语句是从这个数据库游标中获取这部分数据,并把它保存在self.initData中。
# 载入回测数据
if self.hdsClient:
self.dbCursor = self.hdsClient.loadHistoryData(self.dbName,
self.symbol,
self.strategyStartDate,
self.dataEndDate)
else:
if not self.dataEndDate:
flt = {'datetime':{'$gte':self.strategyStartDate}} # 数据过滤条件
else:
flt = {'datetime':{'$gte':self.strategyStartDate,
'$lte':self.dataEndDate}}
self.dbCursor = collection.find(flt).sort('datetime')
然后是获取比strategyStartDate大的行情数据,如果有dataEndDate,那么就这个为止,没有的话就取出strategyStartDate之后的所有数据。
其中的dataEndDate可以通过setEndDate方法来设置。
那么,获取了上面的两端数据的作用是什么呢?我们追杀完几个Date之后,开始追杀一下initData和后来的dbCursor吧。
在初始化函数汇中,
self.initData = [] # 初始化用的数据
数据类型是一个列表。
def loadBar(self, dbName, collectionName, startDate):
"""直接返回初始化数据列表中的Bar"""
return self.initData
#----------------------------------------------------------------------
def loadTick(self, dbName, collectionName, startDate):
"""直接返回初始化数据列表中的Tick"""
return self.initData
然后是在两个方法中,会返回initData。因为会根据mode来进行数据的读取,所以,这里的initData其实是分bar和tick两种模式的,所欲在load的时候也是分了两个函数。
其他地方就没有用到initData了,也就是说,在回测引擎中获取的数据是给别的地方调用的。
4.runBackTesting
而dbCursor在runBacktesting中被使用:
def runBacktesting(self):
"""运行回测"""
# 载入历史数据
self.loadHistoryData()
# 首先根据回测模式,确认要使用的数据类
if self.mode == self.BAR_MODE:
dataClass = VtBarData
func = self.newBar
else:
dataClass = VtTickData
func = self.newTick
self.output(u'开始回测')
self.strategy.onInit()
self.strategy.inited = True
self.output(u'策略初始化完成')
self.strategy.trading = True
self.strategy.onStart()
self.output(u'策略启动完成')
self.output(u'开始回放数据')
for d in self.dbCursor:
data = dataClass()
data.__dict__ = d
func(data)
self.output(u'数据回放结束')
我们看到,其实是从数据游标dbCursor中依次获取数据,然后用func函数来处理,而func函数是什么呢?就是前面根据回测模式选择出来的函数,self.newTick或者self.newBar,我们来看一下newBar,
def newBar(self, bar):
"""新的K线"""
self.bar = bar
self.dt = bar.datetime
self.crossLimitOrder() # 先撮合限价单
self.crossStopOrder() # 再撮合停止单
self.strategy.onBar(bar) # 推送K线到策略中
self.updateDailyClose(bar.datetime, bar.close)
我们看到,其实最核心的函数分别是crossLimitOrder和crossStopOrder,其实作用就是根据新的bar来判断有没有交易成交或者停止。也就是一个成交的逻辑。我们后面展开来讲。而这里还有一部分是strategy的onBar方法来接受bar,我们知道了,其实在回测过程中,每一次新的bar产生之后,都会调用strategy的onBar方法。也就是说,onBar方法的重写是编写策略的核心。
策略的载入在初始化策略方法中完成:
def initStrategy(self, strategyClass, setting=None):
"""
初始化策略
setting是策略的参数设置,如果使用类中写好的默认设置则可以不传该参数
"""
self.strategy = strategyClass(self, setting)
self.strategy.name = self.strategy.className
这段比较简单,就不用多说了。
最后就是运行runBacktesting方法开始获取每一个bar的行情,然后就是利用showBacktestingResult给出结果。
那么我们就来看看最后这个showBacktestingResult的功能吧。如果后续自己想做点可视化和别的策略评价功能的话,最重要的就是改写这个方法。
这个方法一开始会调用calculateBacktestingResult方法,我们继续追杀这个方法。而这个方法其实是策略回测结果展示最核心的一个方法,所有的pnl计算、胜率计算都在这个函数里面。后面一篇文章我们就仔细来拆解一下这个函数吧。
————————————————
版权声明:本文为CSDN博主「钱塘小甲子」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qtlyx/article/details/85013132
栏目列表
最新更新
nodejs爬虫
Python正则表达式完全指南
爬取豆瓣Top250图书数据
shp 地图文件批量添加字段
爬虫小试牛刀(爬取学校通知公告)
【python基础】函数-初识函数
【python基础】函数-返回值
HTTP请求:requests模块基础使用必知必会
Python初学者友好丨详解参数传递类型
如何有效管理爬虫流量?
SQL SERVER中递归
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
这是目前我见过最好的跨域解决方案!
减少回流与重绘
减少回流与重绘
如何使用KrpanoToolJS在浏览器切图
performance.now() 与 Date.now() 对比
一款纯 JS 实现的轻量化图片编辑器
关于开发 VS Code 插件遇到的 workbench.scm.
前端设计模式——观察者模式
前端设计模式——中介者模式
创建型-原型模式