研究到生产的一致性
不同代码 = 不同的策略
Section titled “不同代码 = 不同的策略”回测与实盘用不同的代码是最常见的事故源:研究代码用 pandas 向量化,实盘代码用事件循环重写——两份代码对"信号"的定义必然有细微差别(边界条件、时区、缺值处理),而这些差别在纸面上不可见,在实盘里是真金白银的偏离。
事件驱动:一份代码,两种数据
Section titled “事件驱动:一份代码,两种数据”解决方式是事件驱动架构:策略逻辑只消费"事件"(一根 bar、一笔 tick),不关心事件来自历史还是实时:
┌──────────────┐ ┌──────────────┐历史数据 ──→ │ 事件回放器 │ ──┐ │ │ └──────────────┘ ├──→ │ 策略逻辑 │ ──→ 订单实时行情 ──→ 实时事件源 ──┘ │ (同一份代码) │ └──────────────┘ └──────────────┘from abc import ABC, abstractmethodimport 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 原则。
向量化 vs 事件驱动
Section titled “向量化 vs 事件驱动”| 方式 | 速度 | 与实盘一致性 | 适用 |
|---|---|---|---|
| 向量化回测 | 快 100 倍以上 | 差:成交假设、时钟都与实盘脱节 | 因子研究、初筛 |
| 事件驱动回测 | 慢 | 好:同一份代码 | 实盘前的最终验证 |
两者都用:向量化筛想法,事件驱动验实盘。研究阶段用向量化的速度,上线前必须过事件驱动的一致性检验,见回测引擎设计。
量化中的用法
Section titled “量化中的用法”- 信号逻辑一份代码,通过事件接口消费历史与实时数据;
- 回测时钟 = 数据到达时间;特征只用已到达数据;
- 上线前影子交易比对信号,一致才发真实订单。
- 向量化回测快但难以与实盘一致,事件驱动回测反之
- 把"数据到达时间"而非"数据时间戳"作为回测的时钟
- 实盘前必须做影子交易(paper trading)比对