How To Read The Code
ODTS 10 — 怎么看代码:开发者实用指南
为什么需要这篇文档
前面 9 篇文档告诉你了系统是什么。这篇告诉你怎么改它。
每一节从一个开发者场景开始:“我要做 X。“这一节告诉你准确在哪里看、读什么、改什么。
前置条件: 你读过 01–09。你了解 Java、Spring Boot、Vue 2 基础。你知道 TRS 是什么。
场景 1:我要给合约加一个字段
业务: 交易台要在每笔 TRS 交易上记录一个新属性——比如”融资重置频率 (funding reset frequency)”。
为什么业务需要这个字段: 融资利率从”每月重置”改为”每季度重置”意味着客户的融资成本更稳定(不随 SHIBOR 月波动剧烈变化)。这个改动来自几个大客户的要求——他们认为月度重置增加了不确定性。如果字段加错了或漏加了,TRS 的利息计算全部按默认频率走,客户对账单的融资费用就不对。
找到领域模型
合约领域模型在 hedging-as(定价团队定义了模型):
hedging-as/src/com/cicc/service/contract/model/
找与你业务对象匹配的类。对 TRS,ContractLegStruct.java 定义了 leg 属性。合约头看 ContractHelper.java。
改动的链条
hedging-as/src/com/cicc/service/contract/model/ContractLegStruct.java
↓ (如果用 protobuf 则重新生成)
hedging-as/src/com/framework/protobuf/*/ (如果字段跨服务边界)
↓
eds-web-app/db/migration/Vxxx__add_funding_reset_freq.sql
↓
eds-web-app/src/com/cicc/edsBoot/mapper/ContractMapper.xml
↓
eds-web-app/src/com/cicc/edsBoot/controller/ContractController.java
↓
new-edsweb/src/views/option-contract/ContractForm.vue
场景 2:交易价格算错了
业务: 客户报告他们的雪球 MTM 值不对。
第 1 步:理解客户看到的价格
找估值记录:
SELECT * FROM TodayContractRaInfo
WHERE contractId = 'SNOWBALL20250716001'
ORDER BY tradeDate DESC LIMIT 1;
这告诉你系统算出的 MTM、delta、gamma、vega。
第 2 步:检查行情数据
大部分定价 bug 是行情数据问题,不是公式问题:
hedging-as/src/com/cicc/pricing/dataCenter/
├── PriceCache.java — 标的价格对吗?
├── VolSurfaceManager.java — Vol 曲面是最新的吗?
└── IRCurveManager.java — 利率对吗?
常见问题:
- 标的价格是 T-1 的数据(行情数据源断过)
- 波动率曲面没更新(曲面构建器用了过期数据)
- 股息预测缺失(dividend helper 跳过了某只股票)
- 美元计价交易的汇率不对
第 3 步:手动重跑估值
你可以对单笔合约触发手动估值:
- 找
ValuationAction.startValuation()在hedging-as/src/com/cicc/action/valuation/ - 或者用 eds-web-app 的 EOD debug endpoint
第 4 步:检查 Payoff
如果行情数据正确,问题在收益函数:
hedging-as/src/com/cicc/edslib/payoff/SnowballPayoff.java
常见 bug:
- 敲出障碍比较方向(
>=vs>) - 票息计息的日计数惯例 (day count convention):30/360 vs ACT/365
- 记忆雪球 (Memory Snowball) 逻辑:未付票息没有累积
场景 3:EOD 批处理失败了
业务: 今天的日终处理没完成。仓位和保证金都还是昨天的。
EOD 失败时业务在做什么: 交易员和运营在等,但也得继续工作。2022 年一次 EOD 延迟到晚上 22:00——期间运营手动计算了 TOP 10 头寸的 MTM 发给交易员(用 Excel),交易员用这些手工 MTM 做当晚的保证金催缴估算。这种”手工 EOD”发生过至少 5 次。每次需要 2 人花 2-3 小时,而且手工计算的出错率比系统高——有一次估算和第二天系统实际结果差了 5%。
EOD 在哪里跑
hedging-as/src/com/cicc/action/eod/ — EOD 编排
hedging-as/src/com/cicc/service/eod/ — EOD 步骤实现
EOD 批处理顺序
EOD 批处理是一系列顺序步骤。每一步是 EodScheduler 中的一个方法或一个独立定时任务:
1. MarketDataRefresh — 获取最新价格、vol、利率
2. Fixing — 处理浮动 leg 的定盘事件
3. CorporateAction — 应用分红、拆股、并购
4. BarrierCheck — 检查雪球和障碍期权的 KO/KI
5. Valuation — 对所有活跃合约重新估值
6. Greeks — 计算 delta、gamma、vega、theta、rho
7. Margin — 计算 IM + VM
8. CashMovement — 生成付款指令
9. Reporting — 中期协报告、客户报告
10. Cleanup — 归档临时数据、更新状态
如何诊断失败
-
查日志: EOD 日志去
hedging-as/logs/eod.log。每一步记录开始和结束时间。 -
找哪一步失败了:
grep "EOD_STEP" hedging-as/logs/eod.log | tail -20失败前最后完成的那一步就是嫌疑步骤。
-
常见失败:
- 行情数据: 一个或多个标的没有价格 → 第 1 步失败
- 公司行为冲突: 公司行为条款意外 → 第 3 步失败
- 超时: 一个复杂雪球估值跑太久 → 第 5 步超时
- 空指针 (Null pointer): 合约缺少数据(如没有 leg 定义)→ 任何步骤
-
从失败步骤重新执行: 每一步 EOD 都是幂等的 (idempotent)。你可以单独调用相应步骤的 action 从失败点重跑。
场景 4:我要加一个新品种
业务: 交易台要做 Variance Swap。这是一个系统里完全没有的新品种。
需要加什么
| 组件 | 位置 | 工作量 |
|---|---|---|
| Payoff 函数 | hedging-as/edslib/payoff/VarianceSwapPayoff.java | 3–5 天 |
| 估值方法 | hedging-as/edslib/valuation/ | 1–2 天 |
| 品种类型枚举 | eds-web-app/com/cicc/constant/ | 1 小时 |
| 定价服务 | hedging-as/pricing/service/VarianceSwapPricingService.java | 1–2 天 |
| 簿记表单 | new-edsweb/src/views/variance-swap/ | 2–3 天 |
| API Controller | eds-web-app/edsBoot/controller/VarianceSwapController.java | 1 天 |
| 确认书模板 | eds-web-app/template/variance_swap.docx | 1–2 天 |
| EOD 处理 | hedging-as/service/eod/VarianceSwapEodStep.java | 1–2 天 |
| 保证金规则 | hedging-as/pricing/margin/VarianceSwapMargin.java | 1–2 天 |
| 中期协报告 | eds-web-app/action/sacReport/VarianceSwapReportStep.java | 1 天 |
| 测试 | 以上所有 | 3–5 天 |
总计: 一个人 3–4 周,两个人 2 周。
Payoff 实现模式
public class VarianceSwapPayoff {
public double computePayoff(double[] prices, double strikeVar, double notional) {
int n = prices.length - 1;
double sumSquaredLogReturns = 0;
for (int i = 1; i < prices.length; i++) {
double logReturn = Math.log(prices[i] / prices[i-1]);
sumSquaredLogReturns += logReturn * logReturn;
}
double realizedVar = sumSquaredLogReturns / n * 252;
return (realizedVar - strikeVar) * notional / (2 * strikeVar);
}
}
场景 5:UI 页面加载不出来
业务: 用户点击”合约簿记”,看到白屏。
第 1 步:打开浏览器控制台 (F12)
- 红色的 Vue 报错: JavaScript 异常——可能是缺少 import 或 null 引用
- Network tab → 4xx/5xx: API 调用失败
- Network tab → 200 但没数据: API 返回空结果
第 2 步:追踪请求
浏览器 URL: /eds-boot/views/contract/list.html
│
▼
Nginx → 从 new-edsweb/dist/ 返回 Vue SPA HTML
│
▼
Vue 加载 → 调用 API: GET /eds-boot/api/v1/contract/list
│
▼
Spring Boot (eds-web-app) → 读 DB → 返回 JSON
│
▼
Vue 渲染表格
需要排查的地方:
new-edsweb/src/views/option-contract/ContractList.vue— 组件对吗?new-edsweb/src/api/option-contract/contractApi.js— API URL 对吗?eds-web-app/.../ContractController.java— 端点工作正常吗?- Nginx 日志 — 路由对吗?
常见 Vue 加载 bug
| 症状 | 可能原因 | 修复 |
|---|---|---|
白屏,控制台有 [Vue warn] | 缺少组件 import | import X from './X.vue' |
| 白屏,无控制台错误 | Vue Router 配置错误 | 检查 src/router/index.js |
| API 返回 404 | Controller 路径或方法不对 | 检查 @RequestMapping |
| API 返回 500 | 后端异常 | 查 eds-web-app/logs/ |
| Element UI 样式破掉 | 缺少 CSS import | 检查 main.js 的 import |
场景 6:要调试一条 Protobuf 消息
ODTS 用 Protobuf 做服务间通信。消息定义在 .proto 文件中,编译成 Java。
.proto 文件在哪
hedging-as/src/com/framework/protobuf/
├── bookingAs/ — 簿记服务消息
├── gateway/ — 网关协议
├── hedgingAs/ — hedging-as 特有消息
├── pricingAS/ — 定价服务消息
├── reckoning/ — 结算消息
├── trade/ — 交易数据消息
└── util/ — 共享类型(日期、金额等)
怎么追踪一条 Protobuf 消息
- 找到消息的
.proto定义 - 找到创建消息的 Java
build()调用(搜索.newBuilder()) - 找到对面读取消息的
parseFrom()调用 - 检查传输层:ActiveMQ 队列名或 Dipper endpoint
调试技巧
在 log4j2.xml 中启用 Protobuf 日志:
<Logger name="com.framework.protobuf" level="TRACE"/>
这会记录每条序列化/反序列化的消息,包括十六进制 dump。
场景 7:数据库查询慢
业务: 合约列表页面加载需要 30 秒。
查询路径
- Chrome DevTools → Network tab → 看哪个 API 慢
- 这个 API 调用
ContractController.list()→ 调ContractDao→ 执行 SQL
找到 SQL
MyBatis mapper 在:
eds-web-app/src/com/cicc/edsBoot/mapper/
DAO 实现:
eds-web-app/src/com/cicc/edsBoot/dao/
常见性能问题
| 问题 | 症状 | 修复 |
|---|---|---|
| 缺少索引 | CtrContract 全表扫描 | 在 WHERE 列上加索引 |
| N+1 查询 | 合约列表 → N 次 leg 查询 | 用 JOIN 或批量查询 |
| 结果集太大 | 一次加载 10000 条合约 | 加分页 (大部分视图已经做了) |
| 没有缓存 | 相同 SQL 跑了 100 次 | 加 Redis 缓存 (RedisOperationController) |
场景 8:我搞崩了生产环境,救命!
应急手册
-
冷静。 大部分 bug 不是灾难性的。这个系统已经跑了 10 年。
-
前端还是后端?
- 打开浏览器控制台
- 页面能加载但数据不对 → 后端
- 页面加载不出来 → 前端
- 看到 500 错误 → 后端
- 看到 404 → 路由问题(前端)或端点不存在(后端)
-
查日志:
hedging-as/logs/— 定价、EOD、保证金日志eds-web-app/logs/— API、簿记、确认日志new-edsweb/logs/— 前端
-
回滚策略:
- UI bug: 回退 Vue 构建产物,重新部署
new-edsweb/dist/ - API bug: 回退 Spring Boot JAR,运行
copy_restart_jar.sh - 定价 bug: 回退 hedging-as JAR
- 数据库 schema 变更: 运行回滚 migration
- 配置: 从 git 历史恢复之前的版本
- UI bug: 回退 Vue 构建产物,重新部署
-
告诉业务:
- “我们发现了一个数据问题”(如果是行情数据)
- “我们发现了一个计算 bug 并已部署修复”(如果是代码问题)
- “我们正在用修正后的数据重新运行 EOD 批处理”(如果是批处理问题)
场景 8.5:我要查一笔交易”为什么今天没被估值”
业务: 交易员问,“我昨天簿记的雪球,今天的 MTM 日报里怎么没有?”
这不是”价格算错”(场景 2),而是”这笔交易压根没进估值范围”。这类问题的根因几乎都不在定价引擎,而在”合约有没有被正确纳入 EOD 的活跃合约集合”。
排查路径(按命中率排序)
-
合约状态对不对。
CtrContract.status必须是BOOKED或CONFIRMED之类的”活跃”值。如果它卡在过渡态(见 05 的幽灵状态问题),ValuationAction的预处理会直接跳过它。 -
有没有被 EOD 的”排除名单”捞走。 hedging-as 的 EOD 编排里有一段”特殊合约过滤”逻辑——某些处于争议、司法冻结、或手工锁定的合约会被加到一个排除集合,不参与自动估值。这段逻辑在
service/eod/EodContractFilter.java,经常被人忘记。 -
定盘/行情绑定对不对。 如果这笔交易关联的标的价格在
MarketDataRefresh第 1 步就没拿到(断行情),BarrierCheck和Valuation都会因为缺数据而跳过或报错——表现就是”整批里就它没出数”。
-- 快速确认:这笔交易在今天的估值表里有记录吗?
SELECT contractId, tradeDate, mtm, status
FROM TodayContractRaInfo
WHERE contractId = 'YOUR_CONTRACT_ID';
-- 如果没行 → 它没进估值范围(状态/过滤/行情问题)
-- 如果有行但 mtm 为 NULL → 估值跑了一半因缺数据失败
内行提示: 新人常一头扎进 SnowballPayoff 去改公式,折腾半天发现交易根本没被估值。记住 ODTS 的一个分层铁律——“有没有被算”和”算得对不对”是两个独立的问题,前者先查状态/过滤/行情,后者才查 Payoff。
改代码的真实代价:一条字段链牵动五个系统
场景 1 里”给 TRS 加一个字段”的改动链(hedging-as 模型 → protobuf → migration → mapper → controller → 前端表单),表面是 6 个文件,但业务风险不在文件数,而在”漏改”:
漏了 migration → 字段没进 DB,运行时报列不存在,交易簿记直接失败
漏了 protobuf → 跨服务字段传丢,hedging-as 拿到默认值,MTM 算错
漏了 mapper → SQL 不认新列,查询静默返回旧值(最危险:不报错)
漏了 controller → 前端提交了但后端不接收,数据"消失"
漏了 前端表单 → 交易员根本没地方填这个字段
PM/BA 启示:
这类"跨 5 层加字段"的需求,测试成本 ≈ 改代码成本,
因为它要在每一层都验证"新字段真的端到端通了"。
一次漏改如果进了生产,后果是真实的资金数据错误
(呼应 34 文档:错误的日终/估值数据 = 真实的资金风险,不是 UI bug)。
所以看起来"加一个字段"的小需求,在 OTC 衍生品系统里
永远不该被估成"小"——它的爆炸半径横跨五个系统。
开发工作流总结
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 1. 读 │ │ 2. 写 │ │ 3. 构建 │ │ 4. 部署 │
│ 现有代码 │───▶│ 改代码 │───▶│ 本地测试 │───▶│ 到 beta │
│ │ │ │ │ │ │ → prd │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
读 — 在代码库里 grep 你在做的业务术语。“knockout” 会找到 ContractKnockInOut.java。“margin” 会找到 MarginCalculator.java。ODTS 类名很有描述性。
写 — 遵循现有模式。如果代码库用 ContractHelper 做构造逻辑,把改动放在那里,而不是另写一个工具类。
构建 — 每个项目都有 Gradle wrapper:
eds-web-app: ./gradlew build
hedging-as: ./gradlew build
new-edsweb: npm run build
odts-option-web: npm run build
部署 — 看 eds-web-app 里的 copy_restart_jar.sh,如果有 CI/CD 管线就用管线。
自测
- 发现 EOD 在第 5 步失败了,你怎么从第 5 步重新执行?
- 合约字段改了之后,需要改哪 5 个地方?(从 DB 到 UI)
- 价格不对,排查顺序是什么?
- 一个新品种上线需要改几个模块?估算时间。
现在带着这些知识回去重读 01-Origins-2014-2016.md,你会看懂的。