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