EOD 批处理:每天 8 小时的生死时速
日终批处理是 OTC 交易系统的"心跳"——它把 5000+ 活跃交易从 T 日状态推进到 T+1 状态,每一步失败都可能引发连锁反应。本课拆解 10 个步骤、幂等性设计原则与真实故障案例。
execute() 执行业务逻辑、isIdempotent() 声明是否可以安全重跑,共同构成 EOD 可靠性的基石。一、为什么 EOD 是系统的”心跳”
OTC 交易系统白天处理实时交易,EOD(End of Day)批处理负责把全量持仓和敞口推进到下一天。17:00 市场收盘后,系统开始以批处理方式完成估值、清算、报告等全部”幕后”工作。
EOD 失败的后果是系统性的:
- 交易台在次日早上看不到最新 MTM,继续按旧数据交易——盲目交易
- 追保通知延迟,客户错过追保窗口——违约风险上升
- SAC 报送数据截止时间为当日 17:00,EOD 延迟直接导致报送超时
系统内有 5000+ 活跃交易(TRS、雪球、期权混合),每笔交易在 EOD 中都要经历从市场数据刷新到资金划转的完整链路。正常情况下 3 小时内完成,若某一步卡住,整个链路推迟。
系统锚点(IT 侧)
hedging-as/src/com/cicc/eod/
EodScheduler.java — 调度器(Spring @Scheduled,cron “0 0 17 * * *”)
EodStep.java — 步骤接口:execute(ctx) / isIdempotent()
EodEngine.java — 执行引擎(顺序遍历步骤,处理失败/重跑)
MarginStep → MarginCallProcessor → 追保通知。EOD 第 8 步资金划转 → CashTransferService → IPMP 划转指令。EOD 是 0012 追保与结算的触发引擎。二、10 步详解:每个故障点都要知道
以下是 EOD 完整 10 步的耗时与失败后果——这是运维和开发都须烂熟于心的清单:
| # | 步骤名称 | 预计耗时 | 失败后果 |
|---|---|---|---|
| 1 | 市场数据刷新 | 5 min | 用昨天数据代替,并在状态中标记”待确认”;估值仍跑但结果带警告旗 |
| 2 | 定盘 Fixing | 15 min | 用前收盘价替代,估值结果状态置为”待审”;LIBOR/Shibor 失效时最常见 |
| 3 | 公司行为 | 10 min | 人工介入比例最高;漏掉分红/送股 → TRS 分红算错、簿记得不平 |
| 4 | 障碍检查 | 15 min | 敲出/敲入判定漏检 → 票息错误(触发后仍按未触发算 → 多付票息,或反过来) |
| 5 | 估值 Valuation(MTM) | 30 min | MTM 算错 → 全台收到错误信号;交易员对冲决策失误、风险限额报表失真 |
| 6 | Greeks 计算 | 20 min | DV01 错 → 限额监控失灵;Delta/Gamma 错 → 对冲建议误导交易员 |
| 7 | 保证金 | 20 min | IM/VM 算错 → 追保金额错误(多追 → 客户投诉;少追 → 风险敞口暴露) |
| 8 | 资金划转 | 10 min | 失败 → 次日资金无法按时到账;客户收到追保但付款指令未发出 |
| 9 | 报告生成 | 10 min | SAC 报告缺失 / 客户对账单无法发出;错过监管报送窗口则触发违规记录 |
| 10 | 清理 | 5 min | EOD 状态归档、日志转储、临时表清理;未清理会导致次日 EOD 状态不一致 |
三、幂等性:失败后能重跑才是好设计
EodStep 接口定义了两个方法:
public interface EodStep {
EodResult execute(EodContext ctx); // 执行业务逻辑
boolean isIdempotent(); // 声明是否可以安全重跑
}
isIdempotent() 是 EOD 可靠性的核心设计理念:当某一步失败后,运维需要决定是否重跑。如果该步骤幂等,重跑不会产生副作用;如果非幂等,重跑可能导致重复写入(如重复扣款、重复记录)。
幂等性的实现方式是在写入前先检查记录是否存在(UPSERT 模式):
-- 非幂等写法(重跑会插入重复记录):
INSERT INTO eod_step_log (trade_id, step_id, status)
VALUES (?, ?, 'DONE');
-- 幂等写法(重跑安全):
INSERT INTO eod_step_log (trade_id, step_id, status)
VALUES (?, ?, 'DONE')
ON CONFLICT (trade_id, step_id) DO UPDATE SET status='DONE', run_at=NOW();
GreeksStep.isIdempotent() = true(Greeks 记录用 trade_id 作为唯一键,UPSERT 写入),直接重跑第 6 步,15 分钟后完成,全天 EOD 在 23:00 收口。如果当时选择清空重来,预计次日 04:00 才能完成,影响次日 08:00 的交易启动。四、真实故障案例:两次中断的根因分析
案例一:2019 remark 字段变更截断(中断 4 小时)
数据库从 VARCHAR2(500) 扩展到 VARCHAR2(1000),某条交易记录的 remark 字段超过 500 字符但不足 1000。旧 SQL 在 INSERT 时用 SUBSTR(remark, 1, 500) 截断,但新表结构要求完整写入 1000 字符。截断后的数据拼接了错误的分隔符,导致 EOD 第 5 步估值引擎在解析该记录时报 StringIndexOutOfBoundsException,估值步骤直接崩溃。
运维起初以为是数据库连接问题,重跑了 3 次均失败。根因排查花了 2 小时,最终定位到那条超长 remark 记录,手工修正后 EOD 才跑通。
SUBSTR / 固定长度数组)不会因为底层表结构改了而自动升级。上线前必须做数据迁移脚本,把旧截断数据补齐,同时升级 SQL 代码去掉截断逻辑。案例二:2022 年市场数据同步延迟(EOD 整体延迟到次日 01:00)
当日 16:57 市场数据供应商的系统出现抖动,部分行情数据晚到 3 分钟。EOD 第 1 步市场数据刷新在 17:00 启动时,对应的价格数据尚未到达。
系统的容错设计是”超时等待 5 分钟”,但那天数据晚到 8 分钟。第 1 步在 17:05 判定”数据不可用”后,用了前一日的收盘价代替,并打上”待确认”标记。后续所有步骤均按此标记继续执行,但到第 9 步报告生成时,运营发现 MTM 数据中有 23 笔交易的价格仍是昨日数据——这批交易当天实际发生了大额盈亏,报告数字与真实情况严重不符。
SAC 报送因此推迟到次日 01:00 完成,触发了监管问询。
五、并行化优化:从 3 小时到 2 小时
当前 EOD 是完全串行执行——每一步完成后才启动下一步。这在交易量小的时候没问题,但 5000+ 活跃交易让第 5 步估值和第 6 步 Greeks 成为明显瓶颈。
以下步骤可以安全并行化(不同标的/产品线互不依赖):
- 障碍检查(第 4 步):按标的分组并行,不同标的的 KO/KI 状态互不影响
- Greeks 计算(第 6 步):按产品线并行(TRS 一起算、雪球一起算、期权一起算),不同产品线共享相同的风险因子结果但计算过程独立
-- 串行(当前):
for step in [MD_REFRESH, FIXING, CORP_ACTION, BARRIER_CHECK, VALUATION, GREEKS, ...]:
step.execute()
-- 并行优化后(障碍检查 + Greeks 可并行):
parallel([
VALUATION, -- 必须串行(依赖前一步)
GREEKS_BY_PRODUCT, -- 并行:TRS / 雪球 / 期权各自跑
BARRIER_BY_UNDERLYING -- 并行:按标的分组
])
优化目标是总耗时从 3 小时压缩到 2 小时,为数据异常预留更多缓冲时间。
六、测验(Recall 练习)
EOD 批处理从每天几点开始?
公司行为步骤(第 3 步)失败的主要风险是?
EodStep 接口中的 isIdempotent() 是什么意思?
2019 年 EOD 中断 4 小时的根因是?
障碍检查(第 4 步)漏检的后果是?
EOD 失败后第一步应该做什么?
Greeks 步骤(第 6 步)算错 DV01 会导致什么?
资金划转(第 8 步)失败会导致?
七、错题本与复习
本课错题记录
八、延伸阅读
- 内部:ODTS-16 追保 IM/VM —— EOD 第 7 步保证金与追保通知的衔接逻辑。
- 内部:ODTS-14 结算与资金流转 —— EOD 第 8 步资金划转的代码落点。
- 串联:0012 追保与结算 · 0016 EOD 总览 · 0011 错题本
Lesson 0017 · EOD 批处理 · 上游 0016 EOD 总览 · 错题追踪 0011 错题本 · MISSION: 产品 + IT 双线精通 · 样式 base.css