搬迁(算法体逐字未动,仅换命名空间与归属): - SwapInterest.Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult → YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs - AccrualTrace → Accrual/AccrualTrace.cs(被迫同迁:其 MarkStart 引用 AccrualBoundary, Core 不能反向依赖 DAL) - 引用切换:Simple/CompoundInterestAccrual、AccrualPolicy、SwapCalcTrace、SwapDealService (保留 using YLErp.Derivatives.Interest——IIndexFixer/IndexFixerBase 留 Core) 删除(零生产引用,孤儿清零): - Core:SwapInterest.cs 算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/ AccrueUnrealized/ToInterestRate,未接线且与 DAL 生产实现舍入/rollover 口径已分叉)、 AccrualContext.cs、InterestRate.cs - DAL:AccrualState.cs(零引用死类) - 测试:SwapInterest_CompoundInArrears_RolloverTimingTests.cs(仅测已删原语) 验证:两解决方案 Rebuild 0 错误;磁盘 SwapInterest. 残留 0;影子/分红/场景 86/86 通过 (含 Accrual 3 影子对账、Margin 影子、divPower 新增 AutoUnwindMultiPartial)。 注:AccrualContext 默认精度 11 与生产 12 的分叉隐患随删除一并消除; 已删原语若将来重建须先补对账测试,勿凭记忆复原(ARCHITECTURE.md 已留警告)。
16 KiB
16 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 |
| 计息基准 | mode 分流已重构为 FundingLegs/FundingLegStrategyFactory(原 SwapDealService CalcNotionalByMode 已删;dynomicPrincipal 亦随重构消失)、InterestPrincipalFix 仅平仓递减 |
posiLong/Short 经 PosiNotionalValue 可自动跟随;InterestPrincipalFix 是独立存量,与数量解耦,配股缴款需新写入点 |
SwapDealService.cs + FundingLegs/ |
| 盯市盈亏 | 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」。
4.1 阶段1 落地形态建议:独立 CorporateActions 模块(新增建议 · 待深度验证与评审)
⚠️ 以下为架构建议草案,尚未经过逐文件源码复核与评审。仅作方向性参考,落地前须:
- 逐文件确认现有
DividendService/BondPaymentService/ex_dividend_info的调用边,避免重复造轮子;- 确认
YLErp.Core是否合适承载(须被 OMS / 期权 / TRS 多程序集引用,不能反向依赖业务层);- 与**利息核心(复利/部分平仓)**明确划界——见下方"边界警示"。
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<ex)
EodHooks/ # EOD 收盘链上的接入口(与现有 SwapEodPositionService 解耦的薄适配层)
IRecordDateHandler.cs # 登记日收盘:按快照确认 dividendin/accruedCash(金额钉死)
IExDateHandler.cs # 除息日:价格自动剔息 + 穿越持仓因子(开盘前第一步,绝不可收盘后补)
IPaymentDateHandler.cs # 支付日:纯现金划付,不重算归属
4.1.3 边界警示(重要,避免域混淆)
- 债券 ETF 分红 / 付息 = 公司行为域 → 走
CorporateActions模块。 - 复利 / 部分平仓 / T+1 本金继承 / 重置日动态本金 = 利息计息域,现居
SwapDealService.cs(InitSwapDealInterest/CalcDailyCompoundInterest/CalcUnwindInterest)+SwapEodPositionService.cs(SaveAutoEodWithCloseInterestPosition)。这是另一回事,与"公司行为"正交:- 付息(coupon)的金额由 corp action 决定(按登记日快照);
- 付息的利息滚存/复利/部分平仓结算由利息核心决定。
- 两者通过
swap_flow_event(付息流水)衔接,不要在 corp action 模块里实现复利逻辑,也不要在利息核心里硬编码某种公司行为的日期语义。
- 当前
_0808分支的 4 个复利部分平仓修复(见分支对比分析)属于利息核心,与本模块无关;合并时利息核心以_0808为准,corp action 模块独立演进。
5. 待确认 / 下一步
- 阶段 0 是否现在落地(加字段 + 改
DealDividends+ 补「登记日快照」回归测试)? - 阶段 2 是否借债券 ETF 设计契机一并启动(ETF 分红同需 record/ex/pmt 三日期)?
- 税务(阶段 4)当前是否已有业务痛点,决定优先级?