4、数据源容错:多数据源订阅与优先级切换、数据质量校验、时间戳与价格合理性、数据补全机制
做量化交易,最怕什么?
不是策略亏钱,而是数据断了。
我见过太多团队,策略逻辑写得天衣无缝,结果因为一个数据源的行情延迟,导致整个系统在开盘瞬间做出错误决策。嗯,数据源容错这件事,说白了就是给你的交易系统买一份「保险」。
4.1 多数据源订阅:别把鸡蛋放在一个篮子里
我个人习惯,至少接入三个独立的数据源。为什么?
你想想看,如果只依赖一家数据商,它突然断流了怎么办?我曾在项目中遇到过某头部数据商在早盘9:15出现全品种数据中断,持续了整整3分钟。那3分钟,如果你只有单一数据源,你的系统就是「瞎子」。
多数据源订阅的核心思路:
- 主数据源:延迟最低、质量最稳定,用于实时交易决策
- 备用数据源:延迟稍高但独立,用于主源异常时的切换
- 校验数据源:用于交叉验证,不直接用于交易
关键点:三个数据源最好来自不同的基础设施提供商。比如一个用券商直连,一个用第三方数据商,一个用交易所的Level-2行情。这样即使某个网络节点出问题,也不会全军覆没。
4.2 优先级切换:谁先到就用谁?没那么简单
多数据源有了,接下来就是切换策略。很多人以为「谁先到就用谁」,这是大坑。
我记得有一次,一个备用数据源因为网络抖动,突然比主源快了2毫秒。系统自动切换过去,结果那个数据源的价格是错的——它推送的是上一笔成交价,不是最新价。差点造成一次错误开仓。
我建议的优先级切换逻辑:
- 质量优先:先校验数据质量,再比较延迟
- 延迟阈值:主源延迟超过50ms才触发切换,避免频繁抖动
- 切换确认:连续3个tick都满足切换条件,才真正切换
- 回切机制:主源恢复后,需要连续10个tick稳定,才切回去
// 伪代码:优先级切换逻辑
if (primarySource.delay > 50ms && primarySource.errorCount > 3) {
// 尝试切换到备用源
if (backupSource.isValid() && backupSource.delay < 100ms) {
switchTo(backupSource);
log("主源异常,切换到备用源");
}
}
// 回切逻辑
if (currentSource == backupSource && primarySource.recovered()) {
if (primarySource.consecutiveValidTicks >= 10) {
switchTo(primarySource);
log("主源恢复,切换回主源");
}
}
避坑指南:我曾经因为切换阈值设得太低(10ms),导致系统在早盘高频时段频繁切换,反而引入了更多延迟。后来改成50ms + 连续3个tick确认,问题就解决了。
4.3 数据质量校验:时间戳和价格,一个都不能错
数据源接进来了,但数据对不对?这才是核心问题。
我一般做两件事:时间戳校验和价格合理性校验。
4.3.1 时间戳校验
时间戳是数据的「身份证」。常见问题:
- 时间戳重复:同一个tick出现两次,可能是数据源内部重推
- 时间戳倒退:后一个tick的时间戳比前一个还早,明显异常
- 时间戳跳跃:突然跳过一个时间段,可能是数据源断流后补发
我的校验规则很简单:
// 时间戳校验
bool isTimestampValid(long currentTs, long lastTs) {
// 1. 不能重复
if (currentTs == lastTs) return false;
// 2. 不能倒退
if (currentTs < lastTs) return false;
// 3. 不能跳跃超过5秒(根据品种调整)
if (currentTs - lastTs > 5000) return false;
return true;
}
小技巧:对于股指期货这类高频品种,时间戳跳跃阈值可以设到1秒。对于股票,可以放宽到10秒。不同品种要区别对待。
4.3.2 价格合理性校验
价格校验,说白了就是判断这个价格「像不像人话」。
我常用的几个维度:
- 涨跌幅限制:超过品种的涨跌停板,直接丢弃
- 价格突变:相比上一笔价格,变化超过一定比例(比如1%),需要二次确认
- 买卖价差:卖一价必须大于买一价,否则数据异常
- 价格与均线偏离:偏离超过3个标准差,标记为可疑
// 价格合理性校验
bool isPriceValid(TickData tick, double lastPrice) {
// 1. 涨跌幅限制
if (abs(tick.price - lastPrice) / lastPrice > 0.1) return false;
// 2. 买卖价差
if (tick.askPrice <= tick.bidPrice) return false;
// 3. 价格突变检测
double changeRate = abs(tick.price - lastPrice) / lastPrice;
if (changeRate > 0.01) {
// 需要从另一个数据源交叉验证
if (!crossValidate(tick)) return false;
}
return true;
}
核心原则:宁可错过一个tick,也不要吃进一个错误tick。错误数据导致的交易损失,远大于错过一次交易机会。
4.4 数据补全机制:断流之后怎么办?
数据源断了,恢复之后怎么补?这是很多系统忽略的地方。
我见过最糟糕的做法:断流恢复后,直接把缺失的数据全部补进去,结果导致策略在短时间内收到大量历史tick,误以为市场剧烈波动,疯狂开仓。
我建议的补全策略:
- 只补关键数据:不是所有tick都需要补。只补分钟级别的OHLC(开盘价、最高价、最低价、收盘价),用于指标计算
- 标记补全数据:所有补全的数据都要打上「补全」标签,策略层可以选择是否使用
- 限制补全速度:不要一次性全部补完,按正常速度的10倍播放,避免冲击
- 补全时间窗口:只补最近5分钟的数据,超过5分钟的不补,直接跳过
// 数据补全逻辑
void fillMissingData(long startTime, long endTime) {
// 1. 只补最近5分钟
if (endTime - startTime > 300000) {
log("补全时间窗口超过5分钟,跳过");
return;
}
// 2. 从备用数据源获取缺失数据
vector<TickData> missingData = backupSource.getHistory(startTime, endTime);
// 3. 标记为补全数据
for (auto &tick : missingData) {
tick.isFilled = true;
}
// 4. 按正常速度的10倍播放
for (auto &tick : missingData) {
pushToStrategy(tick);
sleep(100); // 正常tick间隔100ms,这里sleep 10ms
}
}
避坑指南:我曾经在补全数据时,没有做「标记补全」这个操作。结果策略把补全的tick当成实时tick,触发了一个止损条件,平掉了本该持有的仓位。后来我强制要求所有补全数据必须带标记,策略层看到标记就跳过交易信号计算。
4.5 整体架构:一张图看懂
说了这么多,我们来画一张整体架构图。这张图我每次做系统设计都会贴在墙上。
这张图展示了整个数据源容错的流程:三个数据源并行接入,经过数据质量校验层过滤,再由优先级切换引擎决定用哪个源的数据,最后经过数据补全机制处理后,输出到策略引擎。
个人经验:这套架构我用了三年,经历过两次数据源大规模故障,系统都平稳切换过来了。关键不在于技术多复杂,而在于每个环节都要有「兜底」方案。
数据源容错这件事,说白了就是「宁可慢一点,不能错一点」。你想想看,一次错误的数据导致的开仓,可能让你亏掉整个月的利润。所以,花时间把容错机制做好,绝对值。