diff --git a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs index e6a99ca7..bda6393e 100644 --- a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs @@ -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]; } + /// 用生产同一计息入口 GetInterests 计算“次日 EOD”利息(settment:true,无平仓),返回利息现金流。 + /// 仅用于跨日毒链携带验证:prevEod.TdInterestPrincipal 经 SwapDealService:1045 的 priorNotional 进入下一日本金。 + 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 { prevEod }, new List { 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 业务场景3:第3重置期内全平(平仓日 2026-05-11,closePercent=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 为 priorNotional(SwapDealService: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(), remainingNotional, remainingNotional);