Cats Lifecycle
CATS 商品交易业务生命周期 / Commodity Trade Lifecycle
1. 背景:CATS 是什么? / What Is CATS?
CATS(Commodity Auto Trading System)是中金公司 Commodity 部门的商品交易管理系统,覆盖从交易录入到最终结算的全生命周期。它和 ODTS(Options Derivatives Trading System)在同一个技术平台(BaseWeb/Spring+Hibernate)上构建,但 CATS 只处理商品类交易,而 ODTS 处理更广泛的权益类/指数类场外衍生品。
CATS 的核心定位:一个轻量级的一站式商品交易管理平台,不依赖外部交易管理系统,从 Trade Capture 到 Confirmation 到 Settlement 全部自闭环。
业务前提: Commodity 部门的客户主要是产业客户(矿企、贸易商、加工企业)和金融机构,产品以 Commodity Forward/ Swap / Option 为主,标准化程度比 ODTS 的奇异期权要高。
2. Trade Capture:交易怎样进入 CATS?
CATS 的交易录入有两个入口,对应两套完全不同的业务流程。
2.1 自动录入:Bloomberg Feed(TBB / 直通式)
大客户(如矿业公司、对冲基金)的交易通过 BloomBerg TOMS(Trade Order Management System)以电子消息方式推送到 CATS。每条消息是一个 feedRef(Bloomberg 唯一表示)+ feedStatus(操作类型)。
Feed 消息类型(定义见 com.cicc.commodity.statictext.Status):
| 常量 | Feed 值 | 含义 | 场景 |
|---|---|---|---|
BB_NEW | FT | 新建 | 新交易的首次录入 |
BB_AMEND | CFT | 修改 | 交易日起当天修改 |
BB_CANCEL | XFT | 取消 | 交易日起当天取消 |
BB_P_AMEND | PCF | 交易日之后修改 | 已过 trade date 的修改 |
BB_P_CANCEL | PXF | 交易日之后取消 | 已过 trade date 的取消 |
BB_CP_CANCEL | XPF | 交易日之后取消已过结算日交易 | 最复杂的取消场景 |
另外还有一套针对 market sector_two=10 的变体(_2 后缀)以及一个独立版本 _3 后缀,说明 Bloomberg 消息格式因交易所/品种而异,CATS 被迫维护了多套兼容映射。——这是业务集成中最棘手的部分:同一个 API 进来,解析逻辑却有 12 种分支。
Feed 处理流程(CommodityBookingService.bookingFromFeed(), commodity/src/.../booking/CommodityBookingService.java):
feed 消息到达
→ CommodityValidationHelper.feedValidate() // 格式验证
→ CommodityEnrichmentHelper.feedEnrich() // 数据富化
→ 根据 feedStatus 路由:
FT (NEW) → 直接创建新交易
CFT (AMEND) → 查 originalFeedRef,新版本 trade.cancelled=false,originalTradeId 指向原版本
XFT (CANCEL) → 查 feedRef,原版本 trade.cancelled=true
Enrichment(富化)这一步是关键,它把 Bloomberg 的裸字段转换为业务可用的数据:
- Counterparty Resolution(对手方识别): Bloomberg 消息中
counterpartyId可能为空或缩写。CATS 调用ClientProvider.getBySymbol()查 RDS(Reference Data Service),判断该对手方是Client(客户)还是Broker(经纪商)。空值时使用系统默认对手方(配置文件KEY_FEED_DEFAULT_COUNTERPARTY)。 - Product Resolution(产品识别): 根据
ticker(如 “LMCADP 3”)查CommodityProductProvider,确认该产品在 RDS 中有注册。如果没有则标记tradeStatus=PRODUCT_ERROR。 - Premium Date 计算: 如果是期权,
premiumDate = T+1(基于WeekBusinessCalendar,跳过 weekend/holiday)。 - **Legal Entity: ** BookId 决定:
COMMDTY2→ FP (CICC FP),其他 → CT (CICC CT)。
Feed 的 tradeStatus 错误标记:
FEED_ERROR— 无法识别的 feedStatusCLIENT_ERROR— 对手方查找失败PRODUCT_ERROR— 产品查找失败
这些错误不会阻止交易入库,但会阻止下游流程(确认、结算)执行。
2.2 手动录入:Excel 批量导入
CATS 还有一个不太为人知但大量使用的入口:Excel 批量导入(OtcTradeExcelImporter, commodity/src/.../otc/booking/OtcTradeExcelImporter.java)。
这个导入器最初是为 SunGard 系统迁移设计的。它读取 xlsx/csv 文件,解析 sheet 中的行列头映射,批量创建交易。支持的产品类型:
- Future(期货):
fut possheet,读取 underlying/tradeDate/volume2/price/dealId - Option(期权): 历史代码注释中留有三套模板行头:
TRADE_VANILLA、TRADE_SHARK_FIN、TRADE_FUTURE,但当前只启用了 Future
此外还有一个 importFundsExcel() 方法,专门导入 SunGard 格式的客户资金数据(22 个资金相关字段 + 前一日余额),直接写入 OtcFunds 表。
2.3 手动录入:UI 直操作
交易员通过 Web UI 录入交易时,走 CommodityBookingService.booking():
Manual trade → validate() → enrich() →
if (entityId == null): // 新交易
create → 生成 tradeId → 设置 dealId = "bookId-mtardeId"
else: // 修改现有交易
get original → 原交易 cancelled=true → transType 根据日期路由:
tradeDate == today → BOOKING_CANCEL / BOOKING_AMEND
tradeDate < today → BOOKING_P_CANCEL / BOOKING_P_AMEND
create amended version → 关联 originalTradeId
这里有一个业务判断: 同一交易日内修改 vs 跨日修改,在会计处理上完全不同(前者可以”修正”原账目,后者必须分开两条记录)。CATS 通过比较 today.compareTo(tradeDate) 来区分。
3. Booking 完成后发生了什么?
每笔交易 create 或 update 完成后,CommodityBookingService 最后一行是:
BookingDownstreamJob.enqueue(tradeId);
这条队列启动了一条异步处理链,是 CATS 交易生命周期的核心引擎。
3.1 SystemDownstreamPublishJob
(commodity/src/.../downstream/SystemDownstreamPublishJob.java)
这是一个 Spring InitializingBean:容器启动 60 秒后,一个后台线程开始消费队列。对于队列中的每个 tradeId:
CommodityService.get(tradeId)— 从数据库拿完整交易数据JSON.toJSONString(commodity)— 序列化为 JSONdownstreamQueueSender.send(json)— 通过 JMS 发送到下游系统
这个 JMS 消息的消费者是谁? 根据代码日志名为 downstreamLog,这很可能被以下系统消费:
- Risk system(风险管理系统,用于计算希腊值/Greeks)
- Accounting system(会计系统,用于记账)
- Regulatory reporting(监管报送系统)
业务视角: 这是 CATS 与外部系统的唯一单向接口。所有”外部”系统只读 CATS 的 Trade 数据,不能回写。这意味着 ODTS 中那种”Trade →Risk 定价 →Feedback Trade”的闭环在 CATS 中不存在。
3.2 ConfirmationJob
(commodity/src/.../confirmation/ConfirmationJob.java)
同样是 Spring InitializingBean + 后台队列,但逻辑复杂得多。这个 Job 是**自动确认(Auto-Confirm)**引擎,负责:
- Generate(生成 PDF): 调用
ConfirmProducer.produce()生成确认书 PDF 文件 - Send(发送邮件): 调用
EmailService.send(confirm)发送给客户
Confirmation 逻辑:
dequeue → 拿到 commodities
↓
[DEFAULT_AUTO_CONFIRM_DELAY = 30min] // 可配置,用于给交易员窗口修改
↓
if (cancelled trade):
找到原 Confirmation → 如果原确认已发送 → 生成 Cancel Confirmation
→ 如果原确认未发送 → 标记 CONFIRM_NEW_CANCELLED,跳过
else:
生成新的 Confirmation PDF
↓
发送邮件:
if (client == CICCL) → 不发送(2019-03-26 特例处理)
if (发送成功) → confirm.sendOut=true, 状态更新为 CONFIRM_SENT / CONFIRM_CANCELLED_SENT
if (发送失败) → 状态 CONFIRM_SEND_ERROR / CONFIRM_CANCELLED_SENT_ERROR
Confirmation Status 状态机(11 种状态):
NEW
├──→ GEN_ERROR (PDF 生成失败)
├──→ GENERATED (PDF 生成成功)
│ ├──→ SEND_ERROR (邮件发送失败)
│ └──→ SENT (邮件发送成功)
└──→ NEW_CANCELLED (交易取消时还未生成确认)
CANCELLED 时的分支:
CXL_GENERATED → CXL_SENT / CXL_SENT_ERROR
CXL_GEN_ERROR
NEW_CANCELLED (原确认未发送时)
这是一个完备但沉重的状态机。每笔交易在 Confirmation 上有 11 种可能的状态,加上交易本身的 cancelled/deleted/tradeStatus,一笔交易的状态组合超过 40 种。这在运营上意味着:error recovery(错误恢复)不是简单重试,需要人工判断当前处于哪个分支。
4. 资金与结算 / Cash Movement & Settlement
4.1 CashMovementHandler
(commodity/src/.../cash/CashMovementHandler.java)
CATS 的资金处理是通过每日批处理(EOD)触发。dailyCashMovementByClient() 对每个客户:
- 从
ClientTradePnl表获取当日的 PnL 数据 - 按产品类型过滤:Option 的 PnL 不自动生成 CashMovement(因为期权 Premium 和 Settlement 分别在两个不同时间点处理)
- 生成
CashMovement记录,包含:event=PL(PnL settlement)amount= realized PnLcurrency= USD (统一转换为客户结算货币)receiveDate= pnlDatesettled= falsegenPhase=ST(Statement generation)status=NEW(等待审批)
Approval Workflow: CashMovement 的状态从 NEW → APPROVED 需要人工审批。这是一个业务设计选择:即使系统自动计算 PnL,实际资金调拨必须经风控/运营确认后才执行。
4.2 期权的 Premium 处理
期权有 premiumDate(T+2),处理逻辑独立于普通 PnL:
CashMovementHandler.enrichPremium()— 生成 Premium 收付指令- 买入期权(Long):支付 Premium
- 卖出期权(Short):收取 Premium
4.3 Margin Call(追保)
CATS 支持 Margin Call 生成和发送(功能在 Status 中定义了 4 种状态:GEN_ERROR / GENERATED / SEND_ERROR / SENT)。但相比于 ODTS 中复杂的 SIMM/ISDA CSA 计算,CATS 的 Margin Call 处理更原始——它只在 EOD 生成 PDF 通知客户,不涉及 IM/VM 的动态计算。
5. 定价与估值 / Pricing & Valuation
5.1 PricingCalculator
(commodity/src/.../otc/pricing/PricingCalculator.java)
CATS 的定价引擎是一个内嵌的数学计算库,支持以下产品:
| 产品类型 | 数学模块 | 用途 |
|---|---|---|
| Vanilla Option | Vanilla | 普通看涨/看跌期权定价 |
| Call/Put Spread (CPS) | CallPutSpread | 价差期权 |
| Shark Fin | Sharkfin | 鲨鱼鳍(单鲨) |
| Double Shark Fin | DoubleSharkfin | 双鲨鱼鳍 |
| Enhanced Double Shark Fin | EDoubleSharkfin | 增强型双鲨鱼鳍 |
| Barrier Option | Barriers | 障碍期权 |
Pricing Calculator 的工作流:
- 取 underlying 数据:
OtcUnderlyService.getUnderlyByTickerAndRefDate()— 从数据库取指定 ticker 在估值日的结算价 - 取产品元数据:
UnderlyingProvider.getByProductSymbol()— 查 RDS 产品信息 - 计算 Greeks: 每个产品类型计算 delta/gamma/vega/theta
- Margin 计算: 根据 margin rate markup、participation rate 等参数计算保证金
- 输出 OtcPnlOption: 包含 mtm、delta、gamma、vega、theta、margin 等
关键发现:CATS 的定价引擎是硬编码的,没有 ODTS 中的 Pricing Strategy Pattern。 每种产品类型的定价逻辑直接在 PricingCalculator 中通过 ContractType switch 路由。这意味着新增产品类型需要修改核心定价类,不符合开闭原则(Open/Closed Principle)。
5.2 估值频率
定价是 EOD 触发的批处理过程。每日收盘后:
- 拉取所有 market data(交易所结算价、Bloomberg 行情)
- 遍历所有未到期交易的 Portfolio
- 逐个调用
PricingCalculator计算 MTM 和 Greeks - 结果写回
OtcPnlOption/OtcPnlVanilla表 - 汇总到 Client/Book 级别 PnL 用于 CashMovement
没有支持日内实时的 on-demand 定价——这是 CATS 与 ODTS 的一个重要差异(ODTS 有独立的 Pricing Engine 支持实时询价)。
6. EOD 批处理流程 / End-of-Day Batch
CATS 的 EOD Batch 大致按以下顺序执行:
时间轴 (T日收盘后):
17:00 — Batch 调度启动 (Quartz Scheduler)
↓
1. 定价 Engine 运行:所有未到期 Option + Future 的 MTM 计算
↓
2. PnL 归因:每个 Book/Client 的 realized + unrealized PnL
↓
3. CashMovement 生成:Non-Option 的 PnL → CashMovement (NEW)
↓
4. Premium 处理:期权 Premium 收付指令
↓
5. Statement 生成:客户对账单 PDF
↓
6. Margin Call 计算:根据持仓计算保证金需求
↓
7. 下游分发:SystemDownstreamPublishJob 推送数据到 Risk/Accounting
次日 09:00
批处理中有两个”窗口”值得注意:
- Confirmation Delay(默认 30 分钟): 交易在入录后 30 分钟才进入确认流程。这是给交易员一个”后悔期”(cooling-off period),在此期间可以修改或取消交易而不触发确认书发送。
- CICCL 白名单例外(2019-03-26): CICC London 客户的确认书不自动发送邮件,而是走另一套流程。这是业务特例,直接写死在了 ConfirmationJob 中。
7. CATS 中的状态管理 / State Machine
CATS 的 Trade 在数据库层面使用字段组合来表示业务状态(而非单一枚举):
// Commodity (extends Trade) 的关键状态字段
boolean cancelled; // true = 已取消
boolean deleted; // true = 已删除(逻辑删除)
String confirmationStatus; // 11种确认状态
String tradeStatus; // APPROVED / FEED_ERROR / CLIENT_ERROR / PRODUCT_ERROR
String transType; // NEW / AMEND / CANCEL / P_AMEND / P_CANCEL
String feedStatus; // FT / CFT / XFT (Bloomberg 原始类型)
没有单一的状态字段表示”这笔交易现在是什么状态”。 业务状态是上述字段的组合推断。例如:
- “正常活动的交易”:
cancelled=false && deleted=false && tradeStatus=APPROVED - “已取消但还未生成确认书的交易”:
cancelled=true && confirmationStatus=NEW_CANCELLED - “Feed 进来的取消指令但原交易不存在”:
cancelled=false && tradeStatus=FEED_ERROR
这种设计在读取时需要组合判断,SQL 查询中需要多个 Restrictions 组合(如 CommodityService.listPendingTrades() 中复杂的 Criterion AND/OR 组合)。从业务角度看,状态的可理解性交给了 DAO 层的查询逻辑,而不是模型层。
8. 产品覆盖 / Product Coverage
CATS 支持的主要产品类型:
| ContractType | 内部覆盖度 | 说明 |
|---|---|---|
| COMMODITY FORWARD | 完整 | 最基础的商品远期 |
| OPTION (Vanilla) | 完整 | 普通看涨/看跌期权 |
| CALL_PUT_SPREAD | 完整 | 价差组合 |
| FUTURE | 完整(含 SunGard 迁移导入) | 期货 |
| SHARKFIN | 完整(需数学计算) | 鲨鱼鳍结构 |
| DOUBLE_SHARKFIN | 完整 | 双鲨鱼鳍 |
| BARRIER | 基础 | 障碍期权 |
| FX | 基础(不生成确认书) | 外汇远期/掉期 |
空白处: CATS 没有 Snowball(雪球)产品。在 ODTS 中是核心产品的 Snowball,在 CATS 中不存在——因为 Snowball 在 2018 年后才兴起,而 CATS 的开发周期主要在 2014-2018 年。
9. CATS vs ODTS:生命周期差异
| 维度 | CATS | ODTS |
|---|---|---|
| Trade Capture | Bloomberg Feed + UI + Excel | UI + ETF/Stock Feed |
| Confirmation | 自动生成 PDF + 自动发邮件(11 种状态) | 自动生成 + 手动触发发送 |
| Pricing Engine | 内嵌硬编码(Vanilla/CPS/SharkFin) | Pricing Strategy Pattern,独立 Engine |
| Real-time Pricing | 不支持 | 支持实时询价 |
| Cash Movement | 每日 EOD 统一处理,NEW→APPROVED | 实时 + EOD |
| Margin Call | PDF 通知(4 种状态) | SIMM + CSA 计算,复杂 Collateral Management |
| Settlement | 手动审批 | 批量 + 手动 |
| Downstream | JMS JSON 推送(单向) | JMS + 直接 DB 访问(双向) |
| 技术债务 | 多套 feedStatus 映射、硬编码特例 | 同样有大量特例,但有微服务边界 |
10. 基于代码的证据 / Evidence from the Codebase
这篇文档中的所有判断基于以下 CATS 源码文件的模式分析:
CommodityBookingService.java— 交易录入核心,三种分支(new/amend/cancel)CommodityEnrichmentHelper.java— 对手方/产品/日期富化OtcVanValidator.java,OtcCpsValidator.java— 合约验证SystemDownstreamPublishJob.java— JMS 下游分发ConfirmationJob.java— 自动确认 + 邮件发送PricingCalculator.java— 定价引擎(调用 Vanilla/CPS/SharkFin 数学模块)CashMovementHandler.java— 资金处理 + 审批工作流OtcTradeExcelImporter.java— Excel/CSV 批量导入Status.java— 所有状态常量的唯一出处Trade.java,Commodity.java— 数据模型定义
10.5 业务代价:CATS 的设计选择意味着什么成本?
40+ 种状态组合 = 运营排查黑洞
第 3.2 节指出,一笔交易的状态是 cancelled / deleted / confirmationStatus(11) / tradeStatus / transType / feedStatus 的字段组合,组合超过 40 种。从运营视角:
→ 一笔"卡住"的交易,人工要逐字段判断它到底卡在哪个分支
→ error recovery 不是"重试按钮",而是"读懂状态机后手工改字段"
→ 新人培训成本高:理解这 40 种组合需要读源码,而非看文档
PM/BA 启示: 状态机写在代码里(第 7 节)、不配置化,意味着每一次状态逻辑的变更都要改 Java + 发版,而不能靠运营配置解决。这在”监管临时要求新增一种交易状态”时尤其 painful。
Monolith 的一切都在一个部署单元 = 扩展性天花板
第 11 节总结提到,CATS 是”一个 monolith 内部把所有事情做了”——从 Bloomberg feed 解析到 PDF 生成到邮件发送。代价是:
→ 新增产品类型(如想加 Snowball)要改核心 PricingCalculator(第 5.1 节已指出它硬编码)
→ 新增外部接入要改 DownstreamJob
→ 特例(CICCL 不发确认书)hardcode 在业务代码里
业务影响(估算):
→ 一个"加新品种"的需求,在 CATS 要动核心类 + 回归全部 11 种确认状态 + 重测 EOD 链路
→ 这比 ODTS 的微服务边界(加品种只动 Pricing Engine 一个服务)慢得多
→ 2014-2018 年开发周期里没做 Snowball,等到 2018 后雪球爆发时,
CATS 已定型,只能靠 ODTS 扛雪球(见 34 文档)——两套系统边界的代价在此显现
Feed 的 12 种解析分支 = 隐性维护负债
第 2.1 节提到同一 Bloomberg API 进来,解析逻辑有 12 种分支(含 _2 / _3 后缀变体)。这意味着:
→ 每次 Bloomberg 改一次消息格式,CATS 要逐一核对 12 个分支是否还成立
→ 这是 50 文档讲"双系统客户端暴露合并缺口"的同源问题:
外部格式变体越多,内部兼容代码越脆弱
→ 维护成本随"外部系统版本数"线性增长,而非随"产品数"增长
11. 总结 / Summary
CATS 是一个务实的、面向 Commodity 业务的一站式交易管理系统。它的设计决策反映了 Commodity 业务的特点:产品类型相对固定、客户以产业客户为主、交易量相对小但单笔金额大、业务流程可以接受隔夜批处理而不是实时响应。
与 ODTS 相比,CATS 的系统边界更窄(只覆盖 Commodity),但生命周期覆盖更完整(从 feed 到 settlement 完全自闭环)。ODTS 依靠多个微服务和外部 Pricing Engine 协作,CATS 则是一个 monolith 内部把所有事情做了——从 Bloomberg feed 解析,到 PDF 确认书生成,到邮件发送,到定价计算,到资金处理。
这种”everything in one place”的设计简化了运维(只有一个部署单元),但代价是扩展性受限:新增产品类型需要改核心类,新增外部系统接入需要修改 DownstreamJob,特例逻辑(如 CICCL 不发确认书)直接 hardcode 在业务代码中而不是配置化。