K 线是行情系统里最"不像实时系统"的部分——它不追快,追准。同一分钟的开盘价,两个系统算出来不一样,复盘的人就会质疑你的数据。这篇讲周期聚合的规则和坑。
时间桶与对齐
K 线是时间桶上的聚合:把 $[t, t+\Delta)$ 内的所有成交聚合出一组 OHLCV。$\Delta$ 取 1 分钟、5 分钟、日、周。对齐规则的关键是左闭右开:10:00:00.000 到 10:00:59.999 的成交属于 10:00 那根 K 线,10:01:00.000 属于下一根。用成交时刻对桶长取整决定归属:
$$ b(t) = \left\lfloor \frac{t - t_0}{\Delta} \right\rfloor $$
$t_0$ 是交易日起点,不是自然日零点——9:30 开盘,第一根分钟线是 9:30 桶。这个细节错了,全天 K 线整体偏移 30 分钟,肉眼很难发现,是所有对齐 bug 里最难查的。
A 股有两个日历特殊性。一是午间休市:11:30 到 13:00 没有成交,13:00 的桶照常开始,分钟线天然断裂,不需要特殊处理。二是集合竞价:9:25 的开盘集合竞价全部成交记在 9:25 这一根分钟线上(9:25 至 9:30 之间无连续竞价成交),14:57 到 15:00 深市收盘集合竞价同理。规则是交易所行情文档定的,不是自己发明的。
OHLCV 的聚合语义
一根 K 线维护六个聚合量,逐笔成交到来时各更新一次:
$$ \begin{aligned} \text{open} &= \text{桶内第一笔成交价} \\ \text{close} &= \text{桶内最后一笔成交价} \\ \text{high} &= \max(\text{high}, p), \quad \text{low} = \min(\text{low}, p) \\ \text{vol} &\mathrel{+}= q, \quad \text{amount} \mathrel{+}= p \cdot q \end{aligned} $$
volume-weighted 均价直接除出来:$\text{VWAP} = \text{amount} / \text{vol}$,不用单独算。注意 amount 用定点整数累加,最后才除——浮点累加十万笔会漂。
分钟线再往上级聚合(5 分钟、30 分钟、日线)用同一套状态机套娃:5 分钟线的 open 是桶内第一根分钟线的 open,close 是最后一根分钟线的 close,high/low 取极值。所以只要分钟线对了,高级别线只是再聚合一次,不存在独立的计算口径。日线有个例外:A 股日线的高低价用全天连续竞价+收盘集合竞价的极值,和分钟线套娃结果一致;但开盘价取 9:25 集合竞价的结果,和 9:30 桶的 open 不同——做日线必须直接看日线口径,不能从 9:30 桶开始套。
数据源决定精度上限
K 线的质量上限由数据源锁死。用 Level-2 逐笔成交聚合是精确的:每一笔成交有成交价、量、时间,状态机直接喂。用 Level-1 三秒快照算分钟线则是降采样:3 秒间隔的收盘价样本拼不出真实的 high/low,中间的价格毛刺全部丢失,成交量也对不齐。
更隐蔽的是数据源切换:同一个证券,盘中切源(主源故障切备源)时两边的高低点位精度可能不同(一路 L1 一路 L2),分钟线的极值就断档。务实的做法是分钟线状态机按数据源标注,切源时该桶标记"不完整",下游图表画虚线。没人喜欢看到 K 线缺一块,但缺得诚实比补得瞎编好。
除权除息的跳变处理
除权日那天,价格会"凭空"跳变:10 送 10 的股票,昨收 20 元今开 10 元,K 线上是一根 -50% 的大阴线,但股东一分钱没亏。K 线系统必须在聚合前处理这个跳变,不然所有涨跌幅、均线、技术指标全部失真。
办法是复权:以某个历史时点为基准,把除权日之前的价格按调整因子缩放。前复权(以最新价为基准回溯调整历史价)是看图软件的默认,因为保证最新价是真实成交价;后复权(以上市日为基准向前调整)保住历史真实价,收益率计算要用它。调整因子:
$$ f = \frac{p_{\text{prev\_close}}^{\text{理论}}}{p_{\text{prev\_close}}^{\text{实际}}}, \qquad p^{\text{adj}} = p \times f $$
因子维护在一张公司行动事件表里,K 线引擎聚合时查表乘因子。这张表错了,K 线就错了——而且往往是很久之后做收益率回测时才暴露。
增量状态机的工程账
全市场 5000 只证券 × 240 根分钟线/天,如果每天收市重算一遍,数据量不大,问题在盘中要实时出当前分钟线——客户端每几秒就要刷新正在走的那根。所以状态机是增量的:每个证券每类周期维护一份当前桶状态,成交来了就地更新,桶边界到了就封存开新桶。封存后的历史线永不修改,只有"当前线"是活的。
并行化按证券分片,证券之间无依赖,天然可并行。坑在跨日边界:日切(15:00 后)要把所有证券的日线状态收尾,这对应《日切与交易日历》里讲的三套时间——用交易日而不是自然日做桶号,否则夜盘品种(虽然 A 股没有)和跨午夜的处理会全面混乱。