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
This commit is contained in:
hjhan
2026-08-08 09:02:05 +08:00
parent c379e31b4d
commit feffc196c6
5 changed files with 506 additions and 7 deletions
@@ -1214,7 +1214,7 @@ namespace YLErp.Modules.SwapModule
interest.InterestPrincipal = preEodPosition.TdInterestPrincipal * closePrecent;
interest.FloatRate = preEodPosition.FloatRate;
InterestAmount = preEodPosition.InterestIncomeSum * closePrecent;
TdInterestAmount = preEodPosition.InterestIncomeSum;
TdInterestAmount = preEodPosition.InterestIncomeSum * closePrecent;
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.TdInterestAmount = Math.Round(TdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
@@ -1292,7 +1292,9 @@ namespace YLErp.Modules.SwapModule
if (i % interestPeriod == 0)
{
// 复利时:利息并入本金(FR007 取价已提前到 calcFirst/calcLast 跳过之前完成)
var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest;
// 始终使用循环内高精度累加的 interest,不使用 EOD 快照的 resetCarryInterest(舍入值),
// 否则非重置日 EOD 的 InterestIncomeSum 包含多个周期利息,注入首重置日会导致精度偏差。
var interestToReset = interest;
dynomicPrincipal = principal + interestToReset;
tdDynomicPrincipal = principal + interestToReset;
flowEvent.InterestPrincipal = tdDynomicPrincipal;
@@ -1326,7 +1326,7 @@ namespace YLErp.Modules.SwapModule
}
else
{
orginPv = posiNotionalValue;
orginPv = oriPosiNotionalValue;
}
decimal closePercent = oriPosiNotionalValue == 0 ? 0 : closeNational / oriPosiNotionalValue;
var eventType = autoSwap ? (int)SwapEventTypeEnum. : (int)SwapEventTypeEnum.;
@@ -1344,7 +1344,10 @@ namespace YLErp.Modules.SwapModule
List<eod_swap_position> preEodPositions = new List<eod_swap_position>();
preEodPositions.Add(eodPayPosition);
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: autoSwap || calcLast);
// 使用 oriPosiNotionalValue(平仓前原始名义本金)而非 posiNotionalValue(平仓后剩余):
// EOD 重算需基于完整头寸计算总应计利息(TdInterestAmount),再由 flowEvents 的 TdCloseInterest 扣减平仓部分。
// 若用剩余本金(如 212M),重算只得到 70% 利息,导致 InterestIncomeSum 偏差。
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, oriPosiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: autoSwap || calcLast || (valueDate == td.ExerciseDate));
decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount);
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount);
@@ -1380,8 +1383,7 @@ namespace YLErp.Modules.SwapModule
//利息端估值用信息
newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode)
? position.InterestPrincipalFix
: position.InterestMode == (int)InterestModeEnum.
&& position.InterestType != (int)InterestTypeEnum.
: position.InterestType != (int)InterestTypeEnum.
? posiNotionalValue
: interests.Count > 0 ? interests.First().InterestPrincipal : 0;
if (interval != null)
@@ -1401,7 +1403,7 @@ namespace YLErp.Modules.SwapModule
{
intersetAcmount /= tradeExtend.AnnualDays;
}
newEodPayPosition.TdInterestIncome = !autoSwap && calcLast
newEodPayPosition.TdInterestIncome = !autoSwap
? TdInterestAmount - lastInterestIncomeSum
: intersetAcmount;
Log.Info($"InterestIncomeSum is {lastInterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" +