docs: 任务4 EOD续接风险分析 + SwapDealService方向比例收口

新增 docs/eod-continuation-proposal.md:
- 记录4个风险(并本金起点/consumedInterest语义/resetCarry/全平重放)
- 记录验证方案(6场景影子测试)
- 标注待立项,不混入日常重构

SwapDealService 收尾: 3处内联方向比例→DirectionRatio.ReceivePay
- line389: PosiDirection收取?-1:1 → -ReceivePay
- line1104/1235: InterestDirection==1?1:-1 → ReceivePay

SwapModule零回归(7基线/510通过)
This commit is contained in:
hjhan
2026-08-12 15:24:15 +08:00
parent 304e04d60a
commit 89eae7555f
2 changed files with 69 additions and 3 deletions
@@ -386,7 +386,7 @@ namespace YLErp.Modules.SwapModule
floatEvent.PositionQty = 0;
floatEvent.ContractSize = position.ContractSize;
floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize;
var ratio = position.PosiDirection == (int)SwapDirectionEnum. ? -1m : 1m;
var ratio = -DirectionRatio.ReceivePay(position.PosiDirection);
floatEvent.TradingFeePending = CalcInitTradingFeePending(oriPosition, position, unwindData);
floatEvent.DataState = (int)SwapFlowDateStateEnum.;
floatEvent.InterestMode = position.InterestMode;
@@ -1109,7 +1109,7 @@ namespace YLErp.Modules.SwapModule
interest.InterestAmount = Math.Round(interestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.TdInterestAmount = Math.Round(tdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
// 计算InterestClosePnL(方向:收取=1为正,支付=-1为负)
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
if (add) UpdateDbOption(interest);
@@ -1240,7 +1240,7 @@ namespace YLErp.Modules.SwapModule
{
decimal InterestAmount = 0;
decimal TdInterestAmount = 0;
var interestRatio = position.InterestDirection == 1 ? 1m : -1m;
var interestRatio = DirectionRatio.ReceivePay(position.InterestDirection);
var floateRate = preEodPosition.FloatRate;
if (position.InterestType == (int)InterestTypeEnum.)
{
@@ -0,0 +1,66 @@
# 任务 4:盘中复利从 EOD 续接(而非 PosiStartDate 全程重放)
## 状态:待立项(高风险,需专项验证)
## 现状
`CalcDailyCompoundInterest``position.PosiStartDate` 全程重放到 `endDate`,每个重置日把累计利息并入本金(复利),最后扣 `consumedInterest * closePercent`
**调用链**`InitSwapDealInterest``CalcDailyCompoundInterest(endDate, PosiStartDate→endDate 全程重放)`
**问题**:交易存续期长(数月~数年)时,每次盘中平仓都从起息日重放,计算量随天数线性增长。
## 提议
改为从上一日终快照(`preEodPosition`)续接:
- 起点 = `preEodPosition.ValueDate + 1`
- 初始本金 = `preEodPosition.TdInterestPrincipal`(已含历史滚入利息)
- 只算 `ValueDate+1``endDate` 的增量利息
## 风险分析(为什么不能直接改)
### 风险 1:并本金起点不同导致终值不等
| | 全程重放(当前) | EOD 续接(提议) |
|---|---|---|
| 起点 | `principal`(原始平仓名义本金) | `preEod.TdInterestPrincipal`(已滚利息) |
| 滚法 | 每段 `basis = principal + accrued` | 每段 `basis = preEod.TdInterestPrincipal + segmentAccrued` |
两段路径在**中间重置日的四舍五入路径不同**(精度 12 的 Round 作用在不同的中间值上),终值**不一定逐分相等**。
### 风险 2consumedInterest 语义翻转
- 全程重放:总利息 - consumedInterest × closePercent = 增量
- EOD 续接:直接算增量,**不需要**扣 consumedInterest
如果 EOD 快照的 `InterestIncomeSum` 与 consumedInterest 口径不完全一致,直接去掉扣减会引入误差。
### 风险 3resetCarryInterest 耦合
当前逻辑:`resetCarryInterest`(上一日终待实现 × remainingPercent)只在 `endDate` 恰好是重置日时并入本金。EOD 续接模式下,重置日的判定、remainingPercent 的计算、carry 的注入时机都不同。
### 风险 4:全平重放逻辑(lines 1293-1304
`InitSwapDealInterest``closePrecent == 1m` 时做 **两次** `CalcDailyCompoundInterest` 重放(截至平仓日 + 截至上一日终),取差值。EOD 续接模式下这段逻辑需要完全重新设计。
## 验证方案(立项前提)
1. 构造测试用例:同一笔复利交易,跨越 ≥2 个重置周期,有 preEod 快照
2. 用**旧全程重放**算出 `(InterestAmount, TdInterestAmount, finalBasis)`
3. 用**新 EOD 续接**算出同样三个值
4. 断言差额 < 0.01(到分)
5. 覆盖场景:
- 部分平仓(closePercent < 1
- 全平(closePercent == 1
- 平仓日 = 重置日
- 平仓日 ≠ 重置日
- 有/无 consumedInterest
- 有/无 resetCarryInterest
## 建议排期
单独 sprint 处理,不混入日常重构。改动范围:
- `CompoundInterestAccrual.AccruePeriod` 新增 `startBasis` 参数(或新方法 `AccrueFromEod`
- `CalcDailyCompoundInterest` wrapper 改为传 `preEod.TdInterestPrincipal` 作为起点
- `InitSwapDealInterest` 全平重放逻辑简化(不再需要两次重放取差值)
- `consumedInterest` 扣减逻辑移除或调整