含两份分析产物: - corp-action-analysis.md: 现有 corp action 处理覆盖审计(双轨并行/缺统一模型/复权半成品/税务割裂) - corp-action-refactor-proposal.md: 债券付息登记日快照修复 + 份额类(ex-date盘前因子)接入口盘点 均依后续对话澄清校准了 ex-date 开盘前生效/CAF 两种含义/三日期精确映射。
12 KiB
12 KiB
债券 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 数据模型改动
// 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 递推式被写死:
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 时序规则(设计约束,避免二次缩放)
接入送股/配股/拆股时,必须遵循以下时序,否则盯市跳变:
- 因子只作用于"穿越 ex-date 的存量持仓"(上期收盘持有且未在 ex 前平仓);ex-date 当日新成交已在新尺度、不吃因子;ex 前已平仓者不存在。
- ratio 在 ex-date 一次性写死(数量×ratio、成本÷ratio),之后该持仓永久带新数量/新成本,后续每天因子等价=1、无每日重算。
- 价格调整开盘前生效(交易所参考价在 ex-date 开盘前重置为除权价,全天成交已在新尺度)。因此持仓 ratio 调整必须作为 ex-date 当天 EOD 的第一步(盘前) 执行,绝不可收盘后补——否则当天 intraday 成交已被按新尺度记入、再对整仓 ratio 会二次缩放导致盈亏跳变。
- 系统只调持仓记录(数量/成本),价格取自市场已除权价,不手动改价(与现金票息的 ex-date 处理一致)。
- 复权价格序列的 CAF 另算:
CAF(t)=1(t≥ex-date) /=ratio(t<ex-date),活在历史查询;与实时持仓的一次性 ratio 是两件事。
2.2 接入口清单(按口径分组)
| 口径 | 落点 | 送股/配股影响 | 改造最小文件 |
|---|---|---|---|
| 数量递推 | SwapEodPositionService.cs:1897(qty 递推)、:1723/:1905(无事件日结转)、:1631(首次归档) |
需新增「公司行为数量」第三来源项 | SwapEodPositionService.cs |
TdChangedQty |
定义 EodSwapPosition.cs:300(DisplayName "当日公司行为数量");唯一赋值 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/Short 经 PosiNotionalValue 可自动跟随;InterestPrincipalFix 是独立存量,与数量解耦,配股缴款需新写入点 |
SwapDealService.cs |
| 盯市盈亏 | SwapEodPositionService.cs:1657/1730/1815/2019(4 份同构副本 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 条复用边:
SwapEodPositionService.cs—SetPriceInfoByFlowEvent(:1897 qty 递推 + :1926 加权价 + :1650TdChangedQty)+ 债券结转 :1723SwapDealService.cs—UpdateInitalPosition(:2346)+CalcNotionalByMode(:893)SwapTradeBaseService.cs— EOD→实时回写(:66/82/90)- 复用
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. 风险与注意
- 数量入口是总开关:不通
:1897递推式,后续成本/计息全拿不到信号 → 阶段 2 先动数量。 PosiMtmPnL四处副本(:1657/1730/1815/2019)必须一致改动,否则不同归档路径口径分裂、盯市跳变。InterestPrincipalFix是独立存量(:2384 仅平仓递减),配股缴款若计入本金须新增写入点,不可假设随数量自动跟随。- 改造须增量 + 回归测试护住:参考已建
SwapCalcTrace常驻日志(关键 forensic 落盘)与 golden-record 测试(GLMS20260105PartialCloseDividendBugTest)范式,避免「测试覆盖不足时重构引入新 bug」。
5. 待确认 / 下一步
- 阶段 0 是否现在落地(加字段 + 改
DealDividends+ 补「登记日快照」回归测试)? - 阶段 2 是否借债券 ETF 设计契机一并启动(ETF 分红同需 record/ex/pmt 三日期)?
- 税务(阶段 4)当前是否已有业务痛点,决定优先级?