VeighNa量化社区
你的开源社区量化交易平台
Member
加入于:
帖子: 158
声望: 71

现在VeighNa交易是按日进行统计回测的,但是在之前的v1版本,是同时支持按日和按照交易数统计回测结果。

通过借用之前版本的代码,可以获得按交易数统计回测结果,下图示例,其中红框的都是按交易统计获得数据,比如交易胜率就是盈利交易次数比总交易次数:

description

代码改动,在cta_strategy.backtesting新增TradingResult类中,这里如果输入费率如果大于0.1,则费率按手数计算,小于按照保证金百分比计算。

class TradingResult:
    """每笔交易的结果"""

    #----------------------------------------------------------------------
    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)   # 成交金额
        if rate < 0.1:
            self.commission = self.turnover*rate                                # 手续费成本
        else:
            self.commission = abs(volume)*rate*2

        self.slippage = slippage*2*size*abs(volume)                         # 滑点成本
        self.pnl = ((self.exitPrice - self.entryPrice) * volume * size
                    - self.commission - self.slippage)                      # 净盈亏

代码改动,在cta_strategy.backtesting的BacktestingEngine类中,其实这段代码基本就是1.92 copy过来,稍微改改。

    #----------------------------------------------------------------------
    def calculateBacktestingResult(self):
        """
        计算回测结果
        """
        self.output(u'计算回测结果')

        # 检查成交记录
        if not self.trades:
            self.output(u'成交记录为空,无法计算回测结果')
            noResult = {}
            noResult['capital'] = 0
            noResult['maxCapital'] = 0
            noResult['drawdown'] = 0
            noResult['totalResult'] = 0
            noResult['totalTurnover'] = 0
            noResult['totalCommission'] = 0
            noResult['totalSlippage'] = 0
            noResult['timeList'] = 0
            noResult['pnlList'] = 0
            noResult['capitalList'] = 0
            noResult['drawdownList'] = 0
            noResult['winningRate'] = 0
            noResult['averageWinning'] = 0
            noResult['averageLosing'] = 0
            noResult['profitLossRatio'] = 0
            noResult['posList'] = 0
            noResult['tradeTimeList'] = 0
            noResult['resultList'] = 0
            noResult['maxDrawdown'] = 0
            return noResult


        # 首先基于回测后的成交记录,计算每笔交易的盈亏
        resultList = []  # 交易结果列表

        longTrade = []  # 未平仓的多头交易
        shortTrade = []  # 未平仓的空头交易

        tradeTimeList = []  # 每笔成交时间戳
        posList = [0]  # 每笔成交后的持仓情况

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

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

                        # 清算开平仓交易
                        closedVolume = min(exitTrade.volume, entryTrade.volume)
                        result = TradingResult(entryTrade.price, entryTrade.datetime,
                                               exitTrade.price, exitTrade.datetime,
                                               -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

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

                        # 清算开平仓交易
                        closedVolume = min(exitTrade.volume, entryTrade.volume)
                        result = TradingResult(entryTrade.price, entryTrade.datetime,
                                               exitTrade.price, exitTrade.datetime,
                                               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:
                            longTrade.pop(0)

                        # 如果平仓交易已经全部清算,则退出循环
                        if not exitTrade.volume:
                            break

                        # 如果平仓交易未全部清算,
                        if exitTrade.volume:
                            # 且开仓交易已经全部清算完,则平仓交易剩余的部分
                            # 等于新的反向开仓交易,添加到队列中
                            if not longTrade:
                                shortTrade.append(exitTrade)
                                break
                            # 如果开仓交易还有剩余,则进入下一轮循环
                            else:
                                pass

                                # 到最后交易日尚未平仓的交易,则以最后价格平仓
        if self.mode == BacktestingMode.BAR:
            endPrice = self.bar.close_price
        else:
            endPrice = self.tick.last_price

        for trade in longTrade:
            result = TradingResult(trade.price, trade.datetime, endPrice, self.datetime,
                                   trade.volume, self.rate, self.slippage, self.size)
            resultList.append(result)

        for trade in shortTrade:
            result = TradingResult(trade.price, trade.datetime, endPrice, self.datetime,
                                   -trade.volume, self.rate, self.slippage, self.size)
            resultList.append(result)

            # 检查是否有交易
        if not resultList:
            self.output(u'无交易结果')
            return {}

        # 然后基于每笔交易的结果,我们可以计算具体的盈亏曲线和最大回撤等
        capital = 0             # 资金
        maxCapital = 0          # 资金最高净值
        drawdown = 0            # 回撤

        totalResult = 0         # 总成交数量
        totalTurnover = 0       # 总成交金额(合约面值)
        totalCommission = 0     # 总手续费
        totalSlippage = 0       # 总滑点

        timeList = []           # 时间序列
        pnlList = []            # 每笔盈亏序列
        capitalList = []        # 盈亏汇总的时间序列
        drawdownList = []       # 回撤的时间序列

        winningResult = 0       # 盈利次数
        losingResult = 0        # 亏损次数
        totalWinning = 0        # 总盈利金额
        totalLosing = 0         # 总亏损金额
        maxDrawdown = 0         # 最大回撤

        max_win_count = 0
        win_count = 0
        max_lose_count = 0
        lose_count = 0

        for result in resultList:
            capital += result.pnl
            maxCapital = max(capital, maxCapital)
            drawdown = capital - maxCapital
            maxDrawdown = min(drawdown, maxDrawdown)

            pnlList.append(result.pnl)
            timeList.append(result.exitDt)  # 交易的时间戳使用平仓时间
            capitalList.append(capital)
            drawdownList.append(drawdown)

            totalResult += 1
            totalTurnover += result.turnover
            totalCommission += result.commission
            totalSlippage += result.slippage

            if result.pnl >= 0:
                winningResult += 1
                totalWinning += result.pnl

                max_lose_count = max(max_lose_count, lose_count)
                lose_count = 0
                win_count +=1
            else:
                losingResult += 1
                totalLosing += result.pnl

                max_win_count = max(max_win_count, win_count)
                win_count = 0
                lose_count +=1

        # 计算盈亏相关数据
        winningRate = winningResult / totalResult * 100  # 胜率

        averageWinning = 0  # 这里把数据都初始化为0
        averageLosing = 0
        profitLossRatio = 0

        if winningResult:
            averageWinning = totalWinning / winningResult  # 平均每笔盈利
        if losingResult:
            averageLosing = totalLosing / losingResult  # 平均每笔亏损
        if averageLosing:
            profitLossRatio = -averageWinning / averageLosing  # 盈亏比


        #shaperadio
        annualDays =240
        dailyReturn = np.mean(pnlList) * 100
        returnStd = np.std(pnlList) * 100

        if returnStd:
            sharpeRatio = dailyReturn / returnStd
        else:
            sharpeRatio = 0
        # 返回回测结果
        d = {}
        d['capital'] = capital
        d['maxCapital'] = maxCapital
        d['drawdown'] = drawdown
        d['totalResult'] = totalResult
        d['totalTurnover'] = totalTurnover
        d['totalCommission'] = totalCommission
        d['totalSlippage'] = totalSlippage
        d['timeList'] = timeList
        d['pnlList'] = pnlList
        d['capitalList'] = capitalList
        d['drawdownList'] = drawdownList
        d["averageProfit"] = capital/totalResult
        d['winningRate'] = winningRate
        d["totalWinning"] = totalWinning
        d["winningResult"] = winningResult
        d['averageWinning'] = averageWinning
        d["totalLosing"] = totalLosing
        d["losingResult"] = losingResult
        d['averageLosing'] = averageLosing
        d['profitLossRatio'] = profitLossRatio
        d['posList'] = posList
        d['tradeTimeList'] = tradeTimeList
        d['resultList'] = resultList
        d['maxDrawdown']  = maxDrawdown
        d['sharpeRatio'] = sharpeRatio
        d['returnStd'] = returnStd
        d['max_win_count'] = max_win_count
        d['max_lose_count'] = max_lose_count


        return d

使用也很简单,BacktestingEngine在run_backtesting 后, 运行calculateBacktestingResult,可以返回一个Dict,带着相关按次数分析的回测结果。

Member
avatar
加入于:
帖子: 251
声望: 3

老师您好,按照您的提示操作了,回测还是没有显示胜率这些,小白一个,widget里面不太会,使用的是vnpy3.5版本,可以分享一下代码吗?万分感激

Member
加入于:
帖子: 158
声望: 71

ranjianlin wrote:

老师您好,按照您的提示操作了,回测还是没有显示胜率这些,小白一个,widget里面不太会,使用的是vnpy3.5版本,可以分享一下代码吗?万分感激
我的界面实在2.6版本,而且改动比较多。直接引用的话涉及代码太多了。

思路讲下:
1.确定统计结果的Dict可以输出,值正确。

  1. 在原来的backtesting的calculate_statistics中,调用tradeResultDict = self.calculateBacktestingResult(),然后修改statistics这个DICT成你想要的输出。
  2. 更新回测UI中的StatisticsMonitor,如果想和我一样,就按照下面改,pyside和pyQt5可能稍微有区别。

    class StatisticsMonitor(QtWidgets.QTableWidget):
     """"""
     KEY_NAME_MAP = {
         "回测年数": "回测年数",
         "净利润": "净利润",
         "年均净利润": "年均净利润",
         "年均交易次数": "年均交易次数",
         "总盈利": "总盈利",
         "总亏损": "总亏损",
         "总交易次数": "总交易次数",
         "盈利率": "盈利率",
         "交易胜率": "交易胜率",
         "盈利交易次数": "盈利交易次数",
         "亏损交易次数": "亏损交易次数",
         "平均每次交易": "平均每次交易",
         "平均每次盈利": "平均每次盈利",
         "平均每次亏损": "平均每次亏损",
         "平均每次盈利/-平均每次亏损": "平均每次盈利/-平均每次亏损",
         "最多连续赢几次": "最多连续赢几次",
         "最多连续输几次": "最多连续输几次",
         "最大日内回撤": "最大日内回撤",
         "最大日内回撤百分比": "最大日内回撤百分比",
         "总盈利/总亏损": "总盈利/总亏损",
         "平仓回撤": "平仓回撤",
         "夏普比例": "夏普比例"
     }
    
     def __init__(self):
         """"""
         super().__init__()
    
         self.cells = {}
    
         self.init_ui()
    
     def init_ui(self):
         """"""
         self.setRowCount(len(self.KEY_NAME_MAP))
         self.setVerticalHeaderLabels(list(self.KEY_NAME_MAP.values()))
    
         self.setColumnCount(1)
         self.horizontalHeader().setVisible(False)
         self.horizontalHeader().setSectionResizeMode(
             QtWidgets.QHeaderView.Stretch
         )
         self.setEditTriggers(self.NoEditTriggers)
    
         for row, key in enumerate(self.KEY_NAME_MAP.keys()):
             cell = QtWidgets.QTableWidgetItem()
             self.setItem(row, 0, cell)
             self.cells[key] = cell
    
     def clear_data(self):
         """"""
         for cell in self.cells.values():
             cell.setText("")
    
     def set_data(self, data):
         """"""
         data = data[0]
    
         data["回测年数"] = f"{data['回测年数']:,.2f}"
         data["净利润"] = f"{data['净利润']:,.2f}"
         data["年均净利润"] = f"{data['年均净利润']}"
         data["年均交易次数"] = f"{data['年均交易次数']}"
         data["总盈利"] = f"{data['总盈利']:,.2f}"
         data["总亏损"] = f"{data['总亏损']:,.2f}"
         data["盈利率"] = data['盈利率']
         data["交易胜率"] = data['交易胜率']
         data["盈利交易次数"] = f"{data['盈利交易次数']}"
         data["亏损交易次数"] = f"{data['亏损交易次数']}"
         data["平均每次交易"] = f"{data['平均每次交易']:,.2f}"
         data["平均每次盈利"] = f"{data['平均每次盈利']:,.2f}"
         data["平均每次亏损"] = f"{data['平均每次亏损']:,.2f}"
         data["平均每次盈利/-平均每次亏损"] = f"{data['平均每次盈利/-平均每次亏损']:,.2f}"
         data["最多连续赢几次"] = f"{data['最多连续赢几次']:}"
         data["最多连续输几次"] = f"{data['最多连续输几次']}"
         data["最大日内回撤"] = f"{data['最大日内回撤']:,.2f}"
         data["最大日内回撤百分比"] = f"{data['最大日内回撤百分比']:,.2f}"
         data["总盈利/总亏损"] = f"{data['总盈利/总亏损']:,.2f}"
         data["平仓回撤"] = f"{data['平仓回撤']:,.2f}"
         data["夏普比例"] = f"{data['夏普比例']:,.2f}"
    
         for key, cell in self.cells.items():
             value = data.get(key, "")
             cell.setText(str(value))
Member
avatar
加入于:
帖子: 251
声望: 3

description

老师您好,我现使用的也是vnpy2.6版本,按照您的提示操作了,如图所示,CTA回测页面显示完全跟您的一致,但是就是没有结果输出,您说的调用tradeResultDict = self.calculateBacktestingResult(),然后修改statistics这个DICT成你想要的输出,可以把您的backtesting的calculate_statistics代码分享一下吗?我的CTA回测没显示,应该就是这段代码没弄好,小白一个,望回复,万分感激

© 2015-2022 上海韦纳软件科技有限公司
备案服务号:沪ICP备18006526号

沪公网安备 31011502017034号

【用户协议】
【隐私政策】
【免责条款】