Trade Lifecycle
ODTS 05 — 交易生命周期:从询价到结算
为什么需要这篇文档
业务型实习生加入交易台后,知道 Java、Spring Boot、Vue,读过 01–04 了解了系统演进,但还是答不出最重要的问题:
客户想交易一个 XXX。发生了什么?哪段代码跑了?什么顺序?
这篇文档追踪一笔交易在 ODTS 中从端到端的路径。每一步说清楚:业务事件是什么、对应什么纸质单据 (paper)、哪个软件组件处理、关键类在哪里。
第一阶段:交易前(定价与报价)
1.1 客户询价
业务: 客户(对冲基金或私人银行)电话/邮件/IM 联系销售:“一个 6 个月中证 500 雪球,80/100 障碍,报价多少?”
销售 在前台定价工具里配参数——标的、期限、障碍、名义本金、币种、杠杆。这是试算 (what-if),还没有交易存在。
1.2 定价引擎计算报价
定价速度就是业务速度: 从销售收到询价到报价发回客户,时间窗口通常是 15-30 分钟。超过 30 分钟,客户可能已经去别家询价了。系统报价模块的响应时间决定了销售一天能做多少个询价——不是理论上的”能不能算出来”,是”能不能在客户挂电话之前算出来”。
系统: hedging-as(定价应用服务)
hedging-as/src/com/cicc/action/quotation/ — 报价入口
hedging-as/src/com/cicc/pricing/ — 定价引擎核心
hedging-as/src/com/cicc/pricing/model/ — 定价领域对象
hedging-as/src/com/cicc/pricing/calculation/ — 分析方法
关键类:
QuotationAction— 接收前端送来的参数,传给引擎PricingService— 编排计算:加载市场数据 → 构建收益函数 → 运行模拟PricingEngine— 分析引擎(蒙特卡洛 / PDE / 解析解)edslib/payoff/— 雪球、TRS、NDF 等产品的收益函数定义edslib/valuation/— 估值方法(贴现、Greeks)
内部流程:
QuotationAction反序列化前端请求为QuotationRequest,包含:产品类型、标的、名义本金、障碍(雪球)、利差(TRS)、定盘日历、币种、起始日、到期日- 请求路由到
PricingService.getPrice():- 从
dataCenter/加载即期价、股息率、利率、波动率曲面 - 调用
edslib.payoff.PayoffFactory.create(productType, params)构建收益函数 - 调用
edslib.valuation.MonteCarloValuation.evaluate()或ClosedFormValuation.evaluate()(取决于产品类型)
- 从
- 结果——中间价、买卖价差、Greeks(delta/gamma/vega/theta)——返回为
QuotationResult
修定价 bug 的入口:
hedging-as/src/com/cicc/pricing/service/PricingService.java→getPrice()。收益函数在edslib/payoff/,你的产品可能是SnowballPayoff.java或TrsPayoff.java。
1.3 销售谈判与确认
业务: 销售加上交易台的利润(例如 +50bp),发给客户。客户接受。
系统: 报价保存为 QuotationRecord(status=PENDING)。还没有具有法律约束力的合约。
第二阶段:交易簿记(交易录入)
2.1 交易员簿记交易
业务: 交易员将商定的条款录入系统。这叫簿记 (booking)——交易成为有约束力的合约的时刻。
系统: eds-web-app(核心 EDS 后端)通过 new-edsweb(Vue 2 SPA)
new-edsweb/src/views/option-contract/ — 合约簿记页面
eds-web-app/src/.../ContractController.java
hedging-as/src/.../contract/Contract.java — 交易领域模型
hedging-as/src/.../contract/ContractSaveService.java — 持久化
UI 展示的内容:
- 合约类型选择器: TRS / NDF / Snowball / Vanilla Option / Barrier Option / Swap
- 合约条款表单: 根据类型不同最多 80 个字段(标的、名义本金、行权价、障碍、敲入/敲出、融资利率、利差、定盘日、付款日、结算币种等)
- Leg 编辑器: 大多数 OTC 交易有 2 个以上的 legs。TRS 有融资 leg + 价格回报 leg。Swap 有两个固定/浮动 leg。每个 leg 单独定义。
- 对手方选择器: 从 CRM 拉取 (
customerInfo)
后端做的事 (ContractController / ContractAction):
簿记错误的业务代价: 80 个字段的表格,交易员在几分钟内填完。2019-2021 年间,平均每月有 2-3 笔交易因人工录入错误需要修正——比如名义本金多打一个零(从 1000 万变成 1 亿)、对手方选错、日期不一致。每次修正需要:交易员提交修改申请 → 中台审核 → 合规确认。平均每笔修正耗时 2-4 个工作日。更严重的情况:如果错误被确认书环节发现(交易已经发生但法律文件错了),修改需要对手方重新签署确认书——多 1-2 周。
- 校验:
ContractSaveService.validateTerms()检查产品类型与 leg 结构兼容、标的在参考数据中存在、对手方有 ISDA 资质且不在限制名单、日期一致、名义本金在授信额度内 - ID 生成: 系统生成
contractId(如TRS20250716001),前缀编码产品类型,中间是交易日,后缀是序号 - 持久化:
ContractHelper.save()写入CtrContract表及关联的 leg 表 - 状态:
PENDING_APPROVAL——交易尚未合法生效
找合约的 DB 记录: 主表是
CtrContract。唯一标识符(如TRS20250716001)是所有子表的主键。Legs 在CtrContractLeg,现金流在CtrContractLegCashFlow。
2.2 簿记数据结构
核心领域模型在 hedging-as 里(因为定价是生命周期中逻辑最重的部分):
hedging-as/src/com/cicc/service/contract/model/
├── ContractLegStruct.java — leg 结构(收/付、币种、时间表)
├── ContractInstrumentRelate.java — 工具到合约的映射
├── ContractUnderlyingRelate.java — 标的到合约的映射
├── ContractNotional.java — 名义本金(金额 + 币种 + 时间表)
├── ContractWithLegs.java — 聚合对象:合约 + 所有 legs
├── ContractMonitor.java — 监控状态
└── TodayContractRaInfo.java — 风险分析快照
CtrContract 的一行:
contractId VARCHAR(32) PK — 如 TRS20250716001
productType VARCHAR(16) — TRS, NDOLLAR, SNOWBALL, SWAP, OPTION
tradeDate DATE — 簿记日
startDate DATE — 生效日
maturityDate DATE — 到期日
ccy VARCHAR(3) — 结算币种
counterpartyId VARCHAR(32) — 外键→CounterParty
status VARCHAR(16) — PENDING_APPROVAL, ACTIVE, TERMINATED, EXPIRED
notional DECIMAL(20,4)
version INT — 修改时递增
第三阶段:交易后处理
3.1 信用与合规检查
业务: 交易生效前,中台 (middle office) 检查:客户在授信额度内吗?交易符合 ISDA 要求吗?对手方在制裁名单上吗?
系统: eds-web-app — customerInfo 模块 + blackOrWhiteList 模块
BlackOrWhiteListController检查对手方是否在监管黑名单上(OFAC、欧盟制裁等)CustomerMarginController检查授信使用率:所有活跃交易的名义本金之和 vs 对手方的授信额度- 检查通过后,合约状态从
PENDING_APPROVAL→ACTIVE
业务规则: 信用检查是软性的——失败了进人工审核。黑名单检查是硬性的——永久阻止交易。
3.2 确认书生成
业务: 交易需要一份法律确认书 (confirmation document)——用 ISDA 标准法律语言写的合约条款。双方都需要签署。
系统: eds-web-app — 确认书引擎
eds-web-app/src/com/cicc/action/contractFile/ — 确认书文件生成
eds-web-app/src/.../controller/ContractController.java
├── startPdfConvert() — 批量 Word→PDF
└── generateFile() — 生成确认书文档
startPdfConvert()触发ContractFileBatchOperateAction:读取交易台所有活跃合约,逐条渲染到 Word 模板,转成 PDF- 模板引擎将合约数据合并到 ISDA 主确认书模板
- 生成的 PDF 上传到 S3,在
ContractFile表中建立关联 - 确认书通过邮件或确认书门户发送给对手方
确认书争议的现实: 并非每笔交易都能顺利签署。2020-2022 年间大约 2-3% 的确认书遇到争议——通常是对手方不同意某一项条款措辞。最常见的争议原因不是交易要素错误,而是法律措辞(比如终止事件的定义范围)。争议确认书的平均解决时间是 2-4 周,期间合约状态卡在 PENDING,双方的法定权益处于不确定状态。2021 年有一笔大额 TRS(名义本金 5 亿)因为对手方法务部门对 ISDA 定义有不同解读,确认书争议了 6 周才签署。
模板目录:
eds-web-app/template/包含 Word 模板文件。每个产品类型有自己的模板。
3.3 对手方签署
业务: 对手方审阅并签署确认书。这可能需要几天或几周(有些对手方很慢)。
系统: 确认书状态在 CtrContract.confirmationStatus 追踪:PENDING(待签)、SIGNED(已签回)、DISPUTED(争议)
第四阶段:生命周期管理(日常工作)
交易生效后,每天都有工作:定盘重置 (fixing reset)、保证金追缴 (margin call)、票息支付、公司行为处理。
4.1 日固定盘与利率重置
业务: 交易的浮动 leg 定期重置。TRS 的融资 leg 每天/每月重置为 SHIBOR/TONA。雪球的票息观察日决定是否付票息。
系统: hedging-as — EOD 批处理
EOD 流程:
FixingScheduler在日终触发(可配置,通常是北京时间 17:00)- 对每个有浮动利率敞口的活跃合约:
- 从
eds-price-server读取定盘利率(聚合自路透/彭博) - 计算利息/票息金额:
名义本金 × 利率 × 计息因子 - 在
CtrContractLegCashFlow中生成CashFlowRecord
- 从
- 对雪球合约:检查是否发生了敲出或敲入事件(标的价相对障碍位)
- 对 NDF 合约:计算定盘价 vs 合约价的结算金额
4.2 按市值计价 (Mark-to-Market)
业务: 每笔交易有一个当前市场价值 (market value)。交易台需要知道:“如果我们今天平掉这笔交易,要收多少钱或付多少钱?”
系统: hedging-as — 定价引擎(和报价时同一个引擎,但每晚批量跑)
每晚:ValuationAction.startValuation() 触发所有活跃合约的估值 → PricingService.revalue() 加载当前市场数据并重新运行收益函数 → 结果存入 ValuationResult / TodayContractRaInfo → CtrContractEod 更新合约的日 P&L、delta、gamma、vega、theta
4.3 保证金与担保品管理
业务: OTC 交易由担保品 (collateral) 保障。如果交易对你有利 (in the money/ITM),对手方可能需交担保品。如果对你不利,你交担保品。
系统: hedging-as — margin 模块
MarginCalculator按合约计算初始保证金 (IM) 和变动保证金 (VM)- IM 基于 VaR 模型:“这笔交易如果清算需要 5 天,会亏多少?”
- VM 就是按市值计价:如果交易对客户价值 +1000 万,客户必须交 1000 万
- 结果发布到
MarginCall表推到前端给担保品团队
4.4 公司行为
业务: 标的股票分红、拆股、合并。交易的条款可能需要调整。
当 TRS 的标的股票分红时:
eds-price-server从市场数据检测到分红事件ContractDividendHelper识别所有受影响的合约- 对 TRS:分红记入/扣减给相关方(取决于总收益互换的性质——收益接收方获得分红)
ContractDividendHelper.adjustForDividend()调整合约的现金流计划
4.5 定盘/结算事件
业务: 在定盘日,系统读取定盘利率,计算结算金额,生成支付义务。在付款日,系统检查敲出/敲入状态,如果障碍未被触发,票息到期,生成资金变动。
第五阶段:对冲管理
业务: 交易台卖给客户雪球时,不会保留风险。他们会对冲——通常买入/卖出标的股票或指数期货。
系统: 对冲通过独立的对冲交易系统或 OMS 管理。
- 定价引擎计算每笔交易的 delta、gamma、vega
- 对冲交易台汇总每个标的的净 delta
- 净 delta 通过买入/卖出期货或再平衡期权来对冲
- 对冲效果通过对冲 P&L 与客户交易 P&L 的对比来监控
关键监控工具:
odts-option-web的实时对冲仪表盘、单合约 delta/gamma 视图、hedging-as的 EOD 对冲头寸计算
第六阶段:监管与会计
6.1 SAC 报告
业务: 每笔 OTC 交易必须在监管时限内报告给中国证券业协会 (SAC)。
系统: SacReportController 生成每日 SAC 报告。格式:SAC 标准 XML,包含交易明细、对手方信息、名义本金、估值。报告通过 API 提交到 SAC 的报告系统。
6.2 会计凭证
业务: 每笔资金变动产生会计凭证。财务团队需要看 P&L、应计利息、保证金划拨。
6.3 客户估值报告
业务: 客户每月收到估值报告,显示投资组合的当前价值。
第七阶段:交易生命周期结束
7.1 到期
业务: 到期日发生最终结算:
- 融资 leg 的最后一笔利息计算
- 价格回报 leg 的最终表现计算
- 产生净支付
系统: ContractSettleHelper.finalSettle() 计算最终支付 → 结算指令生成并发送到支付系统 → 合约状态 ACTIVE → EXPIRED
7.2 提前终止
业务: 客户或交易台可能提前终止。需要计算终止支付额(交易的市场价值),双方同意终止金额,交易关闭。
7.3 更替 (Novation)
业务: 交易转移到另一个对手方。在重组中罕见但会发生。在系统中实现为修改 counterpartyId 字段,新旧对手方都需确认。
每日时间表
| 时间 | 系统 | 活动 | 如果出问题 |
|---|---|---|---|
| 08:00 | eds-price-server | 市场数据导入 | 8:30 仍未完成 → 交易员用昨天数据开盘 |
| 09:30 | new-edsweb | 交易台开始簿记交易 | 前端加载慢 → 客户在线上等报价 |
| 15:00 | eds-web-app | 同日簿记截止 | 交易员电话催”等我 5 分钟”——每天都发生 |
| 16:00 | hedging-as | 定盘处理 | 卡住 → 17:00 估值启动推迟 |
| 17:00 | hedging-as | EOD 估值(按市值计价) | 估值太慢 → 保证金计算跟不上 |
| 18:00 | hedging-as | 保证金计算 | 算错 → 发错催缴 → 对方投诉 |
| 18:30 | ofareg | 监管报告 (SAC) | 报错 → 运营加班修数据重跑 |
| 19:00 | eds-web-app | 确认书生成 | 模板不匹配 → 运营手工改 |
| 19:30 | eds-web-app | 客户估值报告 | 延迟 → 客户第二天邮件追问 |
代码来找
| 场景 | 目录 |
|---|---|
| 修定价公式 | hedging-as/src/.../pricing/ |
| 加新产品类型 | hedging-as/src/.../edslib/payoff/ |
| 修交易簿记 bug | eds-web-app/src/.../ContractController.java |
| 改确认书模板 | eds-web-app/template/ |
| 调 EOD 定价 | hedging-as/src/.../action/valuation/ |
| 加对冲监控页 | odts-option-web/src/modules/hedge-monitor/ |
| 修保证金计算 | hedging-as/src/.../action/margin/ |
| 更新 SAC 报告 | eds-web-app/src/.../action/sacReport/ |
内行才知道的:簿记状态机里藏着”幽灵状态”
成熟的 PM/BA 总有一天会被问到:“这笔交易现在到底在什么状态?” 系统里有一个中心化的状态机,但它定义的状态比真正能流转到的状态多得多——有些状态是 2015 年设计时的”理想流程图”,从来没有被触发过。
状态机在哪
hedging-as/src/com/cicc/service/contract/statemachine/
├── ContractStateMachine.java — 状态枚举 + 转移规则
└── StateTransition.java — 每个转移的前置条件
ContractStateMachine 里枚举了一长串状态:BOOKED、CONFIRMED、EXECUTED、SETTLED、EXPIRED、KNOCKED_OUT、TERMINATED、DISPUTED、AMENDED…… 但实际上,绝大多数交易只经过 BOOKED → CONFIRMED → (EXECUTED/EXPIRED) → SETTLED 这条主干。像 DISPUTED、AMENDED 这类”看起来很合理”的状态,在生产里几乎不被写入——它们停留在代码里,是因为早期设计者认为”肯定会有争议和修订流程”。
业务陷阱: 如果你在做报表或风控看板,按 ContractStateMachine 的全量状态维度来分组统计,会得到一堆永远为 0 的桶。这不是数据缺失,是状态机过度设计。看真实分布要去 CtrContract 表的 status 字段实际值,而不是读枚举。
一个真实踩坑:敲出之后状态没及时翻
雪球敲出后,StateTransition 理论上应在 EOD 的 BarrierCheck 步骤把状态翻成 KNOCKED_OUT。但 2021 年出过一次 bug:敲出事件发生在北京时间 14:50(早盘收盘前),而 BarrierCheck 用的是 T 日收盘定盘价 (fixing price) 来判断,定盘价要到 15:00 才生成。结果这笔雪球在 14:50 已经实质敲出、客户已被告知,但系统状态直到当晚 EOD 跑完才翻——中间那 4 个小时里,它在”已敲出但状态仍是活跃”的灰色地带,风控日报把它的名义本金照常计入了未敲出敞口。
业务影响: 当天的风险敞口日报高估了约 ¥2 亿名义本金的敲入风险。后来加了一条盘中 (intraday) 敲出快检:在 MarketDataRefresh 之后、Valuation 之前插一步 IntradayBarrierCheck,用实时价而非定盘价做一次轻量敲出预判,只用于风控预警,不改变正式状态——正式状态仍以收盘定盘价为准。这条”双轨”逻辑现在还活着,是新人在 eod/ 包里最容易看懵的一段。
下一篇:06-Code-Map —— 每个项目的详细职责指南