2. 实时计算基础理论:流处理与批处理的区别、事件时间与处理时间、水位线(Watermark)机制、背压(Backpressure)原理
各位同学,今天我们来聊聊实时计算引擎在量化交易中的那些基础理论。说实话,这些概念听起来有点抽象,但你在实际做策略的时候,天天都会跟它们打交道。我刚开始接触这块时,也踩过不少坑,今天就把我的经验分享给你们。
2.1 流处理与批处理的区别
先问大家一个问题:你平时做回测,用的是历史数据对吧?那叫批处理。但如果你要跑实盘,数据是一笔一笔进来的,那就是流处理。这两者有什么区别?
批处理,说白了就是「攒够了再算」。你把一段时间的数据收集起来,然后一次性计算。比如每天收盘后,把当天的所有成交数据拉下来,算个均值、方差什么的。这种方式的好处是简单、稳定,但缺点也很明显——延迟高。
流处理呢?它是「来一条算一条」。每来一笔行情数据,系统立刻处理。比如你监控着某只股票的逐笔成交,一旦发现大单买入,马上触发你的交易逻辑。延迟可以做到毫秒级。
我个人的习惯是:回测用批处理,实盘用流处理。但要注意,这两者的逻辑有时候会不一样。举个例子:
// 批处理:计算过去5分钟的均价
double avgPrice = dataWindow.stream()
.filter(tick -> tick.time >= now - 5 * 60 * 1000)
.mapToDouble(Tick::getPrice)
.average()
.orElse(0.0);
// 流处理:每来一笔数据,更新滑动窗口的均价
public void onTick(Tick tick) {
window.add(tick);
window.removeExpired(now - 5 * 60 * 1000);
double avgPrice = window.getAverage();
}
你看,批处理是「事后诸葛亮」,流处理是「实时反应」。在量化交易中,我们大部分场景都需要流处理,因为市场不等人。
核心区别总结:
- 延迟:批处理分钟级到小时级,流处理毫秒级到秒级
- 数据范围:批处理处理全量数据,流处理处理增量数据
- 计算模式:批处理一次性计算,流处理持续计算
- 状态管理:批处理无状态,流处理需要维护状态
2.2 事件时间与处理时间
嗯,这里有个容易混淆的概念——事件时间和处理时间。我当年刚入行时,就因为这个吃过亏。
事件时间,就是数据实际发生的时间。比如一笔成交在 14:30:15 发生,那事件时间就是 14:30:15。这个时间是由数据源(比如交易所)打上的,不会变。
处理时间,是数据到达你的系统的时间。比如因为网络延迟,这笔成交数据在 14:30:18 才到你的服务器,那处理时间就是 14:30:18。
你想想看,如果网络一直稳定,这两个时间差不多。但现实是网络会抖动,数据会乱序。比如:
- 14:30:15 的成交,14:30:18 到达
- 14:30:16 的成交,14:30:17 到达(反而先到了)
如果你用处理时间来计算,那 14:30:16 的成交会被先处理,14:30:15 的反而后处理。这会导致你的指标计算完全乱掉。
我的建议:在量化交易中,尽量使用事件时间。因为交易策略的逻辑是基于市场实际发生的时间顺序,而不是数据到达的顺序。我曾经有一个策略,因为用了处理时间,导致信号延迟了 3 秒,回测看起来很好,实盘一跑就亏钱。
2.3 水位线(Watermark)机制
说到事件时间,就不得不提水位线。这是个什么东西呢?
简单来说,水位线是一个「时间标记」,告诉系统:到这个时间点为止,数据应该都到齐了。如果还有更早的数据,那就是迟到的数据,你可以选择丢弃或者特殊处理。
举个例子:你设置水位线为 5 秒。也就是说,系统认为 5 秒前的数据应该都到了。比如现在是 14:30:20,那系统认为 14:30:15 之前的数据都到齐了。如果这时候突然来了一笔 14:30:14 的数据,那就是迟到数据。
为什么会这样?因为网络延迟、系统负载等原因,数据可能乱序到达。水位线就是用来处理这种乱序的。
// 伪代码:水位线机制
Watermark watermark = new Watermark(5, TimeUnit.SECONDS);
public void onTick(Tick tick) {
long eventTime = tick.getEventTime();
long currentWatermark = watermark.getCurrentWatermark();
if (eventTime < currentWatermark) {
// 迟到数据,特殊处理
handleLateData(tick);
} else {
// 正常处理
processTick(tick);
// 更新水位线
watermark.update(eventTime);
}
}
注意:水位线设置太短,会导致大量迟到数据被丢弃,影响策略准确性。设置太长,又会增加延迟。我一般建议根据网络延迟的 99 分位值来设置,比如网络延迟 99% 的情况下不超过 2 秒,那水位线就设 2 秒。
2.4 背压(Backpressure)原理
最后聊聊背压。这个词听起来很专业,其实道理很简单——就是「上游太快,下游跟不上」。
在量化交易中,行情数据是源源不断的。比如某只股票在开盘时,每秒可能有几千笔成交。如果你的处理逻辑比较复杂,比如要计算多个指标、更新多个窗口,那处理速度可能跟不上数据到达的速度。
这时候会发生什么?数据会堆积在缓冲区里,内存占用越来越高,最终导致 OOM(内存溢出)或者系统崩溃。
背压机制就是用来解决这个问题的。它让下游告诉上游:「我吃不下了,你慢点发」。具体实现方式有几种:
- 阻塞式:下游处理不过来时,阻塞上游的数据发送。简单粗暴,但可能导致整个系统停顿。
- 丢弃式:下游处理不过来时,丢弃部分数据。适合对数据完整性要求不高的场景。
- 反压式:下游通过某种协议(比如 TCP 的滑动窗口)告诉上游降低发送速率。这是最优雅的方式。
我个人在项目中遇到过这种情况:有一次做高频策略,行情数据量突然暴增,系统直接挂了。后来加了背压机制,虽然会丢弃一些数据,但至少系统不会崩溃。嗯,这里要注意,丢弃数据对量化交易来说是有风险的,所以最好还是做好容量规划。
背压处理的最佳实践:
- 先做容量评估,确保系统能处理峰值流量的 2-3 倍
- 如果还是处理不过来,优先丢弃非关键数据(比如 tick 级别的数据可以降级为 1 秒快照)
- 监控背压状态,一旦触发背压,立刻报警
知识体系总览
下面这张图,是我画的一个知识体系总览,帮你理清这些概念之间的关系:
好了,这一章的内容就到这里。这些基础理论,是后面所有实战内容的地基。你把这些概念吃透了,后面学 Flink、Spark Streaming 这些框架时,就会轻松很多。
公众号:蓝海资料掘金营,微信deep3321