docs: 公司行为(corp action)处理能力分析 + 改造方案(登记日快照/份额类接入口)
含两份分析产物: - corp-action-analysis.md: 现有 corp action 处理覆盖审计(双轨并行/缺统一模型/复权半成品/税务割裂) - corp-action-refactor-proposal.md: 债券付息登记日快照修复 + 份额类(ex-date盘前因子)接入口盘点 均依后续对话澄清校准了 ex-date 开盘前生效/CAF 两种含义/三日期精确映射。
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# zszq-trs 公司行为(Corporate Action)处理能力分析报告
|
||||
|
||||
> 分析范围:`/Users/hjhan/code/zszq-trs`(C# / ASP.NET Core net6.0,债券 TRS 期间结算系统 + 场外期权定价)
|
||||
> 分析日期:2026-08-09
|
||||
> 方法:静态代码考古 + 多子代理并行探索(`ex_dividend_info` / `EodPriceProvider` / EOD 收盘链 / 债券付息 / 设计模式)
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论速览(TL;DR)
|
||||
|
||||
| 维度 | 现状 | 评级 |
|
||||
|---|---|---|
|
||||
| 统一 CorporateAction 领域模型 | **无**(全局搜索 `CorporateAction`/`CorporateActionType`/`ICorpActionHandler` = 0 命中) | ❌ |
|
||||
| 复权(前/后复权) | 仅单向 `P/ratio`(前复权方向);`HisPriceDividendAdjustEnum` 前后复权枚举**零引用(死代码)** | ⚠️ |
|
||||
| 调整因子 adjust factor | 无独立模型、不持久化,每次 `GetRatio` 现算 | ⚠️ |
|
||||
| 现金分红(股票) | `ex_dividend_info` 有字段 + `DividendService` 公式,但**仅服务期权/结算,TRS 未接** | 🟡 |
|
||||
| 送股 / 配股 / 拆股(份额类) | `ex_dividend_info` 有字段,但**全系统仅期权侧数量调整,TRS 完全缺失** | ❌ |
|
||||
| 债券付息 | `bond_payment_info` + 自动互换事件**机制已实现**,但**归属日期错(缺登记日快照、用支付日/期初持仓算票息)** | 🟡 |
|
||||
| 税务 | 两套割裂内联公式(增值税 + 分红税),无通用模型 | ⚠️ |
|
||||
| 日期语义(record/ex/effective/payment) | 仅 payment date(债券)与"股权登记日"(股票),**无 record/ex/effective 独立字段** | ❌ |
|
||||
| 处理时机 | **日终收盘消费**("重新收盘"提示 + "已被执行"状态),无日中实时处理 | 🟡 |
|
||||
| 设计模式 | 每种行为一张表 + 一段 if;行为靠魔法字符串 `EventReason`;无策略/工厂/事件总线 | ❌ |
|
||||
|
||||
**一句话结论**:系统对"公司行为"是**零散、双轨、而非领域化**的实现——债券付息走一套、股票除权走另一套、彼此不相通;TRS 仅实现了现金类的"债券票息→现金流",份额类行为(拆股/送股)在 TRS 侧完全是空位(`EodSwapPosition.TdChangedQty` 是预留却从未接线的死字段)。**没有统一的 CorpAction 抽象,新增一种行为成本高、易引入回归。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据模型与数据库表
|
||||
|
||||
### 1.1 股票除权除息表 `ex_dividend_info`
|
||||
`YLErpDAL/Model/ExDividendInfo.cs:8-61`
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `ExDividendDate` | 命名像"除权日",但 `DisplayName` 是**"股权登记日"**(差一日,易 T/T-1 错配) |
|
||||
| `GiveCashAmount` | 派息金额(现金分红) |
|
||||
| `GiveShareAmount` | 送股股数 |
|
||||
| `RationedSharesAmount` / `RationedSharesPrice` | 配股股数 / 配股价 |
|
||||
| `ValidStatus` | 是否有效(软删) |
|
||||
| `UnderlyingId` / `UnderlyingCode` | 标的 |
|
||||
| `DividRate` | 税率,但 `[NotMapped]`——**不持久化** |
|
||||
|
||||
⚠️ **没有 record date / ex-date / effective date / payment date 的独立字段**,只有一个语义含糊的 `ExDividendDate`。
|
||||
|
||||
### 1.2 债券付息表 `bond_payment_info`
|
||||
`Framework/YLErp.Core/DBModels/BondPayment.cs:15`
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `pay_date_act` | 实际付息日 |
|
||||
| `paying_interest` | 每百元利息 |
|
||||
| `paying_principal` | 每百元本金(含本付息) |
|
||||
|
||||
⚠️ **也只有付息日,无登记日/除权日**(注:缺的是模型层字段,上游聚源付息日历本就含债权登记日/除息日,加列 + 导入映射即可,非数据源问题)。
|
||||
|
||||
### 1.3 缺失:统一 CorporateAction 模型
|
||||
全仓**无任何 `CorporateAction` 表 / 类型枚举 / 状态机**。两种行为各用一张独立表,且字段语义不一致(股票用"股权登记日",债券用"付息日")。
|
||||
|
||||
---
|
||||
|
||||
## 2. 复权(前复权/后复权)与调整因子 adjust factor
|
||||
|
||||
### 2.1 实现现状
|
||||
`YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs:235-261`
|
||||
|
||||
```csharp
|
||||
var dividends = dividendService.GetExDividends(valueDate, unids).ToArray();
|
||||
foreach (var ep in stockQuery) {
|
||||
var dividend = dividends.FirstOrDefault(n => n.UnderlyingId == ep.UnderlyingId);
|
||||
if (dividend != null)
|
||||
ep.ClosePrice = ep.SettlePrice = dividendService.GetPrice(ep.ClosePrice, dividend);
|
||||
}
|
||||
```
|
||||
|
||||
- `GetExDividends`(`DividendService.cs:768-776`)用 `ExDividendDate == valueDate` **精确等值**过滤——**只调当日一条,无历史遍历**。
|
||||
- 调整方向:`price / ratio`(前复权方向,向下调)。
|
||||
- `ratio` 计算(`DividendService.cs:710-732`,标准 A 股除权参考价公式):
|
||||
|
||||
```
|
||||
ratio = closePrice / cDivdPrice
|
||||
cDivdPrice = (closePrice*10 - GiveCashAmount*(1-dividendRate) + RationedSharesAmount*RationedSharesPrice)
|
||||
/ (10 + GiveShareAmount + RationedSharesAmount)
|
||||
调整价 = price / ratio
|
||||
```
|
||||
- 枚举 `HisPriceDividendAdjustEnum.cs:6-19` 定义了 `None / Forward(前复权) / Backward(后复权)`,**全仓库零引用**(死代码)——后复权从未实现。
|
||||
|
||||
### 2.2 缺陷
|
||||
1. **无累计复权因子、无历史序列复权**:`ExDividendDate == valueDate` 只调当日,跨多个除权事件的历史价格序列不连续;业界通行做法是持久化累计因子形成连续序列。
|
||||
2. **`GetRatio` 内部又 `new EodPriceProvider(info.ExDividendDate)` 反查收盘价**(`DividendService.cs:728`)——递归/重复查询,潜在开销。
|
||||
3. **无除零防护**:`cDivdPrice == 0` 或 `closePrice == 0` 时得 `Infinity/NaN`。
|
||||
4. **魔数 `10` 硬编码**(每 10 股基准),公式可读性差、不可配置。
|
||||
5. `Math.Round(...,4)` 反复截断,多次调整累积舍入误差。
|
||||
|
||||
> **「因子」的两种含义(易混,务必分清)**:
|
||||
> ① **实时持仓的 ratio**:在 ex-date **一次性**把 `PosiQuantity ×ratio`、`PosiGrossPrice ÷ratio` 写死进持仓记录,之后该持仓永久带新数量/新成本,**后续每天不再有因子(等价=1)、无每日重算**;因子只作用于"穿越 ex-date 的存量持仓",ex-date 当日新成交已在新尺度、不吃因子。
|
||||
> ② **复权价格序列的 CAF**:是证券历史价格的属性,`CAF(t) = ∏(发生在 t 之后的所有公司行为的 ratio)`;**t ≥ ex-date 时 =1**(这些价格本就除权后尺度),**t < ex-date 时 = ratio**(把老价格抬到新尺度可比)。CAF 活在历史、活在查询时,不是天天跑。
|
||||
> 本系统当前的「复权」只实现了 ② 的单向(前复权)且每次现算、不持久化;① 完全缺失(TRS 从未做份额类调整)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分红 / 送股 / 配股的处理
|
||||
|
||||
### 3.1 字段覆盖
|
||||
`ex_dividend_info` 表**已含** 派息/送股/配股字段,`DividendService` 也有对应数量调整逻辑(`GetPositionAmount` 在 `DividendService.cs:754` 做 `amount * (1 + GiveShareAmount/10)`)。
|
||||
|
||||
### 3.2 实际接线(关键发现)
|
||||
- `ex_dividend_info` / `DividendService` 的**主要消费方**是 EOD 价格复权(`EodPriceProvider.cs:251`)、期权 Fixing(`FixingService.cs:180`)、期权结算时 `DividendService.Execute` 对 trade 的 Strike/Barrier/SpotPrice 做同比率调整。
|
||||
- **`YLErpDAL/Modules/SwapModule` 对 `ex_dividend_info` / `DividendService` 零引用** —— 即 TRS 模块完全没有接入股票的送股/配股/分红份额调整。
|
||||
|
||||
> 这意味着:即便股票除权表"看起来"支持送股配股,TRS 持仓也**永远不会**因送股而增加股数、因配股而改变成本价。
|
||||
|
||||
---
|
||||
|
||||
## 4. 债券付息处理
|
||||
|
||||
### 4.1 数据流
|
||||
`SwapPositionCompose`(`SwapEodPositionService.cs:358`)日终编排:`DealInterests` → `DealAutoInterests` → `DealDividends`(`:428/:690`)。
|
||||
`DealDividends` 触发条件 `PosiDividendSum != 0`(`:693`),复用"分红"通道,最终汇入 `SaveAutoSwapDeal`(`:771`):
|
||||
|
||||
```csharp
|
||||
EventType = (int)SwapEventTypeEnum.自动互换,
|
||||
EventReason = "系统操作-分红",
|
||||
PayDate = dividendPayDate,
|
||||
DividendIn = eodPosi.PosiDividendSum,
|
||||
```
|
||||
→ `AddClientCash(...)` 写资金流(`:782/:789/:799`)。
|
||||
|
||||
金额来源:`BondPaymentService.CalcPayment`(`BondPaymentService.cs:122-140`)按 `payment_date ∈ (startDate, endDate]` 取票息 × 持仓数量 ÷ 100,再扣增值税 `payment/(1+tax)*(1-tax)`。与测试 oracle 一致(`BondTrsAutoSwapScenarioTest.cs:284`:5000万×0.1808/100 = 90,400)。
|
||||
|
||||
### 4.2 "按登记日持仓算"的真相
|
||||
测试中"部分平仓剩 60% → 54,240"看似是按登记日持仓缩放,**实为按付息日当日持仓数量线性缩放**——`PosiDividendSum` 按"前日 + 当日新计 - 当日实现"逐日递推(`:1837`),当日新计用**当日 EOD 持仓** `curretEod.PosiQuantity`(`:1821`)。系统**并未真正建模 record date**。
|
||||
|
||||
---
|
||||
|
||||
## 5. 税务处理
|
||||
|
||||
无通用税务模型,两套割裂机制:
|
||||
- **增值税**:`underlying_manager.ValueAddedTax` 内联公式 `payment/(1+tax)*(1-tax)`,散布于 `SwapEodPositionService.cs:1715/1720/1809/2010`、`SwapDealService.cs:1750`。
|
||||
- **分红税**:`Valuedate.DividendRate`(`Valuedate.cs:341-343`)全局**单一税率**,仅用于 `DividendService.cs:728`。
|
||||
|
||||
无 withholding/代扣实体、无税种枚举、无按客户/税收居民地的差异化。
|
||||
|
||||
---
|
||||
|
||||
## 6. 各类日期语义(record / ex / effective / payment)
|
||||
|
||||
| 语义 | 系统中表示 | 位置 |
|
||||
|---|---|---|
|
||||
| payment date | `BondPayment.pay_date_act` / `swap_flow_event.PayDate` | `BondPayment.cs:67` / `SwapFlowEvent.cs:354` |
|
||||
| 派息资金偏移 | `trade_extend.ExtendObj.DividendPayDate`(0=到期结算日/1=派息日+0/2=+1/3=+2) | `TradeExtend.cs:102-105`,换算 `SwapEodPositionService.cs:696-700` |
|
||||
| 股权登记日 | `ex_dividend_info.ExDividendDate`(命名歧义) | `ExDividendInfo.cs:17` |
|
||||
| record date | **无字段**(测试里 `RegDate` 只是本地常量,`BondTrsAutoSwapScenarioTest.cs:52`) | — |
|
||||
| ex-date / effective date | **无** | — |
|
||||
|
||||
---
|
||||
|
||||
## 7. 日中 vs 日终处理时机
|
||||
|
||||
- **日终收盘消费**:`ex_dividend_infoController.saveDividend`(`:57-78`) 保存后检查"该股权登记日后了结的交易"→ 提示**"请务必重新收盘"**;`deleteDividend`(`:87`) 检查 `checkDividendInfoExecuteStatus`→"该条除权信息已被执行,不允许删除"——证实除权信息是**日终/收盘时应用**的。
|
||||
- TRS 侧:`SwapPositionCompose` 日终编排,`DealDividends` 在 `PosiDividendSum != 0` 时触发自动互换。
|
||||
- 价格侧:`EodPriceProvider` 取价时按 `ExDividendDate == valueDate` 调当日。
|
||||
- ⚠️ **ex-date 价格调整时点(关键)**:行情源的除权价是**开盘前(ex-date 开盘即生效)**重置的,ex-date 全天的成交/取价都已在新尺度;系统侧**只取市场已除权价、不手动改价**。后续若接入份额类(送股/拆股),其持仓 ratio 调整(数量×ratio、成本÷ratio)必须作为 **ex-date 当天 EOD 的第一步(盘前)** 执行,**绝不能在收盘后补**——否则当天 intraday 成交已被按新尺度记入、再对整仓 ratio 会二次缩放、盈亏跳变。
|
||||
- **日中实时处理公司行为的证据为零**:未发现盘中监听公司行为事件并即时调整持仓/损益的逻辑。期权侧 `DividendService.Execute` 在 trade 创建/调整时调比率,但那是定价调整,非持仓实时复权。
|
||||
|
||||
---
|
||||
|
||||
## 8. TRS 侧公司行为(`EodSwapPosition.TdChangedQty`)
|
||||
|
||||
`Framework/YLErp.Core/DBModels/EodSwapPosition.cs:295-300`
|
||||
|
||||
```csharp
|
||||
/// 当日公司行为数量,因当日公司行为事件导致的数量变化
|
||||
[DisplayName("当日公司行为数量")]
|
||||
public decimal TdChangedQty { get; set; }
|
||||
```
|
||||
|
||||
- 全仓仅 **2 处引用**:定义处 + `SwapEodPositionService.cs:1650` 的 `newEodPayPosition.TdChangedQty = 0;`(恒为 0)。
|
||||
- **TRS 从不做任何持仓数量的公司行为调整**。任何改变持仓数量/价格基准的行为(送股、拆股、配股)在 TRS 中完全未实现。
|
||||
|
||||
---
|
||||
|
||||
## 9. 设计模式评估(vs 业界最佳实践)
|
||||
|
||||
### 9.1 现状
|
||||
- **无领域抽象**:无 `CorporateAction` 模型/枚举/策略/工厂/事件总线(全局搜索 0 命中)。
|
||||
- **行为类型靠魔法字符串**:`EventReason = "系统操作-分红"`(`SwapEodPositionService.cs:712`),并被当作过滤键使用(测试 `:445/:482`)。
|
||||
- **双轨并行、互不相通**:债券走 `bond_payment_info`(swap 模块);股票走 `ex_dividend_info`(`YLErpDAL/Model`,服务期权/结算)——**swap 模块对后者零耦合**。
|
||||
- **硬编码复合公式**:`DividendService.cs:730` 含魔数 10;存在"勿改"防御注释(`SwapDealService.cs:358/:987`)。
|
||||
- 唯一亮点:`SwapPositionCompose` 单一日终编排入口 + `SaveAutoSwapDeal` 收敛了资金/事件/明细三类写操作(seam 化已落地,可单测)。
|
||||
|
||||
### 9.2 业界最佳实践对照
|
||||
业界成熟做法通常是:
|
||||
1. **统一 `CorporateAction` 实体**:`actionType` + `announceDate` + `exDate` + `recordDate` + `paymentDate` + `effectiveDate` + `factor`/`splitRatio`/`dividendAmount` + `status` + `source`。
|
||||
2. **类型化 handler**:`ICorporateActionHandler.Process(position, action)`,按 `actionType` 分发(付息/分红/拆股实现同一契约)。
|
||||
3. **事件驱动重算**:公司行为触发持仓数量、成本价、名义本金、计息基准的自动重算 + 复权因子重算。
|
||||
4. **累计复权因子持久化**,前/后复权可切换。
|
||||
5. **税务组件化、可配置**(按账户/税收居民地/券种)。
|
||||
6. **record-date 持仓快照显式化**,用于分红比例计算。
|
||||
|
||||
### 9.3 差距
|
||||
本系统等价于"**每种行为一张表 + 一段 if**":
|
||||
|
||||
| 业界 | zszq-trs |
|
||||
|---|---|
|
||||
| 统一 CorpAction 表 + 类型枚举 | 两张独立表,字段语义不一致 |
|
||||
| typed handler 按类型分发 | 魔法字符串 + `PosiDividendSum != 0` 隐式判定 |
|
||||
| 事件驱动重算持仓/损益 | 日终顺序硬编码 + 复用平仓事件结构 |
|
||||
| 累计复权因子 | 每次现算、单向前复权 |
|
||||
| 税务组件 | 两套内联公式 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 新增 corp action 的成本与建议
|
||||
|
||||
### 10.1 新增「拆股 / 送股」的成本:**中高(约 2–4 人周)**
|
||||
难点不在写入 `TdChangedQty`,而在于数量变化会横穿**盯市盈亏、名义本金、成本均价、计息基准、dv01** 五个口径,而现有代码对这些量的推导深度耦合于"数量只会因平仓而减少"这一隐含假设(如 `SwapEodPositionService.cs:1835 if (curretEod.PosiQuantity > 0)` 的递推分支)。
|
||||
|
||||
需改动:
|
||||
1. 事件枚举扩位(`SwapEventTypeEnum` / `SwapFlowEventTypeEnum`)或定义 `CorpActionType`,摆脱 `EventReason` 字符串判定;
|
||||
2. 数据源接入:让 SwapModule 读取 `ex_dividend_info`(或新建 `corp_action` 表补 ex/effective date),当前 TRS 与该表零耦合;
|
||||
3. `SwapEodPositionService` 三条 EOD 分支 `SaveCurrentEodInitalPosi` / `CopyEodPosition`(`:1700-1745`) / `UpdateEodPosition`(`:1787-1860`) 均需插入数量与成本价折算,并真正写 `TdChangedQty`;
|
||||
4. 同步调整 `swap_position` 实际持仓、`PosiGrossPrice`/`PosiNetPrice`、`PosiNotionalValue`、利息腿 `TdInterestPrincipal`;
|
||||
5. 前端流水/估值列与 Golden 文件全量重跑。
|
||||
|
||||
### 10.2 建议演进方向(优先级排序)
|
||||
1. **抽象统一 `CorporateAction` 实体**:补 announce/ex/record/payment/effective 五类日期 + `factor`/`splitRatio`/`dividendAmount` + `status`;先让债券付息与股票除权汇入同一张表(迁移 `bond_payment_info` / `ex_dividend_info`)。
|
||||
2. **引入按 `actionType` 分发的 handler 接口**,付息/分红/拆股实现同一契约;将 `EventReason` 魔法字符串升级为枚举。
|
||||
3. **复权改造**:持久化累计复权因子、实现后复权、填 `HisPriceDividendAdjustEnum` 引用、加除零/空值防护、抽掉魔数 10。
|
||||
4. **税务组件化**:抽出独立税务计算组件,支持按账户/税收居民地/券种配置;替掉两处内联公式。
|
||||
5. **record-date 持仓快照显式化**:用真实快照算分红比例,替代"按付息日持仓线性缩放"的近似。
|
||||
6. **TRS 接份额类行为**:真正写 `TdChangedQty`,解除"数量只因平仓减少"的隐含假设。
|
||||
|
||||
---
|
||||
|
||||
## 附录:关键文件索引
|
||||
|
||||
| 内容 | 文件:行 |
|
||||
|---|---|
|
||||
| 股票除权除息模型 | `YLErpDAL/Model/ExDividendInfo.cs:8-61` |
|
||||
| 除权信息入库/删除(重收盘提示/已执行状态) | `YLErpWeb/Controllers/ex_dividend_infoController.cs:57-104` |
|
||||
| 复权价格调整 | `YLErpDAL/Modules/DataProviderModule/EodPriceProvider.cs:235-261` |
|
||||
| 复权因子公式 | `YLErpDAL/BLL/.../DividendService.cs:710-732` |
|
||||
| 前/后复权枚举(死代码) | `Framework/YLErp.Core/Enums/HisPriceDividendAdjustEnum.cs:6-19` |
|
||||
| 债券付息表 | `Framework/YLErp.Core/DBModels/BondPayment.cs:15` |
|
||||
| 债券付息计算 | `YLErpDAL/Modules/EodModule/BondPaymentService.cs:122-140` |
|
||||
| 自动互换入口 | `YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs:358,428,690,771` |
|
||||
| TRS 当日公司行为数量(死字段) | `Framework/YLErp.Core/DBModels/EodSwapPosition.cs:295-300`;`:1650` |
|
||||
| 分红税 / 增值税 | `Valuedate.cs:341-343`;`SwapEodPositionService.cs:1715,1720,1809,2010` |
|
||||
| 事件类型枚举 | `Framework/YLErp.Core/DBModels/Enums/SwapEventTypeEnum.cs:9-24` |
|
||||
|
||||
---
|
||||
|
||||
*本报告为静态代码分析,未运行系统、未连接数据库;所有结论均基于源码与既有测试,必要时建议对关键链路补集成测试验证。*
|
||||
@@ -0,0 +1,149 @@
|
||||
# 债券 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<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 条复用边**:
|
||||
1. `SwapEodPositionService.cs` — `SetPriceInfoByFlowEvent`(:1897 qty 递推 + :1926 加权价 + :1650 `TdChangedQty`)+ 债券结转 :1723
|
||||
2. `SwapDealService.cs` — `UpdateInitalPosition`(: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)当前是否已有业务痛点,决定优先级?
|
||||
Reference in New Issue
Block a user