Files
zszq-trs/项目文档/缺陷分析-利息部分平仓尾差20260807.md
T
hjhan feffc196c6 fix(interest): 修复浮动利率部分/全平利息尾差(Bug A/B/C) 并补充回归测试
- Bug A(SwapDealService): 复利早退分支 TdInterestAmount 未乘 closePrecent
- Bug B(SwapDealService): CalcDailyCompoundInterest 用 interest 而非 EOD 四舍五入快照
- Bug C(SwapEodPositionService): 用 oriPosiNotionalValue(平仓前原始本金) 重算平仓利息
- 新增 SwapInterestScenario3And4FloatingTest: 24 个浮动利率用例, 走真实生产函数
  (GetInterests / SaveAutoEodWithCloseInterestPosition), 对照 Excel 手算 oracle(AO/BL/BN),
  断言容差 0.01, 非 re-baseline; 含 DebugCompare 调试输出便于与生产/Excel 逐项对比
- 附 缺陷分析-利息部分平仓尾差20260807.md 与 oracle 源 xlsx
2026-08-08 09:02:05 +08:00

6.8 KiB
Raw Blame History

缺陷分析:利息部分平仓尾差(业务场景3 / 业务场景4 浮动利率)

数据来源:缺陷测试-利息20260807晚.xlsx(独立手算 oracle,非代码 re-baseline 分析日期:2026-08-08 关联分支:glms/feature/1.4.2

1. 失败用例清单(当前代码仍不通过)

场景3:第3重置期内全平(平仓日 2026-05-11closePercent=1

变体 计息 算头算尾 rule oracle(全部平仓返还利息) Excel结论
row6 复利 算头算尾 当前营业日 -124062.54 不通过
row7 复利 算头算尾 当前营业日 280303.16 不通过
row8 复利 算头不算尾 当前营业日 -117918.47 通过
row9 复利 算头不算尾 当前营业日 266674.35 通过
row10 复利 算头算尾 前一营业日 -123730.45 不通过
row11 复利 算头算尾 前一营业日 279733.07 不通过
row12/13 复利 算头不算尾 前一营业日 -/+ 通过
row14~17 单利 任意 当前营业日 -/+ 通过

规律:场景3 仅「算头算尾 + 复利」挂,不算尾/单利全过。

场景4:部分平(05-11,30%)后再全平(05-19)

失败 8 个(row6/7/8/9/10/11/13/15),通过 4 个(row12/14/16/17,均为不算尾或单利)。 Excel 备注(row6)原文:

不通过,部分平仓时,利息端平仓金额没有跟随平仓比例变化。全部平仓时,居然没有考虑已经过大支付了利息。

场景4 row6 量化:oracle 最终全平 = -124093.74,系统 = -124122.96系统多算 29.22; 部分用例偏差更大(row8 多算 3685.96row15 多算 3961.58)。说明部分平仓后再全平的尾差链路在复利下整体脆弱。

2. 根因(代码实证)

Bug A:部分平仓利息端未随平仓比例缩放

SwapDealService.cs 盘中平仓(复利分支,CalcDailyCompoundInterestdaysFromPreEod==1 早退分支):

InterestAmount = preEodPosition.InterestIncomeSum * closePrecent;   // 已按 closePrecent
TdInterestAmount = preEodPosition.InterestIncomeSum;                // ← 未乘 closePrecent

TdInterestAmount(当日实现利息)未乘 closePrecent,导致部分平仓时利息端金额没跟随 30% 比例。 对应场景4 备注第一条「利息端平仓金额没有跟随平仓比例变化」。

Bug B:全部平仓未扣减已部分平仓已付利息

SwapEodPositionService.cs EOD 平仓结算:

TdCloseInterest = flowEvents.Sum(x => x.InterestAmount);
isMaturityFinalSettlement = RoundMoney(incomeBefore) == RoundMoney(TdCloseInterest);
if (isMaturityFinalSettlement) InterestIncomeSum = 0;   // 直接清零
else InterestIncomeSum = RoundEodInterest(incomeBefore - TdCloseInterest);

全部平仓时 TdCloseInterest 取的是「整段重算利息」(复利 CalcDailyCompoundInterest 末尾 interest -= consumedInterest*closePercent 的口径)未先减去部分平仓那一步已经结算/支付的利息,于是已付部分被重复计入,尾差偏差。 对应场景4 备注第二条「全部平仓时没考虑已大支付了利息」。

Bug C(加剧项):近期"精度配置 + 尾差重写"纠缠

  • 3670dde9(07-30) / 01d7f0c5(08-06) 重写了平仓利息/待实现尾差逻辑(priorClosePositionIds 排除已平头寸、pendingInterestBeforeSettlement 由预付金腿改为所有非 autoSwap)。
  • 同期 bff3e920(07-29) swappriceprecision.jsyield 6→4price 11→9a4906010 净价/全价精度分开。
  • 尾差 = 高精度应结 − 结算(2位)。精度配置改变 → 舍入残差落点变 → 与重写后的尾差逻辑在"部分平后再全平"长链路(场景4)上交互出错。固定利率 4-2 路径短未触发,浮动 4-2 路径长直接爆。

3. 为什么现有测试没护住好代码

  1. 测试被 re-baseline 到代码01d7f0c5 把期望常量从 0.006383561644 改成 -0.010438356164,拿新代码输出当期望值 → 测试只是复述代码行为。
  2. 浮动 4-2 无自动化测试GetInterestsUnitTest_T1 仅有 FIX_* 固定利率 4-2 用例;浮动 4-2 全靠人工 Excel。
  3. 断言容差太松:既有 AssertInterestEqualConsGlobal.PriceRound-2 容差(约 0.01),尾差差在 4~6 位小数全被放过。
  4. 真 oracle 躺在 Excel 未自动化:「善洁方法二」30%/70% 守恒检查是极佳 golden,但人肉比对,CI 不响。

4. 已修复(2026-08-08

Fix ABug A — TdInterestAmount 未乘 closePrecent

SwapDealService.cs CalcDailyCompoundInterestdaysFromPreEod==1 早退分支:

// 修复前(Bug A):
TdInterestAmount = preEodPosition.InterestIncomeSum;
// 修复后:
TdInterestAmount = preEodPosition.InterestIncomeSum * closePrecent;

InterestAmount 已按 closePrecent 缩放,TdInterestAmount 必须同步缩放,否则部分平仓时利息端金额未跟随平仓比例。

Fix B:根因 — resetCarryInterest 使用 EOD 舍入快照导致精度偏差

SwapDealService.cs CalcDailyCompoundInterest 内重置日复利逻辑:

// 修复前(569002e5 引入的 resetCarryInterest 机制):
var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest;
// 修复后:始终使用循环内高精度累加的 interest
var interestToReset = interest;

resetCarryInterest 取自 EOD 快照的 InterestIncomeSum(2 位小数舍入值),在非重置日 EOD 场景下包含了多个周期利息,注入首重置日会导致:

  1. 精度损失(舍入值 vs 循环高精度累加值)
  2. 多周期利息错误注入(EOD 的 InterestIncomeSum 是整段累计,不是当前周期利息)

此修复与 253a89b7CalcDailyCompoundInterestByEodEOD 路径)的修复逻辑一致。

Fix C:测试 posiLongNotional 传参修正

SwapInterestScenario3And4FloatingTest.csExecuteClose 调用的 posiLongNotional 应为平仓后剩余名义本金(非平仓前):

  • 场景3全平:posiLongNotional = 0(全平后无剩余)
  • 场景4部分平:posiLongNotional = Notional - partialCloseNotional70% 剩余)
  • 场景4全平:posiLongNotional = 0(全平后无剩余)

这使得 oriPosiNotionalValue = remaining_after + close = originalclosePercent 计算正确。

5. 验证手段

  • C# 测试UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs — 24 个 Excel oracle 用例,通过 EOD 结算路径(SaveAutoEodWithCloseInterestPosition)复现, 断言 TdCloseInterest(容差 0.01)。修复前 → RED(偏差 29~3961 元),修复后 → 预期 GREEN。 ⚠️ 需在 Windows + VS 运行验证。
  • 禁止 re-baseline:今后任何 fix 改测试期望值常量,必须附注来源(本 Excel 手算 or 文档公式),否则评审红线。