本文是《以AI量化为生》系列的第八篇,我们将深入优化vnpy回测框架,增加更多重要的回测指标,并改进参数优化结果窗口展示和保存功能,并增加筛选条件。

增强后的回测参数结果窗口

增强后的回测参数结果窗口

前言

在前面的文章中,我们已经成功编写了自己的第一个量化策略。回测结果中,总觉得少了点能更多评估策略好坏的指标。虽然基础指标都有,但在实际策略评估中,我们还需要更多维度的分析。

在本文中,我们将增强以下回测功能:

  • 交易级别统计:胜率、盈亏比、获利因子、连续盈亏次数等关键指标

  • 高级风险指标:索提诺比率、卡尔马比率、EWM夏普比率

  • 智能仓位管理:基于凯利公式的最优仓位计算

  • 时间维度分析:月度胜率统计、半小时区间交易表现分析

  • 综合评分系统:多指标加权的策略综合评分

  • 分组界面显示:指标按功能分组,界面更清晰

  • 增强优化窗口:16种筛选条件、详细报告导出

  • 完整控制台输出:所有统计数据的详细控制台显示

一、回测指标体系的完善

1.1 当前指标的局限性

让我们先看看vnpy原生的回测指标都有哪些:

# 原生指标主要包括:
statistics = {
    "start_date": start_date,           # 开始日期
    "end_date": end_date,               # 结束日期
    "total_days": total_days,           # 总交易日
    "total_return": total_return,       # 总收益率
    "annual_return": annual_return,     # 年化收益
    "max_drawdown": max_drawdown,       # 最大回撤
    "sharpe_ratio": sharpe_ratio,       # 夏普比率
    "return_drawdown_ratio": return_drawdown_ratio,  # 收益回撤比
    # ... 其他基础指标
}

这些指标虽然涵盖了基本面,但在实际策略评估中,我们经常需要问这些问题:

  • 这个策略的胜率是多少?

  • 平均每笔交易能赚多少?

  • 最大连续亏损是多少笔?

  • 策略的最优仓位应该是多少?

1.2 增强指标模块设计

我创建了一个专门的增强指标计算模块:

# vnpy_ctastrategy/enhanced_backtesting.py
"""
增强的回测指标计算模块
在原有回测框架基础上增加更多重要的回测指标
"""

def generate_trade_pairs(trades: List[TradeData]) -> List[Dict]:
    """
    生成交易配对,用于计算交易级别的统计指标
    这是关键函数,将开仓和平仓交易配对
    """
    long_trades: List[TradeData] = []
    short_trades: List[TradeData] = []
    trade_pairs: List[Dict] = []

    for trade in trades:
        if trade.direction == Direction.LONG:
            same_direction = long_trades
            opposite_direction = short_trades
        else:
            same_direction = short_trades
            opposite_direction = long_trades

        while trade.volume and opposite_direction:
            open_trade = opposite_direction[0]
            close_volume = min(open_trade.volume, trade.volume)
            
            # 计算持仓时间(以小时为单位)
            holding_time_hours = (trade.datetime - open_trade.datetime).total_seconds() / 3600
            
            trade_pair = {
                "open_dt": open_trade.datetime,
                "open_price": open_trade.price,
                "close_dt": trade.datetime,
                "close_price": trade.price,
                "direction": open_trade.direction,
                "volume": close_volume,
                "holding_time_hours": holding_time_hours,
            }
            trade_pairs.append(trade_pair)

            # 更新交易量
            open_trade.volume -= close_volume
            ifnot open_trade.volume:
                opposite_direction.pop(0)
            trade.volume -= close_volume

        if trade.volume:
            same_direction.append(trade)

    return trade_pairs

1.3 核心增强指标计算

基于交易配对,我们可以计算出许多重要指标。让我详细解释每个指标的含义和重要性:

def calculate_trade_statistics(trade_pairs: List[Dict], size: float) -> Dict:
    """
    计算交易级别的统计指标
    """
    ifnot trade_pairs:
        return default_empty_stats()

    # 计算每笔交易的盈亏
    trade_pnls = []
    holding_times = []
    
    for pair in trade_pairs:
        if pair["direction"] == Direction.LONG:
            pnl = (pair["close_price"] - pair["open_price"]) * pair["volume"] * size
        else:
            pnl = (pair["open_price"] - pair["close_price"]) * pair["volume"] * size
        
        trade_pnls.append(pnl)
        holding_times.append(pair["holding_time_hours"])

    # 基础统计
    winning_trades = [pnl for pnl in trade_pnls if pnl > 0]
    losing_trades = [pnl for pnl in trade_pnls if pnl < 0]

    # 胜率计算
    win_rate = len(winning_trades) / len(trade_pnls)
    
    # 平均盈利和亏损
    average_win = np.mean(winning_trades) if winning_trades else0
    average_loss = abs(np.mean(losing_trades)) if losing_trades else0
    
    # 盈亏比
    average_win_loss_ratio = average_win / average_loss if average_loss else0

    # 获利因子(总盈利/总亏损)
    total_wins = np.sum(winning_trades) if winning_trades else0
    total_losses = abs(np.sum(losing_trades)) if losing_trades else0
    profit_factor = total_wins / total_losses if total_losses else0

    # 凯利公式最优仓位
    optimal_position = calculate_kelly_ratio(win_rate, average_win, average_loss)

    # 连续盈亏统计
    max_consecutive_wins, max_consecutive_losses = calculate_consecutive_stats(trade_pnls)

    return {
        "win_rate": win_rate,
        "average_win_loss_ratio": average_win_loss_ratio,
        "optimal_position_ratio": optimal_position,
        "profit_factor": profit_factor,
        "max_consecutive_wins": max_consecutive_wins,
        "max_consecutive_losses": max_consecutive_losses,
        # ... 其他指标
    }
核心指标详解

胜率

  • 含义:盈利交易占总交易次数的比例

  • 计算:胜率 = 盈利交易次数 ÷ 总交易次数

  • 意义:反映策略预测准确性,但不是越高越好

  • 经验值

    • 50%以上:策略有一定预测能力

    • 60%以上:较好的策略

    • 70%以上:优秀策略(但要警惕过拟合)

盈亏比

  • 含义:平均每笔盈利交易与平均每笔亏损交易的比值

  • 计算:盈亏比 = 平均盈利金额 ÷ 平均亏损金额

  • 意义:衡量策略的风险收益特征

  • 经验值

    • 1.0以上:盈利能够覆盖亏损

    • 1.5以上:较好的风险收益比

    • 2.0以上:优秀的风险收益比

获利因子

  • 含义:总盈利与总亏损的比值

  • 计算:获利因子 = 总盈利金额 ÷ 总亏损金额

  • 意义:综合考虑胜率和盈亏比的整体盈利能力

  • 经验值

    • 1.0以上:策略整体盈利

    • 1.5以上:较好的策略

    • 2.0以上:优秀策略

连续盈亏次数

  • 含义:最大连续盈利/亏损的交易次数

  • 意义

    • 连续盈利:反映策略的稳定性和趋势捕捉能力

    • 连续亏损:评估心理承受能力和资金管理需求

  • 应用:设置止损策略和仓位管理的重要参考

持仓时间统计

  • 含义:每笔交易从开仓到平仓的时间长度

  • 统计维度:平均、最大、最小、中位数

  • 意义

    • 了解策略的交易频率特征

    • 评估资金使用效率

    • 判断策略适合的市场环境(震荡/趋势)

1.4 凯利公式最优仓位计算

这是一个特别实用的指标,基于数学理论告诉我们最优的资金分配比例:

def calculate_kelly_ratio(win_rate: float, average_win: float, average_loss: float) -> float:
    """计算凯利公式最优仓位"""
    ifnot average_loss or average_win <= 0or win_rate <= 0:
        return0

    p = win_rate  # 胜率
    q = 1 - p     # 败率
    b = average_win / average_loss  # 赔率

    # 凯利公式:f* = (bp - q) / b
    f_star = (b * p - q) / b

    # 限制仓位在0-1之间,并使用半凯利仓位(更保守)
    f_star = max(0, min(1, f_star))
    return f_star * 0.5# 半凯利仓位
凯利公式详解

数学原理凯利公式由贝尔实验室的约翰·凯利在1956年提出,用于解决在已知胜率和赔率的情况下,如何分配资金以实现长期收益最大化。

公式解析

  • f* = 最优仓位比例

  • p = 胜率(获胜概率)

  • q = 败率(失败概率,q = 1-p)

  • b = 赔率(平均盈利/平均亏损)

实际意义

  • 风险控制:避免过度杠杆导致的破产风险

  • 收益优化:在控制风险的前提下最大化长期收益

  • 资金管理:提供科学的仓位管理依据

使用注意事项

  1. 半凯利策略:实际使用中通常采用50%的凯利仓位,更加保守

  2. 动态调整:随着策略表现的变化,需要定期重新计算

  3. 市场环境:不同市场环境下的最优仓位可能不同

  4. 心理因素:理论最优不等于实际可执行,要考虑心理承受能力

实例说明假设某策略:

  • 胜率:60%

  • 平均盈利:100元

  • 平均亏损:50元

  • 赔率b = 100/50 = 2

凯利仓位 = (2×0.6 - 0.4) / 2 = 0.4 = 40%

半凯利仓位 = 40% × 0.5 = 20%

这意味着每次交易最好使用20%的资金。

半凯利公式是将标准凯利公式计算出的仓位减半。通过降低仓位比例,可大幅降低极端市场波动下的资金回撤风险。 ‌

二、高级风险指标

在传统的夏普比率基础上,我们引入了更精确的风险度量指标。这些指标能够更好地反映策略的真实风险特征。

2.1 索提诺比率

索提诺比率是夏普比率的改进版本,只考虑下行波动,更能反映投资者真正关心的风险:

def calculate_advanced_metrics(daily_df: DataFrame, capital: float, risk_free: float = 0.02, annual_days: int = 240) -> Dict:
    """
    计算高级风险指标
    """
    if daily_df.empty:
        return {}

    # 计算日收益率
    daily_returns = daily_df["net_pnl"] / capital
    
    # 计算下行波动率(只考虑低于无风险利率的波动)
    mar = risk_free / annual_days  # 日度最小可接受收益率
    downside_returns = []
    for daily_return in daily_returns:
        downside_diff = daily_return - mar
        if downside_diff < 0:
            downside_returns.append(downside_diff)

    down_std = np.sqrt(np.mean(np.square(downside_returns))) if downside_returns else0
    annual_down_std = down_std * np.sqrt(annual_days)

    # 索提诺比率
    annual_return = daily_returns.mean() * annual_days
    if annual_down_std:
        sortino_ratio = (annual_return - risk_free) / annual_down_std
    else:
        sortino_ratio = 0

    return {
        "sortino_ratio": sortino_ratio,
        "annual_down_std": annual_down_std,
    }
索提诺比率详解

核心思想传统夏普比率把所有波动都视为风险,但投资者实际上只关心下行风险(亏损的波动)。索提诺比率只计算负收益的波动率,更符合投资者的真实感受。

计算方法

  • 分子:超额收益率(策略收益率 - 无风险利率)

  • 分母:下行标准差(只计算低于目标收益的波动)

优势对比

指标

夏普比率

索提诺比率

波动率计算

全部波动

仅下行波动

风险理解

技术性风险

投资者感知风险

适用场景

一般评估

精确风险评估

经验值参考

  • 1.0以上:较好的风险调整收益

  • 1.5以上:优秀的风险调整收益

  • 2.0以上:卓越的风险调整收益

2.2 卡尔马比率

卡尔马比率专注于最大回撤风险,是年化收益率与最大回撤的比值:

# 计算卡尔马比率
max_ddpercent = daily_df["ddpercent"].min() if "ddpercent" in daily_df.columns else 0
calmar_ratio = abs(annual_return * 100 / max_ddpercent) if max_ddpercent else 0
卡尔马比率详解

核心思想卡尔马比率关注的是"为了获得收益,我需要承受多大的最大回撤风险"。这个指标特别适合评估趋势跟踪策略。

计算方法

  • 公式:卡尔马比率 = 年化收益率 ÷ 最大回撤百分比

  • 示例:年化收益20%,最大回撤10%,卡尔马比率 = 2.0

实际意义

  • 风险收益权衡:数值越高,说明承受单位回撤风险获得的收益越多

  • 策略比较:在比较不同策略时,卡尔马比率提供了统一的评估标准

  • 资金管理:帮助确定可接受的最大回撤水平

经验值参考

  • 1.0以上:可接受的风险收益比

  • 2.0以上:较好的风险收益比

  • 3.0以上:优秀的风险收益比

使用注意

  • 适合评估长期策略,短期策略可能失真

  • 需要足够长的回测期间才有统计意义

  • 与最大回撤的持续时间结合分析更有价值

2.3 EWM夏普比率

传统夏普比率给所有历史数据相同权重,EWM夏普比率给近期数据更高权重:

EWM夏普比率详解

改进思路

  • 时效性:近期表现比历史表现更重要

  • 适应性:能够更快反映策略性能的变化

  • 实用性:更符合实际投资决策的时间偏好

计算特点

  • 使用指数加权移动平均计算收益率和波动率

  • 近期数据权重更高,远期数据权重逐渐衰减

  • 能够更敏感地捕捉策略性能的变化趋势

应用价值

  • 策略监控:及时发现策略性能恶化

  • 动态调整:为仓位调整提供更及时的信号

  • 风险预警:提前识别潜在的风险变化

2.4 综合评分系统

单一指标往往无法全面评估策略优劣,我设计了一个多维度的综合评分系统:

def calculate_comprehensive_rating(statistics: Dict) -> float:
    """
    计算综合评分
    基于多个指标的加权平均,使用对数变换处理极端值
    """
    try:
        # 获取关键指标
        ewm_sharpe = statistics.get("ewm_sharpe", 0)
        max_ddpercent = statistics.get("max_ddpercent", 0)
        win_rate = statistics.get("win_rate", 0)
        average_win_loss_ratio = statistics.get("average_win_loss_ratio", 0)
        calmar_ratio = statistics.get("calmar_ratio", 0)
        
        # 归一化处理(使用对数变换避免极端值)
        normalized_sharpe = 0if ewm_sharpe <= 0else np.log1p(ewm_sharpe)
        normalized_drawdown = 1 - min(abs(max_ddpercent) / 100, 1)
        normalized_winloss = 0if average_win_loss_ratio <= 1else np.log1p(average_win_loss_ratio - 1)
        normalized_winrate = 0if win_rate < 0.35else win_rate
        normalized_calmar = 0if calmar_ratio <= 0else np.log1p(calmar_ratio)

        # 加权综合评分
        overall_rating = (
            0.35 * normalized_sharpe +     # EWM Sharpe权重35%
            0.30 * normalized_drawdown +   # 最大回撤权重30%
            0.20 * normalized_winrate +    # 胜率权重20%
            0.10 * normalized_winloss +    # 盈亏比权重10%
            0.05 * normalized_calmar       # 卡尔马比率权重5%
        )

        return overall_rating
    except Exception:
        return0
综合评分系统详解

设计理念量化交易中,单一指标容易误导判断。比如高胜率可能伴随低盈亏比,高收益可能伴随高回撤。综合评分系统通过多指标加权,提供更全面的策略评估。

权重分配逻辑

指标

权重

选择理由

EWM夏普比率

35%

核心风险调整收益指标,且考虑时效性

最大回撤控制

30%

风险控制是量化交易的生命线

胜率

20%

反映策略预测准确性和心理舒适度

盈亏比

10%

风险收益特征的重要补充

卡尔马比率

5%

长期风险收益评估的补充

归一化处理

对数变换的必要性

  • 问题:不同指标量纲不同,直接加权会被大数值指标主导

  • 解决:使用np.log1p()对数变换,压缩极端值影响

  • 好处:保持指标间的相对关系,避免异常值扭曲结果

阈值设计

  • **胜率阈值35%**:低于此值的策略基本不可用

  • **回撤上限100%**:避免极端回撤策略获得高分

  • 盈亏比阈值1.0:低于1的盈亏比没有正贡献

评分解读

评分区间

策略质量

建议

0.8以上

优秀策略

可考虑实盘,但需验证稳定性

0.6-0.8

良好策略

可进一步优化后实盘

0.4-0.6

一般策略

需要显著改进

0.4以下

较差策略

不建议使用

自定义权重根据你的交易风格,可以调整权重:

  • 保守型:增加回撤权重至40%,降低夏普权重

  • 激进型:增加夏普权重至45%,降低回撤权重

  • 稳健型:增加胜率权重至30%,平衡其他指标

使用注意事项

  1. 相对评估:主要用于策略间比较,不是绝对标准

  2. 市场环境:不同市场环境下的最优权重可能不同

  3. 样本充足:需要足够的交易样本才有统计意义

  4. 定期校准:随着市场变化和经验积累,应定期调整权重

三、时间维度分析

时间维度分析帮助我们理解策略在不同时间周期的表现特征,发现隐藏的规律和风险。

3.1 月度统计分析

月度分析能够揭示策略的长期稳定性:

def calculate_monthly_statistics(trade_pairs: List[Dict], size: float) -> DataFrame:
    """
    计算月度统计数据
    """
    ifnot trade_pairs:
        return DataFrame()

    trade_data = []
    for pair in trade_pairs:
        # 计算盈亏
        if pair["direction"] == Direction.LONG:
            pnl = (pair["close_price"] - pair["open_price"]) * pair["volume"] * size
        else:
            pnl = (pair["open_price"] - pair["close_price"]) * pair["volume"] * size
        
        trade_data.append({
            "close_dt": pair["close_dt"],
            "pnl": pnl
        })

    # 创建DataFrame并按月分组
    trade_df = DataFrame(trade_data)
    trade_df["month"] = trade_df["close_dt"].dt.to_period("M")

    # 计算每月统计
    monthly_stats = trade_df.groupby("month").agg(
        total_trades=("pnl", "size"),
        win_rate=("pnl", lambda x: (x > 0).sum() / x.size if x.size > 0else0),
        total_pnl=("pnl", "sum")
    ).reset_index()

    monthly_stats["win_rate"] = (monthly_stats["win_rate"] * 100).apply(lambda x: f"{x:.2f}%")
    return monthly_stats
月度统计分析详解

分析价值

稳定性评估

  • 连续盈利月数:评估策略的持续性

  • 最大连续亏损月数:评估最坏情况的持续时间

  • 月度胜率分布:了解策略表现的一致性

3.2 半小时区间统计

交易时段分析帮助识别最佳交易窗口和避开不利时段:

def calculate_interval_statistics(trade_pairs: List[Dict], size: float) -> DataFrame:
    """
    计算每个半小时交易区间的统计数据
    """
    trade_data = []
    for pair in trade_pairs:
        # 计算盈亏
        pnl = calculate_trade_pnl(pair, size)
        
        # 创建半小时区间标识
        open_dt = pair["open_dt"]
        interval_start = f"{open_dt.hour:02d}:{open_dt.minute // 30 * 30:02d}"
        
        trade_data.append({
            "interval_start": interval_start,
            "pnl": pnl
        })

    # 按半小时区间分组统计
    trade_df = DataFrame(trade_data)
    interval_stats = trade_df.groupby("interval_start").agg(
        total_trades=("pnl", "size"),
        win_rate=("pnl", lambda x: (x > 0).sum() / x.size if x.size > 0else0),
        total_pnl=("pnl", "sum")
    ).reset_index()

    return interval_stats.sort_values(by="total_pnl", ascending=True)
半小时区间统计详解

时段特征分析

**开盘时段 (09:00-10:00)**:

  • 特点:波动大,流动性充足

  • 机会:捕捉隔夜消息面影响

  • 风险:情绪化交易较多,噪音大

**上午时段 (10:00-11:30)**:

  • 特点:趋势相对明确

  • 机会:跟随主力资金方向

  • 风险:可能出现假突破

**午间时段 (13:30-14:00)**:

  • 特点:成交量萎缩,波动减小

  • 机会:适合震荡策略

  • 风险:流动性不足,滑点增大

**下午时段 (14:00-15:00)**:

  • 特点:全天最重要的交易时段

  • 机会:主力资金活跃,趋势明确

  • 风险:竞争激烈,需要快速反应

实际应用示例

最佳交易时段识别

# 找出盈利最好的时段
best_intervals = interval_stats.nlargest(3, 'total_pnl')
print("最佳交易时段:")
for _, row in best_intervals.iterrows():
    print(f"{row['interval_start']}: 盈利{row['total_pnl']:.2f}, 胜率{row['win_rate']:.2%}")

# 找出应该避免的时段  
worst_intervals = interval_stats.nsmallest(3, 'total_pnl')
print("\n应避免交易时段:")
for _, row in worst_intervals.iterrows():
    print(f"{row['interval_start']}: 亏损{row['total_pnl']:.2f}, 胜率{row['win_rate']:.2%}")

时段过滤策略

# 实现时段过滤
def should_trade_now(current_time):
    """根据历史统计决定是否应该交易"""
    hour = current_time.hour
    minute = current_time.minute
    interval_key = f"{hour:02d}:{minute // 30 * 30:02d}"
    
    # 获取该时段的历史表现
    interval_performance = interval_stats[
        interval_stats['interval_start'] == interval_key
    ]
    
    if interval_performance.empty:
        returnFalse
        
    # 只在盈利时段交易
    return interval_performance.iloc[0]['total_pnl'] > 0

风险控制

  • 时段仓位:在高风险时段降低仓位

  • 止损设置:在波动大的时段设置更严格的止损

  • 流动性管理:避免在低流动性时段进行大额交易

四、集成到回测框架

在设计增强功能时,我面临一个重要的选择:是重写整个回测框架,还是在现有框架基础上增强?经过深思熟虑,我选择了后者。这个决定基于几个考虑:

设计原则

  • 向后兼容:不能破坏现有用户的代码和工作流

  • 渐进增强:新功能作为可选项,用户可以选择性使用

  • 最小侵入:尽量减少对原有代码的修改

  • 模块化设计:新功能独立成模块,便于维护和测试

4.1 交易配对生成的集成思路

最关键的是在哪个环节生成交易配对。我选择在calculate_result方法中进行,原因是:

  1. 数据完整性:此时所有交易数据已经生成完毕

  2. 时机合适:在统计计算之前,为后续增强指标提供数据基础

  3. 影响最小:不会影响策略的执行逻辑

# 在vnpy_ctastrategy/backtesting.py的calculate_result方法中添加
if results:
    self.daily_df = DataFrame.from_dict(results).set_index("date")

# 生成交易配对用于增强统计
# 这里的设计思路是:只有当交易数据存在时才进行配对
# 避免在空数据情况下产生错误
from .enhanced_backtesting import generate_trade_pairs
sorted_trades = sorted(self.trades.values(), key=lambda x: x.datetime)
self.trade_pairs = generate_trade_pairs(sorted_trades)

为什么要排序交易?交易配对算法需要按时间顺序处理交易,确保开仓和平仓的逻辑正确。原始的交易数据可能因为并发或其他原因不是严格按时间排序的。

4.2 统计指标计算的重构思路

原有的calculate_statistics方法主要计算基础指标,我需要在不破坏原有逻辑的前提下,巧妙地插入增强指标的计算。

设计挑战

  1. 数据依赖:增强指标需要交易配对数据,但原方法没有这个概念

  2. 输出格式:需要保持原有输出格式,同时增加新的输出内容

  3. 异常处理:当交易数据不足时,要优雅地降级到基础功能

解决方案我采用了"检查-计算-合并"的三步策略:

# 计算增强交易统计
from .enhanced_backtesting import (
    calculate_trade_statistics,
    calculate_advanced_metrics,
    calculate_monthly_statistics,
    calculate_interval_statistics,
    calculate_comprehensive_rating
)

# 第一步:初始化空的增强数据结构
trade_statistics = {}
monthly_statistics = DataFrame()
interval_statistics = DataFrame()
advanced_metrics = {}

# 第二步:检查数据可用性,避免在无数据时出错
if hasattr(self, 'trade_pairs') and self.trade_pairs:
    trade_statistics = calculate_trade_statistics(self.trade_pairs, self.size)
    monthly_statistics = calculate_monthly_statistics(self.trade_pairs, self.size)
    interval_statistics = calculate_interval_statistics(self.trade_pairs, self.size)

# 高级指标需要日线数据,单独检查
if positive_balance andnot df.empty:
    advanced_metrics = calculate_advanced_metrics(df, self.capital, self.risk_free, self.annual_days)

# 第三步:智能输出 - 只有当数据存在时才输出对应部分
if output and trade_statistics:
    self.output("-" * 30)
    self.output("增强交易统计指标")
    self.output(f"胜率: \t{trade_statistics.get('win_rate', 0):.2%}")
    self.output(f"盈亏比: \t{trade_statistics.get('average_win_loss_ratio', 0):.2f}")
    self.output(f"获利因子: \t{trade_statistics.get('profit_factor', 0):.2f}")
    # ... 更多指标输出

为什么用hasattr检查?hasattr(self, 'trade_pairs')确保只有在交易配对生成成功时才计算增强指标。这样即使增强模块有问题,基础回测功能仍然可用。

输出设计的考虑

  • 使用分隔线区分不同类型的指标

  • 保持与原有输出格式的一致性

  • 采用条件输出,避免空数据时的尴尬显示

五、用户界面的重新设计

在功能实现完成后,我面临另一个挑战:如何让用户友好地使用这些新功能?原有的界面虽然功能完整,但在信息组织和用户体验方面还有提升空间。

图片

图片

图片

图片

图片

回测指标分组显示界面:包含基础信息、收益指标、风险指标等8个分组

5.1 统计监控器的重新思考

原有界面的问题使用原有界面一段时间后,我发现几个问题:

  1. 信息过载:30多个指标平铺展示,用户很难快速找到关键信息

  2. 逻辑混乱:相关指标分散在不同位置,缺乏逻辑分组

  3. 视觉疲劳:单调的列表形式,缺乏视觉层次

分组设计的灵感我想到了现代软件的设计理念:信息分组和渐进式披露。就像手机设置界面一样,相关功能归类在一起,用户可以快速定位到感兴趣的部分。

实现思路

# vnpy_ctabacktester/ui/enhanced_widget.py
class EnhancedStatisticsMonitor(QtWidgets.QTableWidget):
    """
    增强的统计指标监控器
    核心改进:按功能逻辑分组显示指标
    """

    # 分组设计:将相关指标归类,提高信息查找效率
    GROUPED_INDICATORS = {
        "【基础信息】": [
            ("start_date", "首个交易日"),
            ("end_date", "最后交易日"),
            ("capital", "起始资金"),
            ("end_balance", "结束资金"),
        ],
        "【收益指标】": [
            ("total_return", "总收益率"),
            ("annual_return", "年化收益"),
            ("daily_return", "日均收益率"),
        ],
        # ... 其他分组
    }

分组逻辑的考虑

  • 基础信息:回测的基本参数,用户首先关心的内容

  • 收益指标:各种收益率指标,投资者最关注的部分

  • 风险指标:回撤、波动率等风险度量

  • 交易统计:胜率、盈亏比等交易质量指标

  • 持仓统计:持仓时间相关的分析

  • 综合评分:最终的策略评估结果

这样的分组让用户可以快速定位到感兴趣的指标类别,提高了使用效率。

5.2 分组界面的技术实现

界面布局的挑战实现分组显示比想象中复杂。主要挑战是:

  1. 动态行数计算:需要为每个分组标题预留行

  2. 标题行样式:分组标题需要与数据行区分开

  3. 数据映射:保持原有的数据绑定逻辑不变

解决方案的演进我尝试了几种方案:

方案一:合并单元格最初想用单元格合并来实现分组标题,但发现Qt的合并单元格在数据更新时容易出现显示问题。

方案二:分离标题和数据考虑过将分组标题放在垂直表头,数据放在单元格,但这样分组效果不够明显。

方案三:独立标题行(最终采用)为每个分组创建独立的标题行,在数据单元格中显示分组名称:

def init_ui(self) -> None:
    # 计算总行数(包括分组标题行)
    total_rows = sum(len(items) + 1for items in self.GROUPED_INDICATORS.values())
    self.setRowCount(total_rows)
    
    current_row = 0
    for group_name, group_items in self.GROUPED_INDICATORS.items():
        # 创建分组标题行
        group_cell = QtWidgets.QTableWidgetItem()
        group_cell.setText(group_name)  # 在数据单元格中显示分组名称
        group_cell.setTextAlignment(QtCore.Qt.AlignCenter)  # 居中显示
        group_cell.setFlags(QtCore.Qt.ItemIsEnabled)  # 不可选择
        font = group_cell.font()
        font.setBold(True)  # 加粗字体
        group_cell.setFont(font)
        self.setItem(current_row, 0, group_cell)
        current_row += 1
        
        # 创建分组内的数据行
        for key, name in group_items:
            cell = QtWidgets.QTableWidgetItem()
            self.setItem(current_row, 0, cell)
            self.cells[key] = cell  # 保持原有的数据绑定
            current_row += 1

设计细节的考虑

  • 字体加粗:分组标题使用加粗字体,增强视觉层次

  • 居中对齐:分组标题居中显示,与左对齐的数据行形成对比

  • 不可选择:分组标题行设为不可选择,避免用户误操作

  • 向后兼容:保持self.cells[key]的映射关系,确保数据更新逻辑不变

5.3 参数优化窗口的用户体验改进

参数优化结果窗口

参数优化结果窗口

原有优化窗口的痛点在实际使用参数优化功能时,我发现了几个明显的痛点:

  1. 信息过载:几百个参数组合的结果,用户很难快速找到好的策略

  2. 缺乏筛选:只能手动滚动查看,没有智能筛选和排序功能

  3. 导出困难:无法导出筛选后的结果,不便于后续分析

用户需要的不是看到所有数据,而是快速找到符合条件的数据。

class EnhancedOptimizationResultMonitor(QtWidgets.QDialog):
    """
    增强的参数优化结果监控器
    核心改进:智能筛选和批量导出
    """

    def init_ui(self) -> None:
        # 筛选条件的层次设计
        self.filter_combo = QtWidgets.QComboBox()
        self.filter_combo.addItems([
            # 第一层:基础筛选
            "全部",
            "综合评分>0.5", 
            "综合评分>0.7",
            
            # 第二层:单指标筛选
            "夏普比率>1", 
            "夏普比率>1.5",
            "最大回撤<5%",
            "最大回撤<10%", 
            "胜率>60%",
            "胜率>70%",
            "获利因子>1.5",
            "获利因子>2.0",
            "盈亏比>1.5",
            "盈亏比>2.0",
            
            # 第三层:组合条件筛选
            "优质策略(综合评分>0.6且夏普>1且回撤<10%)",
            "高收益策略(总收益>20%且夏普>1)",
            "低风险策略(最大回撤<5%且夏普>0.5)"
        ])

筛选条件的实用考虑

  • 综合评分筛选:快速过滤掉明显不合格的策略

  • 单指标筛选:针对特定需求(如低回撤、高胜率)

  • 组合条件:预设的常用策略类型,满足不同风险偏好

筛选条件界面

筛选条件界面

增强的参数优化结果窗口,支持16种筛选条件和详细报告导出

5.4 智能筛选的技术实现

数据解析的挑战实现筛选功能时遇到的第一个挑战是数据解析。表格中的数据是字符串格式,包含百分号、逗号等格式化字符,需要正确解析为数值才能进行比较。

容错设计的重要性在实际使用中,可能出现数据缺失、格式异常等情况。我采用了多层容错设计:

def apply_filter(self):
    """应用筛选条件 - 重点关注容错和性能"""
    filter_text = self.filter_combo.currentText()
    
    for row in range(self.table.rowCount()):
        try:
            # 数据提取:安全地获取各列数据
            rating = float(self.table.item(row, 1).text()) if self.table.item(row, 1) else0
            sharpe = float(self.table.item(row, 2).text()) if self.table.item(row, 2) else0
            
            # 处理百分比格式的数据
            win_rate_text = self.table.item(row, 5).text() if self.table.item(row, 5) else"0%"
            win_rate = float(win_rate_text.replace('%', '')) / 100if'%'in win_rate_text else float(win_rate_text)
            
            # 处理回撤数据(可能是负数)
            max_dd = abs(float(self.table.item(row, 4).text())) if self.table.item(row, 4) else100
            
            # 复合条件筛选:体现业务逻辑
            if filter_text == "优质策略(综合评分>0.6且夏普>1且回撤<10%)":
                should_show = rating > 0.6and sharpe > 1and max_dd < 10
            elif filter_text == "高收益策略(总收益>20%且夏普>1)":
                total_return = float(self.table.item(row, 10).text()) if self.table.item(row, 10) else0
                should_show = total_return > 20and sharpe > 1
            elif filter_text == "低风险策略(最大回撤<5%且夏普>0.5)":
                should_show = max_dd < 5and sharpe > 0.5
            # ... 其他筛选条件
            
        except (ValueError, AttributeError):
            # 容错处理:数据异常时隐藏该行
            should_show = False
        
        self.table.setRowHidden(row, not should_show)

筛选逻辑的业务考虑每个筛选条件都体现了实际的业务需求:

  • 优质策略:平衡收益、风险和稳定性的综合考虑

  • 高收益策略:适合风险承受能力强的投资者

  • 低风险策略:适合保守型投资者

5.5 完整的控制台输出

现在运行回测后,控制台会显示完整的增强统计信息:

# 运行回测示例
from vnpy_ctastrategy import BacktestingEngine
from datetime import datetime

# 创建回测引擎并运行
engine = BacktestingEngine()
engine.set_parameters(
    vt_symbol="IF88.CFFEX",
    interval="1m", 
    start=datetime(2023, 1, 1),
    end=datetime(2023, 12, 31),
    rate=0.0001,
    slippage=0,
    size=300,
    pricetick=0.2,
    capital=1000000
)

engine.add_strategy(MyStrategy, {})
engine.load_data()
engine.run_backtesting()

# 计算结果(现在包含所有增强指标)
df = engine.calculate_result()
statistics = engine.calculate_statistics(output=True)  # 设置为True显示详细输出

实际控制台输出效果:

开始计算策略统计指标
------------------------------
首个交易日: 2025-05-07
最后交易日: 2025-09-16
总交易日: 94
盈利交易日: 53
亏损交易日: 22
起始资金: 100,000.00
结束资金: 164,185.81
总收益率: 64.19%
年化收益: 163.88%
最大回撤:  -3,960.00
百分比最大回撤: -2.50%
最大回撤天数:  4
总盈亏: 64,185.81
总手续费: 314.19
总滑点: 0.00
总成交金额: 3,141,870.00
总成交笔数: 52
日均盈亏: 682.83
日均手续费: 3.34
日均滑点: 0.00
日均成交金额: 33,424.15
日均成交笔数: 0.55
日均收益率: 0.53%
收益标准差: 1.55%
Sharpe Ratio: 5.26
EWM Sharpe: 5.25
收益回撤比: 25.63
------------------------------
增强交易统计指标
胜率:  62.96%
盈亏比:  10.89
获利因子:  18.50
平均每笔交易盈亏:  2158.89
最大连续盈利次数:  10
最大连续亏损次数:  5
最优仓位比例:  29.78%
平均持仓时间:  3.61天
最大持仓时间:  12.11天
最小持仓时间:  0.02天
中位数持仓时间:  2.05天
索提诺比率: 8.30
卡尔马比率: 65.45
------------------------------
月度统计数据
month  total_trades win_rate  total_pnl
2025-05             4   50.00%     3330.0
2025-06             9   44.44%     7410.0
2025-07             6   83.33%    34980.0
2025-08             5  100.00%    12390.0
2025-09             3   33.33%      180.0
------------------------------
半小时区间统计数据
interval_start  total_trades  win_rate  total_pnl
         21:00             3    0.00%     -420.0
         09:30             3   66.67%     -330.0
         22:00             1  100.00%     2340.0
         14:00             2   50.00%     2790.0
         11:00             2   50.00%     3450.0
         21:30             5   60.00%     5640.0
         10:30             3  100.00%     6420.0
         13:30             6   66.67%     9210.0
         14:30             2  100.00%    29190.0

控制台输出效果

控制台输出效果

完整的控制台输出,包含增强交易统计、月度统计和区间统计数据

六、总结

6.1 应用价值

策略评估更全面
  • 多维度分析:从收益、风险、交易质量等多角度评估

  • 时间维度洞察:发现策略在不同时间段的表现规律

  • 风险量化:更精确的风险指标帮助控制回撤

参数优化更高效
  • 快速筛选:16种筛选条件快速找到优秀参数

  • 批量分析:支持大量参数组合的批量分析

  • 结果导出:便于进一步分析和记录

交易决策更科学
  • 仓位指导:凯利公式提供科学的仓位建议

  • 时机把握:区间统计帮助选择最佳交易时段

  • 风险控制:连续亏损统计帮助设置止损策略

实际开发建议

基于我的实际开发和使用经验,给大家几个建议:

1. 指标权重要合理

综合评分中各指标的权重需要根据你的交易风格调整。如果你更注重稳定性,可以增加最大回撤的权重;如果追求收益,可以增加夏普比率的权重。

2. 不要过度拟合指标

虽然指标越多越全面,但不要为了追求完美指标而过度拟合历史数据。记住,指标只是参考,实际交易中的执行才是关键。

3. 重视交易成本

在计算各项指标时,一定要充分考虑手续费和滑点。很多看起来很好的策略,加上交易成本后可能就不那么亮眼了。

4. 定期更新指标体系

随着对量化交易理解的深入,你可能会发现新的有用指标。保持开放的心态,不断完善你的指标体系。

下一步计划

在下一篇文章中,我们将继续优化回测框架:

  • 导出功能增强:CSV导出所有回测参数、详细参数报告生成

  • AI参数分析:让AI帮助分析回测报告,自动识别策略优势和风险点

  • 可视化图表:集成滚动夏普比率图、持仓时间分布直方图等专业图表

写在最后

经过这次深入的回测框架优化,我们成功实现了一套完整的增强回测系统。这为我们后续的工作奠定了坚实基础:

  • 实盘交易:回测验证的策略可以更自信地投入实盘

  • 风险管理:丰富的风险指标为风控系统提供数据支撑

  • 策略研发:完善的评估体系加速新策略的开发迭代

记住,工具再好也只是工具,关键是要理解每个指标背后的含义,并结合实际市场情况来判断策略的优劣。数据不会说谎,但解读数据需要经验和智慧。


完整代码获取

本文涉及的新代码文件:

  • vnpy_ctastrategy/enhanced_backtesting.py - 增强指标计算核心

  • vnpy_ctabacktester/ui/enhanced_widget.py - 增强UI组件

本文是《以AI量化为生》系列文章的第八篇,完整代码已开源至GitHub:https://github.com/seasonstar/atmquant

本文内容仅供学习交流,不构成任何投资建议。交易有风险,投资需谨慎。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐