Files
zszq-trs/corp-action-refactor-proposal.md
T
hjhan 4e0a9d40be docs: 公司行为(corp action)处理能力分析 + 改造方案(登记日快照/份额类接入口)
含两份分析产物:
- corp-action-analysis.md: 现有 corp action 处理覆盖审计(双轨并行/缺统一模型/复权半成品/税务割裂)
- corp-action-refactor-proposal.md: 债券付息登记日快照修复 + 份额类(ex-date盘前因子)接入口盘点
均依后续对话澄清校准了 ex-date 开盘前生效/CAF 两种含义/三日期精确映射。
2026-08-09 10:24:34 +08:00

12 KiB
Raw Blame History

债券 TRS 公司行为(Corporate Action)改造方案

配套分析:corp-action-analysis.md(现状审计)。本文档给出可落地的改造路线

  • 改造一:债券付息「登记日快照」修复(当前唯一真 bug,独立低风险)。
  • 改造二:份额类公司行为(送股/配股/拆股)接入口盘点(当前完全缺失,改造前须先摸清落点)。

所有结论均基于源码实测,附 文件:行号


0. 现状一句话总结

维度 现状 性质
统一 CorporateAction 模型 无(全局搜 CorporateAction/CorporateActionType/ICorpActionHandler = 0 命中) 架构债
债券付息归属 唯一真 bug 是缺登记日快照(用支付日/期初持仓算票息) 当前要修的 bug
股票除权·份额类 ex_dividend_infoSwapModule 零引用,送股/配股永不生效 缺失
复权 adjust factor 仅单向 P/ratio、无累计因子、不持久化;HisPriceDividendAdjustEnum 死代码 残缺
税务 两套割裂内联公式,无通用模型 不健全

结论:「债券付息归属」这条链路上——ex-date 价格自动剔息(行情驱动)正确、pmt-date 现金划付正确、登记日前平仓不享息正确;唯一错在金额锚定的持仓快照日期(用了支付日/期初持仓,应为登记日收盘快照)。但整个公司行为处理体系并不都正确,份额类/复权/税务是独立的、尚未处理的缺口。


1. 改造一:债券付息「登记日快照」修复(最高优先级 · 低风险)

1.1 根因(已实证)

  • 模型缺字段BondPayment.csbond_payment_info)只有 payment_date_pl / payment_daterecord_date(债权登记日)/ ex_date(除息日)
  • 归属日期错BondPaymentService.GetBondPayments:101)用 payment_date 过滤事件;SwapEodPositionService.cs:1718 & 1821CalcBondPayment(..., eod.ValueDate, valueDate, curretEod.PosiQuantity) —— PosiQuantity 是支付日当天持仓,登记日→支付日间若有开平仓,归属算错。
  • 正确口径(三日期映射)
    • 登记日(record):归属截止——按登记日收盘持仓快照确认 dividendin / accruedCash,金额在此钉死;
    • 除息日(ex):价格自动剔息(现金票息仅价格变动、行情源给;份额类才需价格÷因子+数量×因子),不加 dividendin、无现金
    • 付息日(pmt):现金划付(swap_flow_event.PayDate),纯结算、不再重算归属
    • 注意:dividendin 不在 ex-date 加ex-date 与 pmt-date 通常也非同一天);无论收益腿确认日在登记日还是 ex-date,金额永远按登记日持仓这一定律不变。

1.2 数据模型改动

// BondPayment.cs (bond_payment_info)
public DateTime? record_date { get; set; }   // 债权登记日(归属截止)
public DateTime? ex_date { get; set; }        // 除息日(价格剔息)
// payment_date / payment_date_pl 保留(现金划付日)
  • 导入侧:BondPaymentController.cs 仅 CRUD,存什么由模型决定;上游聚源付息日历本就含这三个日期(表带 jsid/channel_source),加列 + 导入映射即可,低风险。

1.3 计算逻辑改动

落点 改动
SwapEodPositionService.cs:721 DealDividends PositionQty 改用 eod_swap_positionValueDate == record_date登记日快照(而非支付日 EOD
SwapEodPositionService.cs:1718 / :1821 CalcBondPayment 调用 传入的 qty 改为登记日快照数量
BondPaymentService.GetBondPayments:101 保留payment_date 筛事件(决定哪天触发现金);金额一律取登记日快照
收益腿确认日 本系统现状是登记日确认 dividendinGLMS 测试 3/2 当日 EventType=4),保持不变;无论确认日在登记日还是 ex-date,金额恒按登记日持仓

1.4 验证(回归测试护住)

  • 已有 GLMS20260105PartialCloseDividendBugTest:验「2/28 平 40% → 3/2 登记日不拿息、剩余 30M 拿 -54,240」。
  • 新增「登记日快照」测试:构造登记日→支付日间有开/平仓的交易(例如登记日后才买入、或登记日前已平),断言票息只给登记日收盘持有人,验证快照而非支付日持仓。
  • 复用既有 YLErp_UNIT_TEST_SKIP_INITIALIZATION=1 环境变量跑测(沙箱无 MySQL 亦可)。

1.5 影响面

仅修正债券票息归属日期(支付日/期初持仓 → 登记日收盘快照);以下本就正确、一律不动:ex-date 价格自动剔息(行情源开盘前重置、系统只取已除权价、不手动改价)、pmt-date 现金划付时机、登记日前平仓不享息、税务。


2. 改造二:份额类公司行为接入口盘点(送股/配股/拆股)

当前 SwapModuleex_dividend_info 零引用,股票除权的送股/配股字段在 TRS 里永不生效。以下是「若要接入」必须触碰的最小落点。

2.1 最大风险:数量只因平仓减少的硬假设

SwapEodPositionService.cs:1897 的 EOD 递推式被写死

var qty = eod.PosiQuantity + openQty - unwindQty;        // 仅开仓/平仓两项
curretEod.PosiQuantity = qty < 0 ? 0 : Math.Abs(qty);
// :1919 注释「平仓数量一定<持仓数量」把「数量单调不增」写进分支前提

送股使数量增加时,EOD 次日用旧 eod.PosiQuantity 直接结转 → 份额凭空丢失这是头号风险:数量入口不打通,TdChangedQty 永远为 0,后续成本均价/计息基准根本拿不到触发信号。

2.1.1 份额类 ex-date 时序规则(设计约束,避免二次缩放)

接入送股/配股/拆股时,必须遵循以下时序,否则盯市跳变:

  1. 因子只作用于"穿越 ex-date 的存量持仓"(上期收盘持有且未在 ex 前平仓);ex-date 当日新成交已在新尺度、不吃因子ex 前已平仓者不存在。
  2. ratio 在 ex-date 一次性写死(数量×ratio、成本÷ratio),之后该持仓永久带新数量/新成本,后续每天因子等价=1、无每日重算。
  3. 价格调整开盘前生效(交易所参考价在 ex-date 开盘前重置为除权价,全天成交已在新尺度)。因此持仓 ratio 调整必须作为 ex-date 当天 EOD 的第一步(盘前) 执行,绝不可收盘后补——否则当天 intraday 成交已被按新尺度记入、再对整仓 ratio 会二次缩放导致盈亏跳变。
  4. 系统只调持仓记录(数量/成本),价格取自市场已除权价,不手动改价(与现金票息的 ex-date 处理一致)。
  5. 复权价格序列的 CAF 另算:CAF(t)=1(t≥ex-date) / =ratio(t<ex-date),活在历史查询;与实时持仓的一次性 ratio 是两件事。

2.2 接入口清单(按口径分组)

口径 落点 送股/配股影响 改造最小文件
数量递推 SwapEodPositionService.cs:1897qty 递推)、:1723/:1905(无事件日结转)、:1631(首次归档) 需新增「公司行为数量」第三来源项 SwapEodPositionService.cs
TdChangedQty 定义 EodSwapPosition.cs:300DisplayName "当日公司行为数量");唯一赋值 SwapEodPositionService.cs:1650(恒=0 挂进 :1897 递推式(与 TdCloseQty:1942 对称),否则与 PosiQuantity 永久不自洽 SwapEodPositionService.cs
成本均价 SwapEodPositionService.cs:1926-1936(加权重算,TRS 无 CostPrice 字段,等价字段 PosiGrossPrice/PosiNetPrice 送股无成交金额(分子+0、分母增)→ 走 :1912 else if 分支价不摊薄,污染盯市;配股有现金需加 RationedSharesAmount×Price SwapEodPositionService.cs:1912-1937
计息基准 SwapDealService.cs:893 CalcNotionalByMode(五种模式分流)、:1372 dynomicPrincipal:2384 InterestPrincipalFix 仅平仓递减 posiLong/ShortPosiNotionalValue 可自动跟随;InterestPrincipalFix 是独立存量,与数量解耦,配股缴款需新写入点 SwapDealService.cs
盯市盈亏 SwapEodPositionService.cs:1657/1730/1815/20194 份同构副本 PosiMtmPnL)、:1713 GetSwapValuationPrice(取除权后价) 数量突变日若 PosiQuantity/PosiGrossPrice 未同步除权 → 虚假巨亏;4 处副本必须一致改 SwapEodPositionService.cs
数据源 DividendService.cs:752 GetPositionAmount(现成 amount*(1+GiveShareAmount/10) 送股调整)、:730 GetRatio(现成除权价公式) SwapModule 未复用,需建调用边 新增 SwapModule→DividendService 调用

2.3 最小改动集(新增"送股"一种行为)

4 文件 / 5 函数 + 1 条复用边

  1. SwapEodPositionService.csSetPriceInfoByFlowEvent:1897 qty 递推 + :1926 加权价 + :1650 TdChangedQty+ 债券结转 :1723
  2. SwapDealService.csUpdateInitalPosition:2346+ CalcNotionalByMode:893
  3. SwapTradeBaseService.cs — EOD→实时回写(:66/82/90
  4. 复用 DividendService.GetPositionAmount:752/ GetRatio:730)建数据边,无需重写除权公式

2.4 现有可复用资产(避免重写)

  • DividendService.GetPositionAmount:752):return amount * (1 + info.GiveShareAmount / 10.0); —— 现成送股份额调整。
  • DividendService.GetRatio(:730):完整除权价公式,已含送股+配股。
  • EodSettlementService.cs:47/112:场外期权 EOD 已用 GetPositionAmount 调持仓数量(唯一真正消费份额字段的模块,可作参考范式)。

3. 推荐演进路线(分阶段,增量可控)

阶段 内容 风险 依赖
0 改造一:债券付息登记日快照修复(加字段 + 改 DealDividends 数量来源 + 回归测试)
1 抽象统一 CorporateAction 实体(actionType + 四类日期 record/ex/effective/payment + factor/splitRatio),引入按 actionType 分发的 typed handler,替 EventReason="系统操作-分红" 魔法字符串 阶段0
2 份额类真正接入:打通数量入口(:1897)+ TdChangedQty + 成本均价(:1926+ 计息基准同步(:893/:2384)+ 4 处盯市副本一致改(:1657 等) 中高 阶段1
3 复权 adjust factor 持久化(累计因子表)+ 后复权实现(目前 HisPriceDividendAdjustEnum 死代码);GetRatio 去递归反查、补除零防护、去魔数 10 阶段1
4 税务组件化:统一 TaxComponent(按税收居民地/品种差异),替两套割裂内联公式 阶段1

阶段 0 可立即独立提交,不阻塞阶段 1–4;阶段 2 的最大难点是「数量只因平仓减少」假设,须优先打通数量入口。


4. 风险与注意

  1. 数量入口是总开关:不通 :1897 递推式,后续成本/计息全拿不到信号 → 阶段 2 先动数量。
  2. PosiMtmPnL 四处副本:1657/1730/1815/2019)必须一致改动,否则不同归档路径口径分裂、盯市跳变。
  3. InterestPrincipalFix 是独立存量(:2384 仅平仓递减),配股缴款若计入本金须新增写入点,不可假设随数量自动跟随。
  4. 改造须增量 + 回归测试护住:参考已建 SwapCalcTrace 常驻日志(关键 forensic 落盘)与 golden-record 测试(GLMS20260105PartialCloseDividendBugTest)范式,避免「测试覆盖不足时重构引入新 bug」。

5. 待确认 / 下一步

  • 阶段 0 是否现在落地(加字段 + 改 DealDividends + 补「登记日快照」回归测试)?
  • 阶段 2 是否借债券 ETF 设计契机一并启动(ETF 分红同需 record/ex/pmt 三日期)?
  • 税务(阶段 4)当前是否已有业务痛点,决定优先级?