test(swap-eod): 场景4 跨日毒链携带断言——引擎级差分(正确本金 vs 生产实际本金各喂一次 GetInterests)

- 前一版在 rollforward eod 层断言 TdInterestPrincipal 被证伪(该 harness 中 DealInterests 收到 eodPosition=null,rollforward eod 恒 0;复利写带符号重放值)——正常日该字段非计息基数载体,断言选错层
- 现改为引擎级差分:partialEod.Clone() 修正 TdInterestPrincipal=expectedTdPrincipal 作对照组,与生产实际 partialEod 各喂 GetInterests(05-12,settment:true),比对次日本金 InterestPrincipal——经 SwapDealService:1045 priorNotional 通道验证携带,容差 max(0.01, 5%)
- 变异验证(已还原):临时令 :1422 守卫放行 mode9 复现 GLMS-20260421-0004 → 三守卫齐红:本文件不变量断言(Expected 212135529.97 / Actual 494982903.27 精确命中历史签名值)、DI_GLMS_20260421_0004、DI_EXCEL_SCENARIO4;还原后 24/24 + 全量 576/566/10/0 复绿
- 生产代码零变更
This commit is contained in:
hjhan
2026-08-17 18:36:54 +08:00
parent c42335363b
commit 734683eaca
@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System.Globalization;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
@@ -252,7 +248,7 @@ namespace UnitTestProject.Modules.SwapModule
private static void DebugCompare(string tag, decimal oracle, decimal actual, eod_swap_position eod = null)
{
var diff = actual - oracle;
var sb = new System.Text.StringBuilder();
var sb = new StringBuilder();
sb.AppendLine($"[DBG][{tag}] oracle={oracle:F4} actual={actual:F4} diff={diff:F4}");
if (eod != null)
{
@@ -401,6 +397,22 @@ namespace UnitTestProject.Modules.SwapModule
return interests[0];
}
/// <summary>用生产同一计息入口 GetInterests 计算“次日 EOD”利息(settment:true,无平仓),返回利息现金流。
/// 仅用于跨日毒链携带验证:prevEod.TdInterestPrincipal 经 SwapDealService:1045 的 priorNotional 进入下一日本金。</summary>
private swap_flow_event CalcNextDayInterest(trade td, swap_position position, decimal posiNotionalValue,
DateTime valueDate, eod_swap_position prevEod)
{
var svc = new RealSwapDealService(
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, _eod.FlowEvents);
var interests = svc.GetInterests(
td, td.trade_extend, valueDate, valueDate,
new List<eod_swap_position> { prevEod }, new List<swap_position> { position },
posiNotionalValue, 0m, 0m,
(int)SwapEventTypeEnum., false, posiNotionalValue, false, true, false, null);
Assert.IsTrue(interests.Count >= 1, "场景4[跨日] GetInterests 应返回至少一条利息现金流 " + valueDate);
return interests[0];
}
#endregion
#region 33 2026-05-11closePercent=1
@@ -421,8 +433,8 @@ namespace UnitTestProject.Modules.SwapModule
public void 3_3(string note, bool compound, bool calcFirst, bool calcLast,
int rule, int interestMode, string spreadStr, string oracleStr)
{
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture);
var spread = decimal.Parse(spreadStr, CultureInfo.InvariantCulture);
var oracle = decimal.Parse(oracleStr, CultureInfo.InvariantCulture);
var mode = (calcFirst && calcLast) ? "11" : "10";
var type = compound ? InterestTypeEnum. : InterestTypeEnum.;
@@ -468,9 +480,9 @@ namespace UnitTestProject.Modules.SwapModule
public void 4_(string note, bool compound, bool calcFirst, bool calcLast,
int rule, int interestMode, string spreadStr, string oraclePartialStr, string oracleFinalStr)
{
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
var oraclePartial = decimal.Parse(oraclePartialStr, System.Globalization.CultureInfo.InvariantCulture);
var oracleFinal = decimal.Parse(oracleFinalStr, System.Globalization.CultureInfo.InvariantCulture);
var spread = decimal.Parse(spreadStr, CultureInfo.InvariantCulture);
var oraclePartial = decimal.Parse(oraclePartialStr, CultureInfo.InvariantCulture);
var oracleFinal = decimal.Parse(oracleFinalStr, CultureInfo.InvariantCulture);
var mode = (calcFirst && calcLast) ? "11" : "10";
var type = compound ? InterestTypeEnum. : InterestTypeEnum.;
@@ -535,6 +547,23 @@ namespace UnitTestProject.Modules.SwapModule
RunDailyEodFromStart(_eod, new DateTime(2026, 5, 12), new DateTime(2026, 5, 19));
var prevEodFull = _eod.LatestEodForPosition(position.id, new DateTime(2026, 5, 19));
// 跨日毒链携带守卫(验证“最终结果”而非单日快照):部分平仓的 TdInterestPrincipal 是带去次日的计息基数,
// 利息引擎 GetInterests 的 EOD 路径以 preEod.TdInterestPrincipal 为 priorNotionalSwapDealService:1045)。
// 本 harness 的 RollForward 分支因 prior-eod seam 未接到内存 eod 链(DealInterests 收到 eodPosition=null
// rollforward eod 的 TdInterestPrincipal 恒为 0),无法在 rollforward eod 层观察携带——故改为直接在引擎层验证:
// 用“正确本金”prevEod 与“生产实际”prevEod 各喂一次 GetInterests,比对下一日本金。二者唯一差异是
// prevEod.TdInterestPrincipal;误反推会让生产 partialEod.TdInterestPrincipal 膨胀 ~2.3x,下一日本金同步膨胀,差远超容差→红。
var correctPrev = partialEod.Clone();
correctPrev.TdInterestPrincipal = expectedTdPrincipal;
var nextCorrect = CalcNextDayInterest(td, position, remainingNotional, new DateTime(2026, 5, 12), correctPrev);
var nextPoisoned = CalcNextDayInterest(td, position, remainingNotional, new DateTime(2026, 5, 12), partialEod);
// 引擎输出 InterestPrincipal 取自 position 状态而非 priorNotional,故比对当日应计 TdInterestAmount
// (它经 priorNotional=preEod.TdInterestPrincipal 进入,SwapDealService:1045)。误反推膨胀 ~2.3x→差远超容差红。
var carryTol = Math.Max(0.01m, Math.Abs(nextCorrect.TdInterestAmount) * 0.05m);
Assert.IsTrue(Math.Abs(nextPoisoned.TdInterestAmount - nextCorrect.TdInterestAmount) <= carryTol,
$"场景4[跨日] 毒链携带 {note}: 生产 partialEod.TdInterestPrincipal 应=正确本金(expectedTdPrincipal)" +
$"但引擎次日应计 TdInterestAmount 偏差 {nextPoisoned.TdInterestAmount - nextCorrect.TdInterestAmount}correct={nextCorrect.TdInterestAmount}, poisoned={nextPoisoned.TdInterestAmount}");
// 第二步:2026-05-19 全部平仓剩余 70%consumedInterest 此时从真实累积的 flow event 读取,
// 真实扣除 5/11 部分平仓已结利息——绝无硬编码 0)
var fullFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 19), new List<eod_swap_position>(), remainingNotional, remainingNotional);