技术博客

交收执行:资金划付与证券过户——DVP 三原则在代码里的体现

第 48 / 141 篇 · 2025-05-16 · secfin · 结算交收DVP

前面的文章把账算完了、对平了,闸门放行,轮到交收执行——把应收应付净额变成真实的资金划付和证券过户。这一步和前面所有环节的本质区别是:不可重跑。清算算错了可以重算,对账不平时可以挂账,但划付指令一旦发给银行、过户指令一旦发给登记公司,撤回就不是系统内的事了,要走上游撤销、人工协调甚至违约处置流程。所以交收系统的全部设计重心都压在一件事上:怎么让"不可重跑"的操作尽可能"可恢复"。

两条通道,两套语义

资金通道走银证转账体系:券商的指令通过三方存管接口发给银行,银行在"客户资金汇总账户"和"券商结算备付金账户"之间实际划转。指令粒度是法人层面的净额——银行不关心客户明细,只管两个对公账户之间的金额对不对。对客户资金的变更发生在券商内部账上(二级清算已经算好每户多少,客户看到的"资金到账"是内部记账,不是银行划转)。

证券通道走变更登记:中国结算根据券商层面的净应收应付,批量变更托管单元下的证券登记总量。登记变更是典型的批量语义——不是实时逐笔,而是日终一次或若干次批量提交,登记系统返回处理结果文件,券商再据此确认过户成功与否。

两条通道的速度、可靠性、失败模式完全不同:资金划付是准实时接口调用,失败立即返回错误码;证券过户是批量文件交互,提交后要等处理结果文件,中间隔着小时级的不确定性。DVP 的"同时交割"在这个现实下只能是逻辑语义而不是物理语义——这正是需要 CCP 在两边之间做信用转换的原因(见《保证交收与非保证交收》)。

DVP 原则的三个代码落点

DVP 要求券和钱的交割互为条件,落到交收引擎的实现上是三条具体规则:

  1. 双向冻结:交收执行前,应收方的应收资产和应付方的应付资产同时进入"交收锁定"状态——这钱这券在交收完成前谁也动不了,防止一边划走了另一边落空;
  2. 同生同死:资金划付结果和证券过户结果要在同一个交收确认流程里汇合,两边都成功才提交,任一边失败整笔回滚到冻结状态,进入待处理队列而非半提交状态。回滚不是撤销——可能钱已划出去而券没过户——回滚的意思是账上回到"未交收",然后由差错流程去追那笔已划出的资金;
  3. 不可部分可见:客户侧的"已交收"状态只能在交收确认流程完整提交后一次性出现,不允许客户看到"股票到了钱没扣"的中间态。这条规则排除了一个诱人的优化——先放证券到账再补资金,它把内部的处理时序泄漏成了客户可见的不一致。

幂等与断点续作:半批是常态不是异常

"不可重跑"和"系统会在半夜崩"叠加在一起,得出交收引擎的两条硬约束。

幂等:所有外发指令必须带全局唯一的业务流水号,处理方(银行、登记公司)对重复流水号直接返回原处理结果而不是重复执行。引擎侧的实现是"指令状态机"——每条指令有 已生成/已发送/已确认/失败/已撤销 等状态,重试前先查状态,已确认的直接跳过。没有这个状态机,半夜三点重启后你分不清哪 50 笔划出去了哪 50 笔没有。

断点续作:交收按批次组织,一批可能几千条指令。批执行必须支持从失败点继续:每条指令的确认结果实时落库,重启后扫描"已发送未确认"的指令发起查询而不是重发。查询和重发的区别生死攸关——对银行接口来说,"查一下刚才那笔到哪了"永远安全,"再划一次"在对方其实成功只是回执丢了的情况下就是重复划款。

这两条约束合起来给交收批次定义了一种健康的中间状态:半批是设计内的常态。一批 5000 条指令跑了 4300 条系统挂了,恢复后是"4300 确认 + 700 待续"的明确状态,而不是"不知道跑到哪了"的混沌。能把半批状态定义清楚、展示给值班人员、并支持一键续作的交收系统,才算通过了生产检验。

交收执行:两条通道,双向冻结,同生同死交收引擎冻结 → 发令 → 汇合确认 → 提交资金通道(银证转账)证券通道(变更登记)准实时接口 · 净额划付批量文件 · 总量过户同生同死幂等:指令带全局流水号,重试先查状态机,已确认则跳过半批是常态:4300 确认 + 700 待续,可展示、可一键续作