4、实时聚合计算:Tick级OHLC计算、成交量分布(Volume Profile)实时更新、VWAP滚动计算
实时聚合计算,说白了就是给高频数据做「瘦身」和「画像」。
Tick数据流每秒成千上万笔,你不可能全存着。但交易决策又离不开这些微观结构。怎么办?
我的做法是:在内存里维护几个核心聚合器,每来一个Tick,就更新一次。不落盘,不积压,只保留最有价值的统计量。
4.1 Tick级OHLC计算:每一笔都算数
OHLC(Open, High, Low, Close)是K线的基础。但在Tick级别,它不再是「每分钟一根」的概念,而是「每来一个Tick就更新一次」。
核心逻辑:
- Open:当前时间窗口的第一笔成交价
- High:当前窗口内的最高价
- Low:当前窗口内的最低价
- Close:当前窗口内的最新价
嗯,这里要注意:窗口可以是固定时间(比如1秒、100毫秒),也可以是固定笔数(比如100笔)。我个人习惯用固定时间窗口,因为和交易所的快照对齐更方便。
避坑指南:
我曾经在某个项目中,直接用系统时间戳作为窗口边界。结果发现不同Tick的时间戳精度不一致,有的精确到微秒,有的只到毫秒。导致窗口边界错位,OHLC数据对不上。
后来我强制统一为毫秒级时间戳,并在窗口切换时加了一个「锁存」机制——当前窗口关闭后,再等一个Tick才输出,确保数据稳定。
// 伪代码:Tick级OHLC更新
class OHLCUpdater {
long windowStart;
double open, high, low, close;
void onTick(Tick tick) {
if (tick.timestamp >= windowStart + WINDOW_MS) {
// 输出当前窗口,开启新窗口
emitBar(open, high, low, close);
reset(tick.price);
}
high = Math.max(high, tick.price);
low = Math.min(low, tick.price);
close = tick.price;
}
}
4.2 成交量分布(Volume Profile)实时更新
成交量分布,说白了就是看「哪个价格区间成交最多」。这对判断支撑位和阻力位非常有用。
实时更新Volume Profile,本质上是在内存里维护一个价格-成交量直方图。每来一个Tick,找到它对应的价格区间(比如0.5元一个格子),然后把成交量累加上去。
关键设计点:
- 价格区间粒度:太细了内存爆炸,太粗了没意义。我一般用Tick价格的最小变动单位(比如0.01元)作为粒度,再根据品种波动率动态合并。
- 滑动窗口:只保留最近N笔或最近T秒的数据。否则内存撑不住。
- 增量更新:每次只更新一个格子,复杂度O(1)。
我的经验:
你想想看,如果每个Tick都去遍历所有价格区间,那性能就崩了。我建议用哈希表+有序链表的组合:哈希表用于O(1)定位格子,有序链表用于快速获取最高成交量区间。
// 伪代码:Volume Profile实时更新
class VolumeProfile {
Map<Double, Long> priceBuckets; // 价格 -> 成交量
long totalVolume;
void onTick(Tick tick) {
double bucket = Math.floor(tick.price / BUCKET_SIZE) * BUCKET_SIZE;
priceBuckets.merge(bucket, tick.volume, Long::sum);
totalVolume += tick.volume;
}
double getVPOC() { // 获取成交量最大价格区间
return priceBuckets.entrySet().stream()
.max(Map.Entry.comparingByValue())
.get().getKey();
}
}
4.3 VWAP滚动计算:别被「平均」骗了
VWAP(成交量加权平均价)是机构交易最常用的参考指标之一。它的公式很简单:总成交金额 / 总成交量。
但实时滚动计算时,有个坑:
滚动窗口的VWAP,不是简单地把所有历史数据重算一遍。那样计算量太大了。
我的做法是维护两个累加器:
- 累计成交金额:price * volume 的累加
- 累计成交量:volume 的累加
每来一个Tick,更新这两个值。VWAP = 累计金额 / 累计成交量。
注意:
滚动窗口的VWAP需要「过期数据剔除」。比如你只算最近1000笔的VWAP,那第1001笔进来时,第1笔的数据就要减掉。
我曾经用环形缓冲区来存储最近N笔的成交金额和成交量,每次更新时先减掉最旧的一笔,再加最新的一笔。这样计算量恒定,不会随着窗口变大而变慢。
// 伪代码:滚动VWAP计算
class RollingVWAP {
double sumPriceVolume = 0; // 累计成交金额
double sumVolume = 0; // 累计成交量
Queue<Tick> window = new LinkedList<>();
void onTick(Tick tick) {
window.add(tick);
sumPriceVolume += tick.price * tick.volume;
sumVolume += tick.volume;
if (window.size() > WINDOW_SIZE) {
Tick old = window.poll();
sumPriceVolume -= old.price * old.volume;
sumVolume -= old.volume;
}
double vwap = sumPriceVolume / sumVolume;
}
}
4.4 核心架构图:实时聚合计算流程
下面这张图展示了三个聚合器如何协同工作。每个Tick流入后,并行更新OHLC、Volume Profile和VWAP,互不干扰。
4.5 性能优化:别让聚合成为瓶颈
实时聚合计算,最怕的就是「计算跟不上数据」。我总结了几条经验:
- 无锁数据结构:每个聚合器独立运行,用AtomicLong或LongAdder做累加器,避免锁竞争。
- 预分配内存:Volume Profile的价格区间数量是固定的,提前分配好数组,避免动态扩容。
- 批量输出:OHLC和VWAP可以每N个Tick输出一次,而不是每个Tick都输出,减少下游处理压力。
避坑指南:
我曾经在Volume Profile的实现中用了HashMap,结果在高频场景下,HashMap的rehash操作导致偶尔的延迟尖峰。后来换成预分配的数组+直接索引,延迟从微秒级降到了纳秒级。
说白了,高频场景下,任何「动态」的东西都是敌人。能静态就静态,能预分配就预分配。
4.6 小结
实时聚合计算,核心就三件事:
- OHLC:维护四个变量,窗口切换时输出
- Volume Profile:维护价格-成交量直方图,增量更新
- VWAP:维护两个累加器,用环形缓冲区剔除过期数据
这三者并行运行,互不依赖。每个Tick进来,O(1)复杂度完成更新。嗯,这就是我心中的「实时聚合计算」标准答案。