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,互不干扰。

实时聚合计算核心流程 Tick数据流 OHLC计算器 Open/High/Low/Close Volume Profile 价格-成交量直方图 VWAP滚动计算 累计金额/累计成交量 OHLC Bar VPOC / 分布图 滚动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)复杂度完成更新。嗯,这就是我心中的「实时聚合计算」标准答案。


专注资料整理