fix(swap): 复利重置日=平仓日时 calcLast 不再跳过 FR007 取价

根因(GLMS-JIATT-20260805):InterestCalcMode='10'(算头不算尾,calcLast=false)时,
CalcDailyCompoundInterest 循环的 'if(!calcLast && accrueDate==endDate) continue'
会跳过平仓日。若平仓日恰好是重置日(i%period==0),取价代码块被一并跳过,
导致 flowEvent.FloatRate 落库为旧周期利率,传染后续 EOD 复利计算。

修复:把重置日的 FR007 取价提前到 calcFirst/calcLast 跳过判断之前——
calcLast 只应跳过'计息',不应跳过'重置日利率取价'。同时循环外用最终
floatRate 兜底赋值 flowEvent.FloatRate,确保落库值反映最后重置日的利率。

验证:
- CI_007 合成测试(平仓日=重置日):修复前 FloatRate=旧值(FAIL),修复后=新值(PASS)
- 连库验证(GLMS-JIATT-20260805 8/4平仓):FloatRate 0.0123→0.0213(8/3新值)
- 利息金额不变(calcLast 不计当天利息,Amount 不受影响,只修正 FloatRate 字段)
- 全套 swap 测试无新增回归(334通过,2失败均为pre-existing单利/EOD路径)

附:CI_007 复现测试 + GLMS20260805 FR007/EOD 诊断工具 + 文档 longRatio 状态更新
This commit is contained in:
hjhan
2026-08-07 20:43:16 +08:00
parent 3178cf3f0f
commit 66a97e03ef
4 changed files with 762 additions and 35 deletions
+21 -15
View File
@@ -1249,28 +1249,31 @@ namespace YLErp.Modules.SwapModule
for (int i = 0; i <= calcDays; i++)
{
var accrueDate = startDate.AddDays(i);
// 重置日取价必须在 calcFirst/calcLast 跳过之前完成:calcLast=false(不算尾) 只应跳过计息,
// 不应跳过重置日的 FR007 取价。否则平仓日=重置日时会沿用旧周期利率,
// 且 flowEvent.FloatRate 落库为旧值,传染后续 EODGLMS-JIATT-20260805 根因)。
if (accrueDate >= startDate && i % interestPeriod == 0
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
{
if (floatRate1 != 0) floatRate = floatRate1;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
}
}
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,取价已在上方完成)
if (accrueDate >= startDate)
{
if (i % interestPeriod == 0)
{
// 复利时:利息并入本金
// 复利时:利息并入本金FR007 取价已提前到 calcFirst/calcLast 跳过之前完成)
dynomicPrincipal = principal + interest;
tdDynomicPrincipal = principal + interest;
// 获取新的浮动利率
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0));
if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1))
{
if (floatRate1 != 0) floatRate = floatRate1;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
}
}
flowEvent.InterestPrincipal = tdDynomicPrincipal;
TdInterestPrincipal = tdDynomicPrincipal;
}
@@ -1292,6 +1295,9 @@ namespace YLErp.Modules.SwapModule
tdinterest += tdinterest1;
}
}
// 兜底:若循环因 calcLast 跳过最后一天(重置日=平仓日)flowEvent.FloatRate 不会被循环内赋值,
// 用最终 floatRate 兜底,确保落库的 FloatRate 反映最后一个重置日的利率(GLMS-JIATT-20260805)。
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
// 复利从头重放得到的是"假设从未结出"的整段总利息,需扣除历史已通过互换结出的利息,
// 否则已结部分会重复计息(类比分红 PosiDividendSum = totalToDate RealizedDividend)。
// consumedInterest is full-position absolute interest; scale it to this close portion.