跳转到内容

研究到生产的一致性

回测与实盘用不同的代码是最常见的事故源:研究代码用 pandas 向量化,实盘代码用事件循环重写——两份代码对"信号"的定义必然有细微差别(边界条件、时区、缺值处理),而这些差别在纸面上不可见,在实盘里是真金白银的偏离。

事件驱动:一份代码,两种数据

Section titled “事件驱动:一份代码,两种数据”

解决方式是事件驱动架构:策略逻辑只消费"事件"(一根 bar、一笔 tick),不关心事件来自历史还是实时:

┌──────────────┐ ┌──────────────┐
历史数据 ──→ │ 事件回放器 │ ──┐ │ │
└──────────────┘ ├──→ │ 策略逻辑 │ ──→ 订单
实时行情 ──→ 实时事件源 ──┘ │ (同一份代码) │
└──────────────┘ └──────────────┘
strategy.py
from abc import ABC, abstractmethod
import pandas as pd
class Strategy(ABC):
@abstractmethod
def on_bar(self, bar: pd.Series, clock: pd.Timestamp) -> list[dict]:
"""回测与实盘共用:bar 为一根数据,clock 为事件到达时间"""
class TsMomentum(Strategy):
def __init__(self, lookback: int = 126, vol_target: float = 0.10):
self.lookback, self.vol_target = lookback, vol_target
self.prices: list[float] = []
def on_bar(self, bar: pd.Series, clock: pd.Timestamp) -> list[dict]:
self.prices.append(bar["close"])
if len(self.prices) < self.lookback + 1:
return []
r = pd.Series(self.prices).pct_change(self.lookback).iloc[-2] # 只用已收数据
vol = pd.Series(self.prices).pct_change().rolling(60).std().iloc[-2] * (252 ** 0.5)
return [{"symbol": bar["symbol"], "side": "buy" if r > 0 else "sell",
"size": min(self.vol_target / max(vol, 1e-9), 2.0)}]

回测的时钟:数据到达时间,不是时间戳

Section titled “回测的时钟:数据到达时间,不是时间戳”

回测与实盘一致的关键细节:时钟用"数据到达时间"而非"数据时间戳"——

  • 实盘里你在收到 bar 时决策,不是在 bar 生成时刻;
  • 历史回放时,bar 的"到达时间"应模拟真实延迟(收盘后数秒到数分钟);
  • 一切特征只能用已到达的数据,见财务与公告数据的 as-of 原则。
方式速度与实盘一致性适用
向量化回测快 100 倍以上差:成交假设、时钟都与实盘脱节因子研究、初筛
事件驱动回测慢好:同一份代码实盘前的最终验证

两者都用:向量化筛想法,事件驱动验实盘。研究阶段用向量化的速度,上线前必须过事件驱动的一致性检验,见回测引擎设计。

  • 信号逻辑一份代码,通过事件接口消费历史与实时数据;
  • 回测时钟 = 数据到达时间;特征只用已到达数据;
  • 上线前影子交易比对信号,一致才发真实订单。
  • 向量化回测快但难以与实盘一致,事件驱动回测反之
  • 把"数据到达时间"而非"数据时间戳"作为回测的时钟
  • 实盘前必须做影子交易(paper trading)比对