vn.py source interpretation (eight backtesting result of the calculation code analysis)

        We look calculateBacktestingResult core of this method, which is the core of a large circle.

        for trade in self.tradeDict.values():
            # 复制成交对象,因为下面的开平仓交易配对涉及到对成交数量的修改
            # 若不进行复制直接操作,则计算完后所有成交的数量会变成0
            trade = copy.copy(trade)

        Throughout the cycle, the core is the tradeDict, this is an ordered dictionary, which is an orderly dictionary. Inside the value is VtTradeDate object. We look to achieve this class:

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           # 成交时间

        So, obviously, this is back to test the transaction process is put in this tradeDict. We went to look over crossLimitOrder method.

                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

        Probably involved in the above code, create a VtTradeData objects, followed by the assignment, and finally put tradeDict in. However, I have a question, is not defined VtTradeData dt this property, but it can indeed assignments can also get in the back, is it wrong version? Leave a Q here may be the author of the python yourself what blind spots.

We simply look at some of the properties in which it:

We found that trading hours and dt information is duplicated in fact, I do not know the reason for this design.

orderID is very simple, that is, 2, 3, has been back.

Then we continue to look at the code in detail the logic behind the entire traverse tradeDict.

 if trade.direction == DIRECTION_LONG:

First, we determine what direction the transaction, is a multi-empty, if the transaction is long, then the judge continued:

           if not shortTrade:
                    longTrade.append(trade)
                # 当前多头交易为平空
                else:
                    while True:
                        entryTrade = shortTrade[0]
                        exitTrade = trade

Then look at, shortTrade this list is not empty. If it is empty, indicating that currently there is no short positions, directly into the long list, which is longTrade it. If not, go to netting.

Code netting as follows:

                    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

First, come up with the first pen open short positions, and then calculate this sum of short exposure and the current size of this more than a single volume, in accordance with the small numbers flat.

                        # 清算开平仓交易
                        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)

Specific results netting two transactions, calculated by TradingResult this 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)                      # 净盈亏

We can see that the object of this class is initialized after the fact, contain commissions, slippage and pnl. All in all, after the match, there will be a resultList.

Then we look at the direct result of content storage and trade.Dict among the list. That is the result of brokered transactions and transaction information.

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)

We can see the pen to enter the date for the first result of 12 years October 6; closing date is May 6, 14 to 12 years. Then in the dictionary storage transaction which, the first transaction record refers to the first opening transaction a result, the second deal, the author set is the second deal directly open up. So second deal is the first open trading results. So, in fact, theoretically, if the positive and negative words of similar items, and then distributed more symmetrical, then about half the length of resultlist dictionary storage transactions.

Too many details will not go back, dimension vnpy each transaction is stored in the data presented above in tradeDict is calculated and given a function in accordance with another data transaction date dimension.

nvpy showDailyResult of a given function. We look at the first few lines of code.

 def showDailyResult(self, df=None, result=None):
        """显示按日统计的交易结果"""
        if df is None:
            df = self.calculateDailyResult()
            df, result = self.calculateDailyStatistics(df)
        self.daily_result_store = df

We found that there is a calculateDailyResult function df function returns. This function computes the results of daily level, and some indicators kept in a DataFrame inside.

The function code also fill long:

    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

        In fact, very simple, we look at the details of the code can understand, we take a look at df inside.

        We can see that there are so many df column, not shown here, is the index. It was the index date. We explain briefly the contents. closePosition is the day after the close position, closePrice is the closing price of the day, commission fee is to this day, netPnl is the day of net profits and losses, including possible additional costs such as fees and slippage, openPosition is opening time positions. In fact, most of them are literally mean it.

        We can actually do a lot of follow-up tests based on these data, and this is the most important work behind, such as Monte Carlo simulation to return and so on.

 

 

 

Published 205 original articles · won praise 236 · views 980 000 +

Guess you like

Origin blog.csdn.net/qtlyx/article/details/85028796