净额结算是清算体系里回报最高的一项设计:全天全市场可能有几百万笔成交、几千亿成交金额,经过轧差之后真正需要交割的资金和证券只有成交总额的百分之几。上一篇《清算 vs 结算》用例子展示过压缩效果,这篇把背后的计算拆开——轧差在数学上是什么,在工程上难在哪。
数学形式:折叠求和
把全天的成交集合记为 $T$,每笔成交 $t$ 带有四个关键属性:买入方 $b(t)$、卖出方 $s(t)$、标的证券 $i(t)$、金额 $a(t)$(证券方向同理,记数量 $q(t)$)。多边净额结算下,所有参与者只对中央对手方(CCP)交割,因此轧差不需要两两配对,只需要按参与人 × 标的分组求和:
$$ \text{净额}(p, i) = \sum_{t:\, b(t)=p,\, i(t)=i} a(t) \;-\; \sum_{t:\, s(t)=p,\, i(t)=i} a(t) $$
结果大于零是应收,小于零是应付。这个式子就是轧差算法的全部数学内容——一个带过滤条件的分组聚合,SQL 三行就能写完。真正的复杂性百分之百在分组键的构造上。
分组键:规则编译的产物
实际系统里的分组键不是裸的"参与人 × 标的",要叠加一串边界规则,每一条都对应一条业务约束:
- 日期:只有同日成交参与轧差(T+1 体系下这是天然的;但跨日回购、T+0 品种要显式排除);
- 结算路径:自营席位和客户席位的成交不能轧在一起——资金路径不同(自营走法人自有资金,客户走客户资金池),轧在一起会让两类资金池的边界变模糊,违反三方存管的隔离要求;
- 币种:沪港通的人民币和港币负债分开轧,兑换发生在额度层面而不是轧差层面;
- 品种的特殊规则:质押式回购的首次交收按逐笔全额处理不参与净额(法律要求券足额质押),到期交收才进净额——同一个合同两次交收走了两条不同的轧差路径。
算法工程师的工作就是把所有这类规则编译进分组键。判断编译得好不好的标准只有三个:可重算(给定同样的输入流水和同样的规则版本,任何时候重跑结果逐分一致)、可解释(每一笔记账都能回答"为什么进这个组、为什么是这个符号")、可审计(规则版本和输入快照要落档,三年后监管检查能复现当时的计算)。反过来,凡是用"特殊情况 if-else 补丁"堆出来的轧差实现,三个标准全过不了。
守恒性:轧差结果的自检
轧差输出必须满足两条全局不变量,系统应该在落账前强制校验:
$$ \sum_{p} \text{净额}(p, i) = 0 \quad \forall i \qquad\qquad \sum_{p} \sum_{i} \text{净额}(p, i) = 0 $$
第一条是分券种守恒:每一只证券的多头净额和空头净额必然抵消,因为每一笔成交都有一买一卖;第二条是总额守恒。两条都源于"成交不产生也不消灭资产,只转移所有权"这个公理。校验不通过说明聚合逻辑有 bug(比如某种币种走了单独通道漏加回总量),这种 bug 必须在进入结算前拦住——轧差错了再往下走,错的就是真钱。
还有一个更细的检查:分券种守恒之外,还要核对券种内部的资金方向。一只股票当天净买入应该对应资金净应付,两者符号关系错了往往意味着资金科目和证券科目用了不同的过滤条件,这种错位藏得很深,靠的就是这对对称性检查。
为什么不用数据库直接 GROUP BY
初看轧差就是一个 GROUP BY participant, instrument,为什么不直接在交易库上跑?生产里的做法是流水的副本 + 独立的计算域:从交易库导出当日成交快照(带源流水号),导入清算库再做聚合。理由和清算结算分离一脉相承:清算计算可以反复重跑(反正只是算),绝不能让它和在线交易争资源、抢锁;更关键的是快照机制给了"可重算"一个支点——输入冻结了,重跑才有意义。轧差算法本身三行 SQL,但要让它在每晚批量里值得信任,周边这套隔离和校验设施才是大头。