技术博客

事前风控检查链路:资金校验、持仓校验、黑名单的毫秒级预算

第 27 / 141 篇 · 2026-05-10 · secfin · 事前风控交易风控性能

委托在柜台里的旅程:接入 → 风控检查 → 报交易所。中间这一段事前风控是交易时延的固定成本,也是资损防线——检查漏一项,可能就是客户透支或违规交易。它的设计约束很拧巴:既要全(监管和风控要求的检查项只增不减),又要快(检查耗时直接加在每一笔委托的延迟上,量化客户为此付费)。这篇讲检查链路本身,以及"全"和"快"怎么同时成立。

检查项全景:按否决成本排序

事前检查的项目大致五类,每类挂在链路的不同位置:

检查 内容 典型成本
静态权限 账户是否正常、有无该市场/品种交易权限、品种是否停牌 微秒级,查配置
合规规则 价格笼子、涨跌停价、最小变动价位、整数手 微秒级,纯计算
资金/持仓 买入冻结资金够不够、卖出冻结持仓够不够、保证金比例(两融) 十微秒~百微秒,查余额
名单类 黑名单(操纵市场处罚名单)、限制账户、实控人关联检查 微秒级,查集合
限额类 客户级/产品级单日委托金额上限、自成交限额 微秒级,原子计数

排布顺序有个朴素原则:便宜的在前,贵的在后。静态权限和合规规则是纯计算或读热数据,放最前,早拒早解脱——委托在第一条就不合格时,后面的检查白做还白占锁。资金持仓校验最贵(后面细说),放在中间;名单和限额最慢也要做,放最后兜底。

为什么资金校验是最贵的一道

静态检查只读配置,资金持仓检查要读可变状态,而且这状态正在被全系统的并发委托改着。最直觉的实现:校验时 SELECT balance FROM ... FOR UPDATE 加行锁检查可用余额——慢且把并发度锁死了。生产实现都是内存缓存 + 预留记账:

def check_buy(self, acct, amount):
    avail = self.cache.available(acct)      # 内存读,纳秒级
    if avail < amount:
        return REJECT
    self.cache.reserve(acct, amount)        # 原子扣减可用,转入冻结

委托报出前先原子扣减内存里的可用余额,成交后按实际金额多退少补,废单/撤单全额返还。关键点:内存余额是权威数据的镜像 + 本柜台的预记账,权威余额仍在数据库,日终对账抹平差异。风控检查允许的瞬时"超卖"窗口(内存已扣、数据库未落)由"预留"语义保证——扣减发生在委托受理时,不是成交时,透支只会发生在极端并发下两个通道同时受理——所以跨通道(双柜台)场景下,内存账本要么全局唯一(一个风控实例),要么按账户分片路由到同一实例,这是事前风控系统并发设计的头号约束。

预算超了怎么办:分级降级

单条检查链路的全量预算通常是几百微秒到 1 毫秒。预算超支时的武器不是优化到底,而是分级:

  1. 同步必检项:合规规则(价格笼子、涨跌停)和资金持仓,毫秒预算内的硬检查,不过直接拒单。合规项一项都不能异步化——这是监管红线;
  2. 准同步检查:名单类的部分维度(如大额可疑名单),同步查本地镜像,本地镜像由异步任务分钟级刷新。接受"名单更新延迟一分钟"的风险换取微秒级响应;
  3. 异步复核:限额类的统计维度(如客户单日累计买入金额的趋势预警),委托放行后异步算,异常时事后处置(限制后续委托 + 人工介入)。

分级的本质是把"检查"拆成"否决"和"记录"两个动作:能毫秒级内确定性回答的才配否决,其余一律先放行后追。这个原则和清算篇"先落日志再处理"一脉相承——系统的每个决策点都要想清楚"我此刻依据的信息有多新"。

事前风控链路:三类检查的分级排布静态权限账户/品种/停牌~1μs合规规则笼子/涨跌停/手数~1μs资金/持仓内存预留扣减~50μs名单本地镜像分钟级刷新~1μs限额异步复核放行后计算事后任一硬检查不过 → 拒单,已做的内存预留按序回滚(检查链要有补偿语义)排序原则:便宜在前早拒早解脱;资金校验的原子性是链路正确性的根基红线:合规规则(笼子/涨跌停)永不异步化——宁可拒错,不可漏放限额超支的处置不是拒单而是限后续:先放行已受理的,再冻结新增委托入口总预算通常 <1ms,每道检查耗时都要可观测:没有度量的优化是赌博

链路本身的工程细节

补偿语义。 检查链是责任链模式,但比教科书多一层:第 N 道检查失败时,前 N-1 道已经做了的副作用(内存预留、计数器递增)要按逆序回滚。漏回滚的经典 bug:资金扣了但静态权限检查失败,预留没释放——客户可用余额越用越少,直到日终批量才对平。把"检查"和"副作用"分开登记,失败时统一走回滚清单。

检查耗时可观测。 每道检查的耗时打点(histogram),线上任何一道检查 P99 上涨都能立刻定位。事前风控是延迟预算的消耗大头,没有分项度量,"委托变慢了"这种投诉永远查不到根因。

规则热更新。 涨跌停价每日更新、价格笼子参数按交易所通知调整,名单分钟级刷新——链路里的所有规则数据都必须支持无重启热加载,且加载用双缓冲(新版本就绪后一次性切换),避免检查读到一半规则换了。风控参数的热更新篇会展开这套下发机制。

最后说一个容易忽视的校验精度问题:资金比较不能用浮点。价格×数量在二进制浮点下会产生 0.1+0.2 式误差,"可用 10000.00 元买 10000.00 元的单"可能被拒。余额和金额全程用定点数(分或厘为单位的整数)或 decimal 类型,比较逻辑才有确定性——这个坑在浮点世界里不大,但在"每分钱都要找到出处"的证券账务里,踩一次就是一次资损事件。