# 债券 TRS 公司行为(Corporate Action)改造方案 > 配套分析:`corp-action-analysis.md`(现状审计)。本文档给出**可落地的改造路线**: > - 改造一:**债券付息「登记日快照」修复**(当前唯一真 bug,独立低风险)。 > - 改造二:**份额类公司行为(送股/配股/拆股)接入口盘点**(当前完全缺失,改造前须先摸清落点)。 > > 所有结论均基于源码实测,附 `文件:行号`。 --- ## 0. 现状一句话总结 | 维度 | 现状 | 性质 | |---|---|---| | 统一 CorporateAction 模型 | 无(全局搜 `CorporateAction`/`CorporateActionType`/`ICorpActionHandler` = 0 命中) | 架构债 | | 债券付息归属 | **唯一真 bug 是缺登记日快照**(用支付日/期初持仓算票息) | **当前要修的 bug** | | 股票除权·份额类 | `ex_dividend_info` 在 `SwapModule` **零引用**,送股/配股永不生效 | 缺失 | | 复权 adjust factor | 仅单向 `P/ratio`、无累计因子、不持久化;`HisPriceDividendAdjustEnum` 死代码 | 残缺 | | 税务 | 两套割裂内联公式,无通用模型 | 不健全 | **结论**:「债券付息归属」这条链路上——**ex-date 价格自动剔息(行情驱动)正确、pmt-date 现金划付正确、登记日前平仓不享息正确;唯一错在金额锚定的持仓快照日期(用了支付日/期初持仓,应为登记日收盘快照)**。但**整个公司行为处理体系并不都正确**,份额类/复权/税务是独立的、尚未处理的缺口。 --- ## 1. 改造一:债券付息「登记日快照」修复(最高优先级 · 低风险) ### 1.1 根因(已实证) - **模型缺字段**:`BondPayment.cs`(`bond_payment_info`)只有 `payment_date_pl` / `payment_date`,**无 `record_date`(债权登记日)/ `ex_date`(除息日)**。 - **归属日期错**:`BondPaymentService.GetBondPayments`(:101)用 `payment_date` 过滤事件;`SwapEodPositionService.cs:1718 & 1821` 调 `CalcBondPayment(..., 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 数据模型改动 ```csharp // 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_position` 中 `ValueDate == record_date` 的**登记日快照**(而非支付日 EOD) | | `SwapEodPositionService.cs:1718 / :1821` `CalcBondPayment` 调用 | 传入的 qty 改为登记日快照数量 | | `BondPaymentService.GetBondPayments`(:101) | **保留**按 `payment_date` 筛事件(决定哪天触发现金);金额一律取登记日快照 | | 收益腿确认日 | 本系统现状是**登记日**确认 dividendin(GLMS 测试 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. 改造二:份额类公司行为接入口盘点(送股/配股/拆股) > 当前 `SwapModule` 对 `ex_dividend_info` **零引用**,股票除权的送股/配股字段在 TRS 里永不生效。以下是「若要接入」必须触碰的最小落点。 ### 2.1 最大风险:数量只因平仓减少的硬假设 `SwapEodPositionService.cs:1897` 的 EOD 递推式被**写死**: ```csharp 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 **阶段 0 可立即独立提交**,不阻塞阶段 1–4;阶段 2 的最大难点是「数量只因平仓减少」假设,须优先打通数量入口。 --- ## 4. 风险与注意 1. **数量入口是总开关**:不通 `:1897` 递推式,后续成本/计息全拿不到信号 → 阶段 2 先动数量。 2. **`PosiMtmPnL` 四处副本**(:1657/1730/1815/2019)必须一致改动,否则不同归档路径口径分裂、盯市跳变。 3. **`InterestPrincipalFix` 是独立存量**(:2384 仅平仓递减),配股缴款若计入本金须新增写入点,不可假设随数量自动跟随。 4. **改造须增量 + 回归测试护住**:参考已建 `SwapCalcTrace` 常驻日志(关键 forensic 落盘)与 golden-record 测试(`GLMS20260105PartialCloseDividendBugTest`)范式,避免「测试覆盖不足时重构引入新 bug」。 --- ## 4.1 阶段1 落地形态建议:独立 `CorporateActions` 模块(新增建议 · **待深度验证与评审**) > ⚠️ 以下为**架构建议草案**,尚未经过逐文件源码复核与评审。仅作方向性参考,落地前须: > 1. 逐文件确认现有 `DividendService` / `BondPaymentService` / `ex_dividend_info` 的调用边,避免重复造轮子; > 2. 确认 `YLErp.Core` 是否合适承载(须被 OMS / 期权 / TRS 多程序集引用,不能反向依赖业务层); > 3. 与**利息核心(复利/部分平仓)**明确划界——见下方"边界警示"。 ### 4.1.1 为什么必须新模块,而不是往现有屎山堆 - 现有 `SwapDealService` / `SwapEodPositionService` 已高度耦合(计息、平仓、EOD 递推、公司行为全搅在一起),继续往里加 if/else 只会放大"隐式不变量跨函数跨日不可见"的风险(这正是"测试绿但全错"的温床)。 - 公司行为域(除权除息 / 复权 / 付息 / 分红 / 拆股 / 送股 / 配股)在 **OMS、期权、TRS** 多个业务都要用,**必须抽到共享核心程序集**,各业务只消费、不各写一份。 ### 4.1.2 推荐目录形态 ``` YLErp.Core / CorporateActions/ ← 共享核心,被 OMS/期权/TRS 引用,不反向依赖业务层 CorporateAction.cs # 统一实体:actionType + record/ex/effective/payment 四日期 + factor/splitRatio + 金额 ICorporateActionSource.cs # 上游数据源适配(聚源付息日历已含全日期,仅做映射) ActionType.cs # 枚举:CashDividend / StockDividend / Split / BondCoupon / RightsIssue / Merger ... handlers/ # 每种行为一个 typed handler(新增行为 = 加类,不动旧代码) CashDividendHandler.cs # 现金分红(含债券 ETF 分红) BondCouponHandler.cs # 债券付息(record 日快照归属) SplitHandler.cs # 拆股 StockDividendHandler.cs # 送股 RightsIssueHandler.cs # 配股 IAdjustmentFactorProvider.cs # 前复权 / 后复权 / 累计 CAF(t)=1(t≥ex)/=ratio(t