当前位置:
首页 > Python基础教程 >
-
vn.py源码解读(八、回测结果计算代码解析)
我们核心关注一下calculateBacktestingResult这个方法,这个方法中最核心的是一个大循环。
for trade in self.tradeDict.values():
# 复制成交对象,因为下面的开平仓交易配对涉及到对成交数量的修改
# 若不进行复制直接操作,则计算完后所有成交的数量会变成0
trade = copy.copy(trade)
整个循环中,最核心的就是那个tradeDict,这个是一个ordered字典,也就是有序的字典。里面的value是VtTradeDate对象。我们来看一下这个类的实现:
class VtTradeData(VtBaseData):
"""成交数据类"""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
super(VtTradeData, self).__init__()
# 代码编号相关
self.symbol = EMPTY_STRING # 合约代码
self.exchange = EMPTY_STRING # 交易所代码
self.vtSymbol = EMPTY_STRING # 合约在vt系统中的唯一代码,通常是 合约代码.交易所代码
self.tradeID = EMPTY_STRING # 成交编号
self.vtTradeID = EMPTY_STRING # 成交在vt系统中的唯一编号,通常是 Gateway名.成交编号
self.orderID = EMPTY_STRING # 订单编号
self.vtOrderID = EMPTY_STRING # 订单在vt系统中的唯一编号,通常是 Gateway名.订单编号
# 成交相关
self.direction = EMPTY_UNICODE # 成交方向
self.offset = EMPTY_UNICODE # 成交开平仓
self.price = EMPTY_FLOAT # 成交价格
self.volume = EMPTY_INT # 成交数量
self.tradeTime = EMPTY_STRING # 成交时间
那么,很显然,这是在回测成交的过程中被放到这个tradeDict中的。我们去crossLimitOrder方法中看一下。
trade = VtTradeData()
trade.vtSymbol = order.vtSymbol
trade.tradeID = tradeID
trade.vtTradeID = tradeID
trade.orderID = order.orderID
trade.vtOrderID = order.orderID
trade.direction = order.direction
trade.offset = order.offset
# 以买入为例:
# 1. 假设当根K线的OHLC分别为:100, 125, 90, 110
# 2. 假设在上一根K线结束(也是当前K线开始)的时刻,策略发出的委托为限价105
# 3. 则在实际中的成交价会是100而不是105,因为委托发出时市场的最优价格是100
if buyCross:
trade.price = min(order.price, buyBestCrossPrice)
self.strategy.pos += order.totalVolume
else:
trade.price = max(order.price, sellBestCrossPrice)
self.strategy.pos -= order.totalVolume
trade.volume = order.totalVolume
trade.tradeTime = self.dt.strftime('%H:%M:%S')
trade.dt = self.dt
self.strategy.onTrade(trade)
self.tradeDict[tradeID] = trade
大概是上面这段代码涉及的,先创建一个VtTradeData对象,然后依次赋值,最后放到tradeDict中。但是,笔者有一个疑问,就是VtTradeData中并没有定义dt这个属性,但是确实是可以赋值也可以在后面进行获取,难道是版本不对?留一个Q在这里,可能是笔者自己对python还有什么盲点吧。
我们简单看一下其中的一些属性吧:
我们发现交易时间和dt其实信息是重复的,不知道这样设计的原因。
orderID就很简单,就是1,2,3,一直往后。
然后我们继续看整个遍历tradeDict背后的详细代码逻辑。
if trade.direction == DIRECTION_LONG:
首先,我们判断一下交易的方向,是多还是空,如果是多头交易,那么继续判断:
if not shortTrade:
longTrade.append(trade)
# 当前多头交易为平空
else:
while True:
entryTrade = shortTrade[0]
exitTrade = trade
然后查看一下,shortTrade这个list是不是空的。如果是空的,说明目前没有空头的头寸,直接放入多头的list,也就是longTrade就可以了。如果不是就去轧差。
轧差的代码如下:
while True:
entryTrade = shortTrade[0]
exitTrade = trade
# 清算开平仓交易
closedVolume = min(exitTrade.volume, entryTrade.volume)
result = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-closedVolume, self.rate, self.slippage, self.size)
resultList.append(result)
posList.extend([-1,0])
tradeTimeList.extend([result.entryDt, result.exitDt])
# 计算未清算部分
entryTrade.volume -= closedVolume
exitTrade.volume -= closedVolume
# 如果开仓交易已经全部清算,则从列表中移除
if not entryTrade.volume:
shortTrade.pop(0)
# 如果平仓交易已经全部清算,则退出循环
if not exitTrade.volume:
break
# 如果平仓交易未全部清算,
if exitTrade.volume:
# 且开仓交易已经全部清算完,则平仓交易剩余的部分
# 等于新的反向开仓交易,添加到队列中
if not shortTrade:
longTrade.append(exitTrade)
break
# 如果开仓交易还有剩余,则进入下一轮循环
else:
pass
首先,拿出空头头寸的第一笔敞口,然后计算一下这一笔空头敞口和当前这笔多单的volume大小,按照小的数字来平。
# 清算开平仓交易
closedVolume = min(exitTrade.volume, entryTrade.volume)
result = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-closedVolume, self.rate, self.slippage, self.size)
resultList.append(result)
具体两笔交易的轧差结果,通过TradingResult这个class来计算。
class TradingResult(object):
"""每笔交易的结果"""
#----------------------------------------------------------------------
def __init__(self, entryPrice, entryDt, exitPrice,
exitDt, volume, rate, slippage, size):
"""Constructor"""
self.entryPrice = entryPrice # 开仓价格
self.exitPrice = exitPrice # 平仓价格
self.entryDt = entryDt # 开仓时间datetime
self.exitDt = exitDt # 平仓时间
self.volume = volume # 交易数量(+/-代表方向)
self.turnover = (self.entryPrice+self.exitPrice)*size*abs(volume) # 成交金额
self.commission = self.turnover*rate # 手续费成本
self.slippage = slippage*2*size*abs(volume) # 滑点成本
self.pnl = ((self.exitPrice - self.entryPrice) * volume * size
- self.commission - self.slippage) # 净盈亏
我们可以看到,这个class初始化之后的对象其实含有了佣金,slippage和pnl。总而言之,撮合之后,会有一个resultList。
然后,我们直接看一下存储result的list和trade.Dict当中的内容。也就是撮合成交结果和交易信息。
resultList[0]
entryDt: datetime.datetime(2015, 1, 12, 10, 6)
exitDt:datetime.datetime(2015, 1, 12, 14, 6)
self.tradeDict.values()[0]
dt:datetime.datetime(2015, 1, 12, 10, 6)
self.tradeDict.values()[1]
dt:datetime.datetime(2015, 1, 12, 14, 6)
我们可以看到,第一笔result的进入日期是12年10月6日;平仓的日期是12年14月6日。然后在存储交易的字典里面,第一笔交易记录就是指第一个result的开仓交易,第二笔交易,笔者设置的是第二笔交易就直接平仓了。所以第二笔交易就是第一个结果的平仓交易。所以,其实理论上,如果正反的笔数差不多的话,然后分布比较对称的话,那么resultlist的长度大概是存储交易的字典的一半。
后面过多的细节就不赘述了,vnpy给出的每笔交易维度的数据就存储在上面的tradeDict中,按照交易日维度的数据则在另外一个一个函数中计算并给出。
nvpy给出了一个showDailyResult的函数。我们看一下前面几行代码。
def showDailyResult(self, df=None, result=None):
"""显示按日统计的交易结果"""
if df is None:
df = self.calculateDailyResult()
df, result = self.calculateDailyStatistics(df)
self.daily_result_store = df
我们发现,有一个返回df函数的calculateDailyResult函数。这个函数计算了daily级别的结果,并把一些指标保存在一个DataFrame里面。
这个函数的代码也补长:
def calculateDailyResult(self):
"""计算按日统计的交易结果"""
self.output(u'计算按日统计结果')
# 检查成交记录
if not self.tradeDict:
self.output(u'成交记录为空,无法计算回测结果')
return {}
# 将成交添加到每日交易结果中
for trade in self.tradeDict.values():
date = trade.dt.date()
dailyResult = self.dailyResultDict[date]
dailyResult.addTrade(trade)
# 遍历计算每日结果
previousClose = 0
openPosition = 0
for dailyResult in self.dailyResultDict.values():
dailyResult.previousClose = previousClose
previousClose = dailyResult.closePrice
dailyResult.calculatePnl(openPosition, self.size, self.rate, self.slippage )
openPosition = dailyResult.closePosition
# 生成DataFrame
resultDict = {k:[] for k in dailyResult.__dict__.keys()}
for dailyResult in self.dailyResultDict.values():
for k, v in dailyResult.__dict__.items():
resultDict[k].append(v)
resultDf = pd.DataFrame.from_dict(resultDict)
# 计算衍生数据
resultDf = resultDf.set_index('date')
return resultDf
其实很简单,代码细节大家看一下就可以了解了,我们来查看一下df里面的东西。
我们可以看到,这个df有这么多列,这里没有展示的是index。它的index是日期。我们简单解释一下这些内容。closePosition就是这一天收盘后的持仓、closePrice就是当天的收盘价、commission就是这一天的手续费、netPnl就是这一天的净盈亏,包括了手续费和滑点等其他可能的费用、openPosition就是开盘的时候的仓位。其实大部分都是字面意思吧。
我们其实可以根据这些数据完成很多后续的测试,而这也是后面最重要的一个工作,比如对return进行蒙特卡洛仿真等等。
————————————————
版权声明:本文为CSDN博主「钱塘小甲子」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qtlyx/article/details/85028796
栏目列表
最新更新
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.
前端设计模式——观察者模式
前端设计模式——中介者模式
创建型-原型模式