Trade Lifecycle Events
ODTS 27 — 交易生命周期事件:Novation、Amendment、Termination、Unwind、Compression
PM/BA 日常工作中接触最多的是”普通”交易流程(询价→成交→存续→到期)。但 OTC 衍生品交易在存续期内可能发生多种生命周期事件,每种事件都对应特定的系统功能和运营流程。不理解这些事件,你在设计交易管理系统时就会漏掉大量需求。
一、Novation(更换交易对手)
什么是 Novation
Novation(交易对手变更) 是指一笔存续期交易的一方将其权利和义务全部转让给第三方,原对手方退出,新对手方加入。
典型场景:私募基金 A 有 5000 万名义本金的雪球,持有 6 个月后想提前变现,找到券商 B——不是直接平仓,而是券商 B 接手它作为交易对手方的位置。
Novation 的流程
- 寻找受让方:原交易方找到一个愿意接手的第三方(通常是通过前台或经纪商)
- 三方确认:原交易方、受让方、剩余方(counterparty remaining)三方确认 novation 条款
- ISDA Novation 确认书:签署 ISDA Novation Confirmation,明确转让的交易和生效日
- 主协议接入:受让方需与剩余方已有有效主协议,或新签
- 系统更新:交易系统中将原对手方替换为受让方,并保留 novation 历史记录
对系统的要求
| 需求 | 说明 |
|---|---|
| 历史保留 | 交易不能”改”成新对手方,必须保留原交易记录 + novation 事件记录 |
| 估值延续 | 转让价格 = 当前盯市估值 + 可能的转让费。系统需记录转让价格 |
| 资金流更改 | 后续利息、票息的支付路径改为新对手方 |
| 审批流程 | Novation 通常需要风控和法务审批,系统需嵌入工作流 |
| CSA 条款切换 | 新对手方可能有不同的 CSA 条款,保证金计算参数需随之切换 |
系统实现的常见 bug
- 系统把 novation 实现为”修改原交易对手方 ID”,导致历史报表中这笔交易出现在错误的对手方名下
- 存续期估值没有从 novation 生效日重新计算,导致交割日上的所有现金流仍发往原对手方
- 没有考虑新对手方需要重新做 KYC/AML 检查,导致合规风险
PM 检查清单: 新交易的对手方修改和 novation 在系统中必须是两个不同的功能。前者改交易要素,后者创建一条独立的事件记录。
二、Assignment(收益权转让)
与 Novation 的区别
| 维度 | Novation | Assignment |
|---|---|---|
| 转让内容 | 全部权利义务 | 仅收益权(收取现金流的权利) |
| 对手方关系 | 原方退出,新方加入 | 原方仍在,新方只收钱 |
| 需要对手方同意 | 需要三方同意 | 根据协议,可能不需要对手方同意 |
| ISDA 框架 | ISDA Novation Confirmation | ISDA Assignment Agreement |
| 系统复杂度 | 中等 | 较高(因为原对手方还在) |
典型场景
银行需要将一笔贷款的信用风险转移出去但不破坏客户关系,通过信用衍生品(CDS)的 assignment 将风险转移给第三方,但保留与客户的直接关系。
对系统的要求
Assignment 比 novation 更复杂,因为:
- 系统需要维持原交易记录不变,同时标注:某部分现金流已 assign 给第三方
- 支付路由分叉:部分现金流给受让方,部分给原方
- 需要支持”比例 assignment”(转让 50% 的收益权)
三、Amendment(交易要素变更)
Amendment 是 OTC 交易生命周期中最常见的事件——比 novation 和 termination 加起来都多。
常见的 Amendment 类型
| 变更类型 | 示例 | 系统处理方式 |
|---|---|---|
| 费率变更 (Rate Change) | TRS 融资利率从 4.5% 改为 4.0% | 修改利息计算参数,自生效日执行 |
| 期限变更 (Tenor Change) | 交易展期 3 个月 | 修改到期日,重新计算摊还计划 |
| 标的变更 (Underlying Change) | 替换 TRS 的标的股票 | 关闭原标的头寸,开新标的头寸 |
| 行权价变更 (Strike Change) | 期权行权价调整 | 涉及期权重新定价,可能产生额外费用 |
| 名义本金变更 (Notional Change) | 部分提前还款 | 同 Partial Termination |
| 客户信息变更 (Client Info) | 客户公司改名 | 修改客户 ID 映射 |
Amendment 在系统中的实现
ODTS 中,Amendment 是通过 ContractManager 的修改接口实现的,而不是通过直接修改数据库记录。每个修改操作生成一个事件记录 (ContractEventBean):
Amendment 流程:
1. 运营在系统界面发起 Amendment 请求
2. ContractEventBean 记录变更类型和变更内容
3. 审批工作流(取决于变更类型和金额)
- 简单变更(如联系方式)→ 自动通过
- 重大变更(如行权价)→ 风控审批
4. 审批通过 → ContractManager 执行变更
5. 变更后 → 重新计算 EOD 估值和保证金
6. SAC 报关校验 → 监管报送标记为"修改"
Amendment 的最常见 bug
利率变更后,历史 P&L 计算错误——系统在重新计算历史估值时用了新利率,而不是新旧利率的分离处理。
正确做法:Amendment 生效日是分水岭,生效日前的估值用旧参数,之后用新参数。
四、Partial Termination(部分提前终止 / Partial Unwind)
概念
Partial Termination 是指一笔交易的部分名义本金提前终止,剩余部分继续存续。
典型场景:一个亿名义本金的收益互换,客户提前还了 3000 万,剩余 7000 万继续。
系统处理
原始交易:名义本金 1亿,2026年1月到期
↓
部分终止:终止本金 3000万
↓
结算支付终止部分的 MTM 差额
↓
原交易继续:名义本金变为 7000万,到期日不变,其他条款不变
↓
系统操作:修改原交易的名义本金并记录部分终止事件
ODTS 中的 Unwind 处理器
ODTS 为 Unwind 事件设计了专门的处理器架构,位于 eds-utility/src/.../domain/linear/processor/:
Unwind 处理器(按产品和类型):
PbTrsUnwindProcessor.java — PB TRS 全额 unwind
PbTrsPartiallyUnwindProcessor.java — PB TRS 部分 unwind
PbTrsUnwindAdjCfProcessor.java — PB TRS unwind 含现金流调整
PbTrsPartiallyUnwindAdjCfProcessor.java — PB TRS 部分 unwind 含现金流调整
CatsUnwindProcessor.java — CATS 全额 unwind
CatsPartiallyUnwindProcessor.java — CATS 部分 unwind
LmeCatsUnwindProcessor.java — LME CATS 全额 unwind
LmeCatsPartiallyUnwindProcessor.java — LME CATS 部分 unwind
CalCCYAnnualFeeCatsUnwindProcessor.java — 年费 CATS unwind
CalCCYAnnualFeeCatsPartiallyUnwindProcessor.java — 年费 CATS 部分 unwind
每一种处理器实现特定产品类型的 unwind 逻辑,包括:
- 现金流计算:终止部分的 MTM 和费用
- 名义本金减少:更新合约的名义本金字段
- 历史保留:记录 unwind 事件,不删除原交易
- DTT 报送:生成对应的 DTT unwind 请求
关键系统细节
- 支付计算:终止部分的现金流如何计算?通常 = 终止部分的名义本金 × 当前 MTM 单价。系统需要支持”按比例计算终止支付”
- 会计处理:终止损益需记入当期 P&L,剩余部分继续按原会计处理。系统需要区分”交易结束产生的损益”和”存续期估值变动”
- 重新计算:部分终止后,剩余部分的保证金计算参数不变,系统需使用新的名义本金重新计算
五、Full Termination / Early Termination(提前终止 / Unwind)
触发原因
- 双方协商一致:都同意终止(最常见)
- 违约事件 (Default):一方违约,另一方行使终止权
- 终止事件 (Termination Event):非法性、税收事件、合并事件等
- 客户要求:客户支付提前终止费后退出
ODTS 的 Unwind 实现
系统的 Unwind 流程由 ContractUnwindQueueCallable 驱动,这是一个异步队列处理器,支持批量 unwind:
Unwind 流程(代码级别):
1. ContractUnwindQueueCallable 接收 unwind 请求
2. 根据交易类型选择对应的 Processor(PbTrsUnwindProcessor / CatsUnwindProcessor 等)
3. Processor 计算:
a. 当前 MTM(调用定价引擎获取)
b. 终止费用(根据合同条款)
c. 应付/应收金额
4. 生成 UnwindTradeDetail 记录 (UnwindTradeDetail.java)
5. 更新合约状态为 UNWIND
6. 更新 EOD 表和保证金计算
7. 生成 DTT 报送消息
8. 通知运营(Unwind 完成)
DTT 报送(中证报价)
Unwind 事件需要向中证报价 (DTT) 报送。系统有独立的 DTT unwind 服务:
DTTUnwindServiceImpl.java — DTT 全额 unwind 报送
DTTUnwindLpServiceImpl.java — DTT 部分 unwind 报送
报送数据模型:
UnwindRequest.java — 全额 unwind 请求
PartiallyUnwindRequest.java — 部分 unwind 请求
BaseUnwindRequest.java — 基础 unwind 请求
系统实现的陷阱
最常犯的错误: 系统把提前终止和正常到期视为同一件事。区别在于:
| 维度 | 正常到期 | 提前终止 |
|---|---|---|
| 终止金额 | 最终现金流(如本金回收) | MTM + 终止费 |
| 审批流程 | 自动 | 需风控/法务审批 |
| 交易状态 | 到期 | 提前终止/Unwind |
| 会计处理 | 到期结算 | 终止损益 |
| 风险系统 | 自动移除 | 需做终止确认后才能移除 |
| DTT 报送 | 不需要 | 需要单独报送 |
如果系统不做区分,监控报表上”提前终止量”的数据就会为零,管理者无法了解多大比例的交易是提前结束的——这会影响业务决策(比如:客户流失率是不是太高了?)。
Unwind 的并发问题
ODTS 的 Unwind 是通过异步队列 (ContractUnwindQueueCallable) 处理的。当多个 unwind 请求同时到达同一笔交易时,可能产生竞态条件:
场景:
09:00:运营 A 发起 TRS001 全额 unwind
09:01:运营 B 发起 TRS001 部分 unwind (unwind 3000万)
队列处理顺序不确定:
如果 A 先处理 → TRS001 已全额 unwind → B 的请求失败
如果 B 先处理 → TRS001 名义本金减少 → A 的请求仍然有效
问题:没有锁机制防止对同一笔交易的同时操作
这种问题在实际中靠运营流程避免(同一笔交易不会同时处理两次 unwind),但系统没有强制保护。
六、Trade Compression(交易压缩)
概念
Trade Compression 是指一组交易通过多方净额结算减少名义本金和交易笔数而不改变各方的风险敞口。
典型场景:两家银行之间有几百笔利率互换,净敞口其实很小。通过 compression 可以把几百笔交易压缩成几笔甚至一笔,降低操作成本和资本占用。
Compression 的商业模式
- 由第三方服务商(如 TriOptima、LCH)组织
- 参与方提交交易组合
- 算法计算最优压缩方案
- 各参与方进行同步 novation(同时替换一批交易)
- 每次压缩周期涉及数百笔交易同时变更
对系统的要求
- 批量交易修改:系统需支持同时修改/终止数百笔交易,且保证数据一致性
- 事件匹配:压缩产生的新交易需与原有交易保持正确的映射关系
- 会计处理:压缩可能产生会计损益(即使净敞口不变,总头寸变化了)
- 监管报告:压缩交易需在监管报告中正确标示
ODTS 没有专门的原生 compression 支持——压缩通过批量 novation/unwind 的组合方式实现。这意味着每次 compression 周期需要大量的开发和运营支持。
七、ContractEventBean — 事件记录基础设施
系统通过 ContractEventBean 来记录所有生命周期事件:
eds-web-app/src/.../action/dynamic/model/ContractEventBean.java
关键字段:
contractId — 合约 ID
eventType — 事件类型(NOVATION/AMENDMENT/UNWIND/TERMINATION)
eventDate — 事件日期
oldValue — 变更前值(如果是 amendment)
newValue — 变更后值
approvalStatus — 审批状态(PENDING/APPROVED/REJECTED)
operatorId — 操作人
remark — 备注
相关 Action:
QueryContractEventAction.java — 查询合约事件
SacListEventReportAction.java — 事件监管报送
EquityEventInfoBean 和 EquityEventInfoComparisonAction 处理股权相关事件的信息比对。
八、系统设计原则
针对以上所有生命周期事件,系统设计应遵循这些原则:
- 事件驱动,记录不可变:每个生命周期事件都是一条独立的记录,原始交易不可修改。系统通过事件回放可以重现任意时间点的交易状态
- 事件类型可扩展:新增事件类型不应要求修改核心交易模型
- 审批工作流可配置:不同的事件类型可以关联不同的审批链
- 会计处理灵活:不同事件类型触发不同的会计科目
- 监管报告适配:生命周期事件需要在监管报告中正确分类和标示
- 并发保护:同一笔交易不能同时被多个生命周期事件处理(ODTS 的异步队列没有锁机制,靠运营流程保证)
业务代价:生命周期事件出错的真实成本
Novation 改错了对手方——100 万保证金发错了人
Novation 在系统中的实现有两种方式:要么用”事件记录”(正确),要么用”修改原交易字段”(错误)。ODTS 早期版本选择了后者。
2019 年:
一笔利率互换(IRS,名义本金 2 亿)做 novation——交易员 A 把仓位转让给交易员 B。
系统逻辑:修改原交易的 counterpartyId 从 A 到 B。
问题:CSA(信用支持附件)是绑定 counterparty 的。
原交易的 CSA 阈值是 500 万(A 的阈值)。
新对手方 B 的 CSA 阈值是 0(全额追保)。
系统改了 counterpartyId,但没有重新计算保证金参数。
→ 接下来一个月的保证金计算仍然使用 500 万阈值
→ 该收的保证金没收到(因为阈值太高了)
发现时:
→ 1 个月后运营对账发现:这笔交易的保证金余额"不对"
→ 追查发现 CSA 参数没更新
→ 此时已经少收了约 100 万保证金
→ 虽然最终可以从 B 追回,但过程涉及多次沟通和人工调整
业务成本:
→ 运营花了 3 个工作日人工重新计算 1 个月的保证金
→ B 方对"为什么你们的保证金计算总是改不对"提出了投诉
→ 最终系统修复:novation 必须用"事件记录"而非"修改原交易"
Partial Unwind 的并发问题——同一笔合约被 unwind 了两次
ODTS 的 ContractUnwindQueueCallable 没有锁机制。如果运营并发提交了两个 partial unwind 请求(比如运营 A 提交了 30% unwind,运营 B 提交了 50% unwind),系统不会阻止它们同时处理同一笔合约。
事件链:
合约 X(名义本金 1000 万)
→ Unwind 请求 1:unwind 30%(名义本金应变为 700 万)
→ Unwind 请求 2:unwind 50%(名义本金应变为 500 万)
→ 如果两个请求同时处理:
→ 请求 1 读到了原始本金 1000 万,算完写到 700 万
→ 请求 2 也读到了原始本金 1000 万(还没被请求 1 写回去),算完写到 500 万
→ 最终名义本金 = 500 万,但实际应该是 700 万
→ 相当于多 unwind 了 200 万
业务影响:
→ 这笔合约的后续利息计算都基于错误的名义本金
→ 发现时已经过了 2 个月(运营对账才发现名义本金少了)
→ 修复:人工调整 + 与对手方沟通确认
→ 这对于 PM/BA 来说意味着:你在设计交易系统时,
必须考虑"两个运营人员同时操作同一笔合约"的场景
运营流程的补救:
团队后来加了一条流程规则——"同一笔合约的 unwind 必须分次提交,
确认前一个完成后才能提交下一个"。
这是用流程补了系统的债。
Amendment 的审计黑洞——改了但没记录
Amendmend(合约要素修改)在系统中如果实现为”直接 UPDATE 数据库字段”,就会产生审计问题。ODTS 也不是所有 amendment 都走事件记录。
真实场景:
运营收到一笔修改请求——把某笔 TRS 的终止日期从 2024-06-30 改为 2024-09-30。
运营在系统里改了日期。
3 个月后:
→ 监管检查:需要提供这笔交易的"完整修改历史"
→ 系统只显示了最新的终止日期
→ 没有记录"这个日期是什么时候、被谁、为什么改的"
→ 运营只能手动翻邮件记录找到当时的修改请求
后果:
→ 监管问询:你们对合约要素的修改没有审计日志吗?
→ 这不是技术故障——是系统设计缺陷
→ 修复代价:在合约事件表中增加 amendment 记录逻辑
但存量交易的修改历史已经丢了
PM/BA 视角:
交易系统不是 CRM。一笔衍生品合约的每次修改,
都可能影响后续数年的现金流、保证金、风控计算。
没有审计的修改 = 无法回答"这笔交易为什么变成这样"。
Compression 没对上的坑
Compression(多笔交易压缩)涉及复杂的净额计算。ODTS 的压缩是一个批量处理——系统同时对多笔交易做修改。如果其中一笔交易的修改失败,整个 batch 可能部分完成、部分失败。
某次 compression 处理了 20 笔 IRS。
第 18 笔的数据库写入失败(主键冲突——因为另一笔交易的修改恰好用了同一个序列号)。
系统没有整体事务回滚——前 17 笔已经写进去了。
影响:
→ 这 17 笔的名义本金变成了"压缩后"的错误值
→ 但对应的事件记录没有同步更新
→ 运营花了一整天逐笔核对压缩前后的数据
→ 手动修复了 4 笔不一致的记录
压缩是一个典型的高风险操作:
一笔交易错了可能只影响一笔。
压缩错了可能影响几十笔——而且错误在批量处理中更难发现。
| 组件 | 路径 |
|---|---|
| Unwind 处理器总入口 | eds-web-app/src/.../web/doContract/ContractUnwindQueueCallable.java |
| Unwind 异步线程 | eds-web-app/src/.../web/doContract/ContractUnwindThread.java |
| TRS Unwind | eds-web-app/src/.../web/doContract/UnwindTrs.java |
| 风险 Unwind | eds-web-app/src/.../web/doContract/UnwindRiskyWithoutException.java |
| 合约管理器 | eds-web-app/src/.../web/doContract/ContractManager.java |
| PB TRS Unwind | eds-utility/src/.../processor/PbTrsUnwindProcessor.java |
| PB TRS 部分 Unwind | eds-utility/src/.../processor/PbTrsPartiallyUnwindProcessor.java |
| CATS Unwind | eds-utility/src/.../processor/CatsUnwindProcessor.java |
| LME CATS Unwind | eds-utility/src/.../processor/LmeCatsUnwindProcessor.java |
| DTT Unwind 服务 | eds-utility/src/.../fileGenertor/DTT/DTTUnwindServiceImpl.java |
| DTT 部分 Unwind | eds-utility/src/.../fileGenertor/DTT/DTTUnwindLpServiceImpl.java |
| Unwind 请求模型 | eds-utility/src/.../fileGenertor/bean/DTTBean/UnwindRequest.java |
| 部分 Unwind 请求 | eds-utility/src/.../fileGenertor/bean/DTTBean/PartiallyUnwindRequest.java |
| Unwind 交易明细 | eds-utility/src/.../fileGenertor/bean/UnwindTradeDetail.java |
| 合约事件模型 | eds-web-app/src/.../action/dynamic/model/ContractEventBean.java |
| 合约事件查询 | eds-web-app/src/.../action/dynamic/QueryContractEventAction.java |
| 股权事件比对 | eds-web-app/src/.../action/dynamic/EquityEventInfoComparisonAction.java |
| 合约自动开仓 | eds-web-app/src/.../web/doContract/ContractOpenQueueCallable.java |
一个交易管理系统成熟度的标尺,不是它能不能跑通”询价→成交→到期”这条主路,而是它能不能正确处理 novation、amendment、partial unwind 这些”非标”事件。ODTS 的 Unwind 处理器采用按产品类型拆分的设计(PbTrsUnwindProcessor / CatsUnwindProcessor),这是合理的事件驱动架构;但同样的并发保护缺失——
ContractUnwindQueueCallable没有锁机制——是运营流程补了系统的债。