diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 00f3419d..8ae2c8d3 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -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.复利) { diff --git a/YLErpDAL/Modules/SwapModule/docs/eod-continuation-proposal.md b/YLErpDAL/Modules/SwapModule/docs/eod-continuation-proposal.md new file mode 100644 index 00000000..3f31c83c --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/docs/eod-continuation-proposal.md @@ -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 作用在不同的中间值上),终值**不一定逐分相等**。 + +### 风险 2:consumedInterest 语义翻转 + +- 全程重放:总利息 - consumedInterest × closePercent = 增量 +- EOD 续接:直接算增量,**不需要**扣 consumedInterest + +如果 EOD 快照的 `InterestIncomeSum` 与 consumedInterest 口径不完全一致,直接去掉扣减会引入误差。 + +### 风险 3:resetCarryInterest 耦合 + +当前逻辑:`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` 扣减逻辑移除或调整