matters
V2EX  ›  投资

[分享] 如何避免量化回测里的未来函数?

  •  
  •   matters · May 21 · 543 views

    上次问了回测和实盘差距的因素,V 友 @KeinHong 特别提了“数据有未来函数”,最近又重新研究了下这块,和大家分享下收获:

    1. 什么是未来函数?

    简单来说,就是在 T 时刻的代码逻辑里,使用了 T+1 时刻的信息。

    最典型的逻辑错误:

    • 收盘价陷阱: 计算出今日收盘价突破压力位,然后在“今日开盘”买入。

    • 全局归一化: 在预处理数据时,用了整个数据集的最大值/最小值。

    • 偷看一眼: 使用 df\['close'\].shift(-1) 却忘了这是在模拟历史。

    2. 难以察觉未来函数?

    在复杂的策略中,未来函数往往隐藏在数据清洗和特征工程阶段。

    一旦引入,回测结果就会变成上帝视角,曲线完美,但实盘时并没有上帝视角。

    3. 尝试去掉未来函数

    以下是一个简单的 Python 示例,演示如何获取数据并确保交易决策仅基于过去的信息。

    
    import time
    import requests
    import pandas as pd
    
    API_KEY = 'YOUR_API_KEY'
    BASE_URL = 'https://quote.alltick.io/quote-b-api/kline'
    
    def get_historical_data(symbol, bin_size='1m'):
        params = {
            'token': API_KEY,
            'symbol': symbol,
            'kline_type': bin_size, # 1m, 5m, 1h, 1d
            'query_count': 500
        }
        response = requests.get(BASE_URL, params=params)
        data = response.json()
        
        if data['code'] != 200:
            print("Error fetching data")
            return None
        
        df = pd.DataFrame(data['data']['list'])
        df['time'] = pd.to_datetime(df['t'], unit='s')
        df.set_index('time', inplace=True)
        return df[['o', 'h', 'l', 'c', 'v']] # Open, High, Low, Close, Volume
    
    def backtest_logic(df):
        """
        一个简单的突破策略
        核心:确保信号产生后,在下一根 K 线才能成交
        """
        # 1. 特征计算:仅使用过去的数据 (shift 1 位)
        # 计算前 20 分钟的最高价,不包含当前这一分钟
        df['prev_high'] = df['c'].shift(1).rolling(window=20).max()
        
        # 2. 产生信号:当前价格 > 过去 20 分钟最高价
        # 注意:这里的 'c' 是当前时刻确定的,买入动作必须发生在‘未来’
        df['signal'] = df['c'] > df['prev_high']
        
        # 3. 模拟成交 (关键!避免未来函数)
        # 我们不能以产生信号那一刻的收盘价成交,而应模拟以“下一分钟开盘价”买入
        df['execution_price'] = df['o'].shift(-1) 
        
        # 计算收益
        df['returns'] = 0.0
        # 只有信号为 True 且我们有下一分钟成交价时才计算
        hold_mask = df['signal'].shift(1) == True
        df.loc[hold_mask, 'returns'] = (df['c'] - df['execution_price'].shift(1)) / df['execution_price'].shift(1)
        
        return df.dropna()
    
    # 运行回测(以黄金 XAUUSD 为例)
    gold_data = get_historical_data('XAUUSD')
    if gold_data is not None:
        results = backtest_logic(gold_data)
        print(results[['c', 'prev_high', 'signal', 'returns']].tail(10))
    
    

    4. 如何自测?

    如果你怀疑回测有问题,尝试:

    1. 随机噪声法: 将历史数据的价格顺序随机打乱,如果策略还能跑出高收益,说明逻辑里肯定藏着未来函数——因为它正在利用“乱序”后的未来信息。

    2. 信号漂移检查: 记录下回测中某天的买入信号。然后删除该日期之后的所有数据,重新跑一遍。如果那个信号消失了,说明该信号依赖于未来的数据。

    最后,回测是用来证伪的,要时刻警惕 shift(-1) 或 max(future) 的逻辑,才能在量化这条路上走得远一点。欢迎大家来讨论下避免未来函数的方式

    3 replies    2026-06-01 14:08:29 +08:00
    KeinHong
        1
    KeinHong  
       May 21
    我回测里参考了别人的使用:
    1 、PIT 全量
    2 、DSR 多重检验修正
    3 、Alpha 分解日期对齐
    4 、Walk-Forward Analysis (WFA) 模块
    5 、多重检验修正
    james2013
        2
    james2013  
       May 22 via Android
    vnpy 框架有成熟的回测框架避免未来函数,k 线数组遍历,一根根 k 线依次回放,当前 bar 收盘时开仓,比如开多,要等到下根 k 线及以后,k 线最低价小于等于开仓价才能成交
    matters
        3
    matters  
    OP
       1 day ago
    @KeinHong 感谢补充!这几点确实是标配了,尤其是 WFA 滚动回测和 Alpha 分解的日期对齐。DSR 修正对抑制虚假策略也很有帮助

    @james2013 vnpy 逐 bar 撮合机制确实稳健。很多时候回测看起来很完美,就是因为在当前 Bar 还没收盘时就提前按收盘价成交了。不过,下根 K 线的滑点控制和流动性消耗通常比未来函数更磨人
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3300 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 13:59 · PVG 21:59 · LAX 06:59 · JFK 09:59
    ♥ Do have faith in what you're doing.