test(swap): EQD-6968 自洽化契约钉死——事件利率确定性+快照再定盘载体+CI_007/008 新口径

- Determinism_NoTail_EventFloatRateIndependentOfPublishTime:尾日价缺(上午) vs
  有(下午)两世界,金额与事件 FloatRate 完全一致(=末段已消费利率0.01425)
- CloseOnly_平仓日为重置日_剩余持仓快照再定盘(+非重置日反例):经 DealInterests
  真路由断言快照 FloatRate=当日新定盘(RefixCalls 计数)——载体职责显式化验收
- CalcFirst 用例去掉 closeRate 免疫 hack:priorValueDate 根治后首重置日恒取价
- CI_007 更名+断言翻转:平仓日=重置日+不算尾 → 事件利率=末段已消费利率(旧);
  原断言(取新定盘)正是被替换的旧口径契约,根因注释留痕 GLMS-JIATT-20260805
- CI_008:本金结转断言保留,利率断言改为末段旧利率

验证:ConsumedInterestScenario 9/9、InterestEodTailSnapshot+Fr007 36/36、
全量 976 例 145 败与基线 diff=0
This commit is contained in:
hjhan
2026-08-19 11:07:10 +08:00
parent fafae5cac0
commit 6e2cae7c1f
3 changed files with 142 additions and 43 deletions
@@ -273,45 +273,28 @@ namespace YLErp.Modules.SwapModule
}
// ================================================================
// 场景7复现"平仓日=重置日 + calcLast=false → 重置日跳过 FR007 取价"
// 场景7:平仓日=重置日 + calcLast=false 的事件利率口径(EQD-6968 自洽化后)
// ================================================================
/// <summary>
/// [CI_007] 平仓日恰好是重置日时,calcLast=false 不应导致该重置日的 FR007 取价被跳过
/// [CI_007] 平仓日=重置日 + calcLast=false:排除日不取价,事件利率=末段已消费利率(确定性)
/// ----------------------------------------------------------------
/// 背景(GLMS-JIATT-20260805 根因)InterestCalcMode='10'(算头不算尾,calcLast=false)
/// CalcDailyCompoundInterest 循环里 `if(!calcLast && accrueDate==endDate) continue` 会跳过平仓日当天。
/// 若平仓日恰好是重置日(i%period==0),这个跳过会让"重置日取新FR007"的代码块永远不执行,
/// 沿用上一个重置周期的旧利率。
/// 历史(GLMS-JIATT-20260805):原缺陷是重置日取价被 calcLast 跳过 → flowEvent.FloatRate 停留旧值
/// → 落库后传染 EOD。当时的修复=排除日"有价则取新定盘",事件利率因而取决于平仓时刻
/// (上午=旧/下午=新),与金额实际使用的利率脱钩。
///
/// 构造:PosiStartDate=4/27, ResetPeriod=3, InterestCalcMode='10'(calcLast=false)
/// - FR007 按日期分段:5/3之前返回 rateOld=0.0015/3及之后返回 rateNew=0.002
/// - 对照A:平仓日=5/5(非重置日,9? 不: (5/5-4/27)=8, 8%3=2 非重置) → 不该取新值
/// - 对照B:平仓日=5/6(重置日,(5/6-4/27)=9, 9%3=0) → 应取新值 rateNew
/// EQD-6968 自洽化后的新契约:
/// ① 已平部分:排除日一概不取价(有价也不取),事件 FloatRate=末段已消费利率(rateOld),
/// 与金额同源、与平仓时刻无关;
/// ② 剩余持仓的新周期利率:由 EOD 快照"重置日再定盘"显式获取
/// InterestEodTailSnapshotTest.CloseOnly_平仓日为重置日_剩余持仓快照再定盘)。
///
/// 修复前:5/6 重置日被 calcLast 跳过 → 取到旧 rateOld → 与 5/5 相同
/// 修复后:5/6 重置日正常取价 → 取到 rateNew → 与 5/5 不同
/// ----------------------------------------------------------------
/// </summary>
/// <summary>
/// [CI_007] 平仓日恰好是重置日时,calcLast=false 不应导致该重置日的 FR007 取价被跳过
/// ----------------------------------------------------------------
/// 根因(GLMS-JIATT-20260805)InterestCalcMode='10'(calcLast=false)
/// CalcDailyCompoundInterest 循环 `if(!calcLast && accrueDate==endDate) continue` 跳过平仓日。
/// 若平仓日=重置日,取价代码块被跳过 → flowEvent.FloatRate 停留旧值 → 落库后传染 EOD。
///
/// 构造(避开周末,period=7)
/// PosiStartDate=4/27(周一), period=7, interest_rule=0, InterestCalcMode='10'
/// 重置日:i=0→4/27(周一), i=7→5/4(周一,工作日)
/// 平仓日=5/4(=重置日=endDate)
/// FR007 分界:rateDate>=5/4 返回 rateNew,否则 rateOld
///
/// 修复前:i=7(5/4)被 calcLast 跳过 → FloatRate=rateOld(旧值)
/// 修复后:i=7(5/4)正常取价 → FloatRate=rateNew(新值)
/// 构造(避开周末,period=7)PosiStartDate=6/1(周一), 平仓日=6/8(周一,重置日,7%7=0)
/// FR007 分界:取价日>=6/8 返回 rateNew,否则 rateOld。
/// ----------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_007_平仓日等于重置日_calcLast_false_仍应取新FR007()
public void CI_007_平仓日等于重置日_calcLast_false_事件利率为末段已消费利率()
{
const double rateOld = 0.001;
const double rateNew = 0.002;
@@ -363,12 +346,14 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(1, interests.Count);
var result = interests[0];
Console.WriteLine($"6/8(重置日,周一)平仓:FloatRate={result.FloatRate} Amount={result.InterestAmount:F6}");
Console.WriteLine($" 期望 FloatRate={rateNew}(6/8 重置日查询日=6/8工作日,应取新利率)");
Console.WriteLine($" 新口径期望 FloatRate={rateOld}(排除日不取价,事件利率=末段已消费利率)");
// 核心断言6/8 是重置日,flowEvent.FloatRate 应反映新利率 rateNew
Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateNew) < 0.0001m,
$"平仓日=重置日时 FloatRate 应={rateNew}(取到新利率)。" +
$"实际={result.FloatRate},若={rateOld} 说明 calcLast=false 跳过了重置日取价(GLMS-JIATT-20260805 根因)");
// 核心断言(EQD-6968 自洽化契约):排除日(不计息)一概不取价——即使 6/8 新定盘已发布,
// 事件 FloatRate 也必须是末段已消费利率 rateOld,与金额同源、与平仓时刻无关。
// 剩余持仓的新周期利率由 EOD 快照"重置日再定盘"显式获取(见 InterestEodTailSnapshotTest)。
Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateOld) < 0.0001m,
$"排除日不取价:FloatRate 应=末段已消费利率 {rateOld}。实际={result.FloatRate}" +
$"若={rateNew} 说明排除日仍在取价(旧口径:记录利率取决于平仓时刻)");
var interestBeforeResetDate = Principal * (FixedRate + (decimal)rateOld) * 7m / AnnualDays;
AssertDecimal(Principal + interestBeforeResetDate, result.InterestPrincipal,
@@ -426,10 +411,12 @@ namespace YLErp.Modules.SwapModule
var remainingInterest = previousInterest * remainingPrincipal / previousPrincipal;
var expectedPrincipal = remainingPrincipal + remainingInterest;
var expectedDailyInterest = expectedPrincipal * (fixedRate + (decimal)newFloatRate) / AnnualDays;
AssertDecimal(expectedPrincipal, result.InterestPrincipal);
AssertDecimal(expectedDailyInterest,
result.InterestPrincipal * (result.InterestRate + result.FloatRate.Value) / AnnualDays);
// EQD-6968 自洽化:排除日(平仓日=重置日)不取价,事件利率=末段已消费利率(旧)——与金额同源、
// 与平仓时刻无关。剩余持仓的新周期利率由 EOD 快照"重置日再定盘"显式获取
// InterestEodTailSnapshotTest.CloseOnly_平仓日为重置日_剩余持仓快照再定盘)。
AssertDecimal((decimal)oldFloatRate, result.FloatRate.Value,
"排除日不取价:事件 FloatRate 应=末段已消费旧利率");
}
[TestMethod]