3. 行情数据特征

在行情系统中,数据并不是单一形态。同一只标的、同一秒内,可能同时存在逐笔成交、盘口变更、快照切片和K线聚合结果。它们的数据结构、更新频率、业务语义和存储策略差异极大。如果建模时把它们混为一谈,后续的实时聚合、回测查询和存储成本都会失控。本节从数据结构与业务语义两个维度,逐一拆解五类核心行情数据。

一、五类行情数据的本质差异

行情数据的分类,本质上取决于两个问题:它记录的是事件还是状态?它的时间粒度是逐笔还是切片?逐笔成交和Tick是事件流,盘口是状态快照,快照行情是周期性切片,K线则是聚合结果。理解这一点,才能为每类数据选择正确的表引擎和排序键。

行情数据五类形态:事件流 vs 状态切片 逐笔成交 (Trade) 事件流 · 不可变 价格/数量/方向/序号 驱动成交额与VWAP 频率:毫秒级不定 Tick 行情 事件流 · 最小粒度 含成交+盘口变更 时间戳精度最高 频率:毫秒/微秒级 盘口 (OrderBook) 状态快照 · 可覆盖 买卖N档价量 反映瞬时供需 频率:快照周期 快照行情 (Snapshot) 周期切片 · 定频 最新价+盘口+统计 便于回放与对齐 频率:3s/1s/500ms K线 (Bar) 聚合结果 · 可重算 OHLCV+时间窗口 由Tick/快照聚合 频率:1m/5m/1d

二、逐笔成交与Tick:事件流的数据结构

逐笔成交是最细粒度的成交记录,每笔成交对应一次撮合。其核心字段包括:成交时间、成交价、成交量、买卖方向(主动买/主动卖)、成交序号。Tick则更宽泛,通常指行情源推送的最小更新单元,可能同时携带成交和盘口变化。

-- 逐笔成交典型结构(ClickHouse 建表片段)
CREATE TABLE market.trade_tick (
    symbol      LowCardinality(String),
    trade_time  DateTime64(3, 'Asia/Shanghai'),
    price       Decimal64(4),
    volume      UInt64,
    direction   Enum8('buy'=1, 'sell'=-1, 'unknown'=0),
    seq         UInt64,
    exchange    LowCardinality(String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(trade_time)
ORDER BY (symbol, trade_time, seq);

业务语义上,逐笔成交是不可变事件,只能追加、不能修改。它是计算成交额、VWAP、主动买卖比、大单监控的原始依据。排序键必须包含时间与序号,保证同一毫秒内多笔成交的顺序可还原。

三、快照与盘口:状态切片的结构

快照行情是行情源按固定周期(如3秒)推送的切片,包含最新价、开盘价、最高价、最低价、成交量、成交额以及买卖盘口。盘口则是快照中的核心部分,记录买一至买N、卖一至卖N的价格与挂单量。

数据形态时间语义可变性典型字段主要用途
逐笔成交事件时间不可变价、量、方向、序号成交分析、VWAP
Tick事件时间不可变成交+盘口变更高频回测、微观结构
盘口快照时间可覆盖买卖N档价量流动性、价差分析
快照行情切片时间可覆盖OHLC+盘口+统计实时展示、回放
K线窗口时间可重算OHLCV策略信号、图表
盘口数据在存储时,建议将买盘和卖盘分别展开为数组列(Array(Decimal64)),或使用 Nested 结构。ClickHouse 对 Array 的聚合支持良好,但要注意盘口深度N不宜过大,否则单行宽度膨胀明显。

四、K线:聚合结果与业务语义

K线不是原始数据,而是由Tick或快照按时间窗口聚合得到的OHLCV结果。它的业务语义是可重算的派生数据:只要原始Tick完整,任意周期的K线都可以重新生成。因此K线表通常使用 AggregatingMergeTree 或 SummingMergeTree,配合物化视图实现增量聚合。

-- 1分钟K线聚合(物化视图思路)
CREATE MATERIALIZED VIEW market.bar_1m_mv
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(bar_time)
ORDER BY (symbol, bar_time)
AS SELECT
    symbol,
    toStartOfMinute(trade_time) AS bar_time,
    argMinState(price, trade_time) AS open,
    maxState(price) AS high,
    minState(price) AS low,
    argMaxState(price, trade_time) AS close,
    sumState(volume) AS volume
FROM market.trade_tick
GROUP BY symbol, bar_time;

注意K线的时间窗口语义:是左闭右开还是左闭右闭,直接影响边界成交归属。实盘中必须与行情源定义一致,否则会出现开盘价错位。

五、常见误区与复盘

  • 误区一:用快照代替Tick做回测。快照是切片,会丢失切片间的价格路径,导致回测结果偏乐观。高频策略必须使用逐笔或Tick。
  • 误区二:盘口与成交混存一张表。两者更新频率和可变性不同,混存会导致排序键失效、查询放大。应分表存储,通过时间对齐关联。
  • 误区三:K线直接落原始表。K线是派生数据,应通过物化视图或定时任务生成,避免手工写入造成口径不一致。
  • 误区四:忽略交易所时间与本地时间差异。行情时间戳必须保留交易所原始时间,并单独记录入库时间,便于排查延迟与乱序。
在ClickHouse中,逐笔成交表建议使用 MergeTree 并按 symbol + 时间 + 序号排序;快照表可使用 ReplacingMergeTree 按 symbol + 快照时间去重;K线表使用 AggregatingMergeTree。三类表的引擎选择错误,是行情系统性能问题的最常见根源。

总结来看,行情建模的第一步不是写SQL,而是先回答:这条数据是事件还是状态?是原始还是派生?是定频还是不定频?把五类数据的结构与语义对齐后,表引擎、排序键、分区策略和聚合路径都会自然清晰。