test(swap-eod): 场景4 跨日毒链携带守卫改为生产自身 DailyAccrual 不变量(真捕获 mode9 误反推)
- 原引擎级差分版(734683ea)为假绿:固定 obsDate=2026-05-12 在 td.StartDate 重置日判定下恒为重置日,
basis=notional+priorAccrued 不取 priorNotional(被误反推的 TdInterestPrincipal),故未捕获 bug。
- 现改为直接复用生产单一真相源 SwapEodPositionService:1388 的 TdInterestIncome=DailyAccrual(TdInterestPrincipal,…)
纯函数:正确本金与“生产实际”本金各算一次 D+1 应计,二者唯一差异即 TdInterestPrincipal;误反推(膨胀~2.3x)
使 D+1 应计同步膨胀,差远超容差→红。该不变量与重置日/平仓比例无关,稳健。
- 守卫置于单日 TdInterestPrincipal 不变量之前,使其成为首要捕获点(实跑验证:注入 mode9 误反推后
首失败点即“场景4[跨日] 毒链携带 D+1 应计偏差 -5501.96”,correct=-4126.47 / poisoned=-9628.43)。
- 移除不再使用的 CalcNextDayInterest 引擎级辅助方法。
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using YLErp;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule
|
||||
{
|
||||
@@ -397,22 +398,6 @@ 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 业务场景3:第3重置期内全平(平仓日 2026-05-11,closePercent=1)
|
||||
@@ -537,6 +522,28 @@ namespace UnitTestProject.Modules.SwapModule
|
||||
? _eod.LastBaseInterestPrincipal * (1m - cp) / cp
|
||||
: _eod.LastBaseInterestPrincipal;
|
||||
}
|
||||
// 跨日毒链携带守卫(验证“最终结果”而非单日快照,置于单日守卫之前使其为首要捕获点):
|
||||
// 部分平仓的 TdInterestPrincipal 是带去次日的计息基数;生产下一日本金利息 TdInterestIncome 正是由
|
||||
// 该基数经 DailyAccrual 算出(SwapEodPositionService:1388 单一真相源:
|
||||
// TdInterestIncome = DailyAccrual(TdInterestPrincipal, 当日利率, 浮动利率, 年化, 年化天数))。
|
||||
// 故“D 日 TdInterestPrincipal → D+1 TdInterestIncome == DailyAccrual(该基数)”是生产自身的不变量。
|
||||
// 本守卫用生产同一纯函数直接验证跨日携带:正确本金与“生产实际”本金各算一次 D+1 应计,
|
||||
// 二者唯一差异就是 TdInterestPrincipal;误反推(膨胀~2.3x)会让 D+1 应计同步膨胀,差远超容差→红。
|
||||
// (注:本 harness 的 RollForward 分支因 prior-eod seam 未接到内存 eod 链,rollforward eod 的
|
||||
// TdInterestPrincipal 恒为 0,无法在 eod 层直接观察携带;故在纯函数层验证该不变量。)
|
||||
var annualDays = td.trade_extend == null ? 365 : td.trade_extend.ExtendObj.AnnualDays;
|
||||
var correctNextDayIncome = InterestIncomeCalc.DailyAccrual(
|
||||
expectedTdPrincipal, partialEod.TdInterestRate, partialEod.FloatRate, partialEod.IsAnnualized, annualDays);
|
||||
var poisonedNextDayIncome = InterestIncomeCalc.DailyAccrual(
|
||||
partialEod.TdInterestPrincipal, partialEod.TdInterestRate, partialEod.FloatRate, partialEod.IsAnnualized, annualDays);
|
||||
var carryTol = Math.Max(0.01m, Math.Abs(correctNextDayIncome) * 0.05m);
|
||||
Assert.IsTrue(Math.Abs(poisonedNextDayIncome - correctNextDayIncome) <= carryTol,
|
||||
$"场景4[跨日] 毒链携带 {note}: D+1 TdInterestIncome 应=DailyAccrual(正确本金 {expectedTdPrincipal})," +
|
||||
$"但生产 partialEod.TdInterestPrincipal={partialEod.TdInterestPrincipal} 使 D+1 应计偏差 {poisonedNextDayIncome - correctNextDayIncome}" +
|
||||
$"(correct={correctNextDayIncome}, poisoned={poisonedNextDayIncome})");
|
||||
|
||||
// 单日不变量守卫(跨日守卫之后的次级细节):TdInterestPrincipal 必须精确等于经济不变量推导的
|
||||
// 正确本金(mode9=计息器 base,禁任何 (1-cp)/cp 反推;mode2=base*(1-cp)/cp)。
|
||||
AssertStrict(expectedTdPrincipal, partialEod.TdInterestPrincipal, "场景4[部分] TdInterestPrincipal " + note);
|
||||
|
||||
// 部分平仓后,剩余名义本金缩减为 70%(真实代码路径更新持仓口径)
|
||||
@@ -547,22 +554,6 @@ 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)
|
||||
|
||||
Reference in New Issue
Block a user