🚀 AI 一键生成 joinquant 策略代码
立即体验

聚宽策略出现多年数百倍收益?教你如何排查未来函数、滑点并设计样本外验证

joinquant | 发布时间: 2026/8/9
以下内容由 EasyQuant 生成。

问题描述

聚宽复杂 ETF 轮动策略回测:如何识别过拟合并进行样本外验证?
聚宽策略出现多年数百倍收益时,如何检查手续费、滑点、未来函数、参数调整和过拟合风险,并设计至少半年样本外验证?

解决方案

在聚宽(JoinQuant)平台上开发复杂策略(如 ETF 轮动、多因子选股)时,回测出现“多年数百倍”的超高收益往往令人兴奋,但资深交易员的第一反应通常是质疑。这种异常高收益大概率是由**未来函数、不切实际的交易摩擦设置、过拟合(Overfitting)**导致的。

以下是系统性排查策略漏洞、识别过拟合风险并设计至少半年样本外验证的完整指南:


一、 核心漏洞排查清单

1. 未来函数(Future Data)排查

未来函数是回测高收益的“头号杀手”,即在历史回测的 T 日使用了 T 日收盘后甚至 T+N 日才已知的数据。

  • 排查方法
    • initialize 中开启避免未来数据模式:
      set_option("avoid_future_data", True)
      
      开启后,若策略尝试获取未来数据,系统会抛出 FutureDataError
    • 检查数据获取 API:使用 get_priceget_bars 时,确保 end_dateend_dt 不超过当前逻辑时间 context.current_dt。在日级策略中,使用 historyattribute_history 获取天数据时,默认是不包含当天的,这能有效避免未来函数。
    • 财务数据排查:使用 get_fundamentals 时,若传入 statDate(如指定季度),可能会引入未来函数(因为财报披露有滞后性)。建议使用 date 参数查询,系统会自动寻找该日期前已披露的最新财报。

2. 交易摩擦(手续费与滑点)检查

默认的摩擦设置可能过于宽松,导致高频调仓的 ETF 轮动策略在回测中几乎没有成本。

  • 手续费设置:ETF 基金不征收印花税,但有佣金。必须显式设置:
    # 设置场内基金(ETF)的手续费:买入万三,卖出万三,无印花税,最低5元
    set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='fund')
    
  • 滑点设置:真实市场中,大额订单或流动性不足的 ETF 会产生明显滑点。默认的 PriceRelatedSlippage(0.00246) 可能不够。建议针对 ETF 设置更严格的滑点:
    # 设置基金类滑点为 0.2%(双边)
    set_slippage(PriceRelatedSlippage(0.002), type='fund')
    

3. 真实价格(动态复权)模式

  • 必须开启:在 initialize 中调用 set_option('use_real_price', True)。如果不开启,回测使用的是静态前复权价格,买入数量和持仓计算会与历史真实情况产生偏差,甚至引入未来复权因子导致的未来函数。

二、 如何识别过拟合(Overfitting)风险

过拟合是指策略过度拟合了历史回测时段的噪音,导致在历史表现极佳,但实盘(或样本外)迅速失效。

  1. 参数敏感性分析(Parameter Sweep)
    • 如果你的 ETF 轮动策略依赖于“过去 20 天动量”,尝试将参数调整为 15、18、22、25。如果只有 20 天能产生数百倍收益,而 18 天或 22 天收益暴跌甚至亏损,说明该参数是孤立的极值点,存在严重过拟合。
    • 黄金法则:优秀的策略在参数宽幅变动时,收益应当呈现平缓的钟形曲线分布,而非陡峭的孤峰。
  2. 策略复杂度评估
    • 检查策略中过滤条件和参数的数量。如果策略包含“均线 + MACD + 波动率过滤 + 行业暴露限制 + 5个自定义阈值”,参数超过 5 个,过拟合风险将指数级上升。

三、 样本外验证(Out-of-Sample Validation)设计

为了客观评估策略的真实盈利能力,必须将历史数据严格划分为训练集(样本内)测试集(样本外)

1. 数据划分方案

假设我们有 2018 年至 2024 年的数据:

  • 样本内(In-of-Sample, 80%):2018-01-01 至 2023-06-30。在此区间内进行策略开发、参数调优和规则设计。
  • 样本外(Out-of-Sample, 20%):2023-07-01 至 2024-12-31(共 1.5 年,远超半年要求)。在此区间内,绝对不允许修改任何代码和参数,直接运行回测。

2. 滚动交叉验证(Walk-Forward Analysis)

对于复杂的轮动策略,推荐使用滚动窗口进行验证:

  • 用 2020-2021 年数据训练参数 -> 验证 2022 年上半年样本外。
  • 用 2021-2022 年数据训练参数 -> 验证 2023 年上半年样本外。

四、 聚宽排查与验证模板源码

以下是一个标准的聚宽 ETF 轮动策略模板,内置了严格的摩擦成本、防未来函数设置,并展示了如何编写规范的初始化结构:

import jqdata
import pandas as pd
import numpy as np

def initialize(context):
    # 1. 开启真实价格模式(防未来函数核心)
    set_option('use_real_price', True)
    
    # 2. 开启避免未来数据模式
    set_option("avoid_future_data", True)
    
    # 3. 设置基准
    set_benchmark('000300.XSHG')
    
    # 4. 严格的交易税费设置(针对场内基金/ETF)
    # 买入万三,卖出万三,无印花税,最低5元
    set_order_cost(OrderCost(
        open_tax=0, 
        close_tax=0, 
        open_commission=0.0003, 
        close_commission=0.0003, 
        min_commission=5
    ), type='fund')
    
    # 5. 严格的滑点设置(设置 0.2% 的价格滑点,模拟真实冲击成本)
    set_slippage(PriceRelatedSlippage(0.002), type='fund')
    
    # 策略参数(样本内调优确定的参数,在样本外测试时切勿修改)
    g.momentum_window = 20  # 动量观测窗口
    g.etf_pool = [
        '510300.XSHG',  # 沪深300ETF
        '510500.XSHG',  # 中证500ETF
        '159915.XSHE',  # 创业板ETF
        '511880.XSHG'   # 银华日利(避险货币基金)
    ]
    
    # 定时运行:每周第一个交易日开盘运行
    run_weekly(handle_etf_rotation, weekday=1, time='09:30')

def handle_etf_rotation(context):
    # 获取历史数据,确保不包含当天,防止未来函数
    # 结束时间设为前一交易日
    prev_date = context.previous_date
    
    scores = {}
    for etf in g.etf_pool:
        if etf == '511880.XSHG':
            continue
        # 获取过去 N 天的收盘价
        hist = attribute_history(etf, g.momentum_window, '1d', ['close'], df=False, fq='pre')
        if len(hist['close']) < g.momentum_window:
            continue
        # 计算动量得分:(当前价 - N天前价) / N天前价
        score = (hist['close'][-1] - hist['close'][0]) / hist['close'][0]
        scores[etf] = score
        
    if not scores:
        return
        
    # 找出动量最强的 ETF
    best_etf = max(scores, key=scores.get)
    
    # 如果最强 ETF 的动量为正,则买入;否则买入货币基金避险
    target_etf = best_etf if scores[best_etf] > 0 else '511880.XSHG'
    
    # 调仓执行
    current_positions = context.portfolio.long_positions.keys()
    for position in current_positions:
        if position != target_etf:
            order_target(position, 0)
            log.info(f"卖出避险或调仓: {position}")
            
    if target_etf not in current_positions:
        cash = context.portfolio.available_cash
        if cash > 0:
            order_value(target_etf, cash)
            log.info(f"买入强势标的: {target_etf}")

五、 总结

当你在聚宽上看到令人难以置信的业绩时,请遵循以下三步:

  1. 脱水:加入万分之三佣金与单边千分之一点五的滑点,重新运行。
  2. 脱毒:开启 avoid_future_data,检查是否有代码在盘中获取了当日收盘价。
  3. 体检:将回测时间切换至从未运行过的最近半年或一年(样本外),如果曲线依然稳健,恭喜你,这才是真正具有实盘价值的量化策略。