fix(swap): EQD-6977 无日终快照时承接①兜底取事件基数——复利重放把 normalEvent.InterestPrincipal 写为末次并本金后基数(=被平份额本金+①),同为实际值;UAT实测(无快照+已有8/19重置)原①=0少算≈3.17元已消除;重置日+无快照退化场景补告警trace;新增兜底路径恒等式测试(23/23)

This commit is contained in:
hjhan
2026-08-20 16:09:43 +08:00
parent 8d7bc1b457
commit 286a834aa5
2 changed files with 45 additions and 10 deletions
@@ -49,14 +49,14 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
ExerciseDate = maturity, TradeStatus = "确认成交", ValidState = "Valid"
};
private static swap_position CompoundLeg(DateTime posiStart, DateTime maturity, decimal spread)
private static swap_position CompoundLeg(DateTime posiStart, DateTime maturity, decimal spread, int periodDays = 7)
=> new()
{
id = 1001, SwapTradeId = 1, PosiDirection = 0, InterestDirection = 1,
InterestMode = (int)InterestModeEnum., InterestRateDefault = spread,
InterestPrincipalFix = Notional, PosiStartDate = posiStart, PosiMatuirityDate = maturity,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
IsAnnualized = true, interest_rest_days = periodDays, interest_rule = 0,
FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
};
@@ -66,16 +66,18 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
TdInterestPrincipal = rollingBasis, InterestIncomeSum = incomeSum };
private static decimal RunFee(trade td, swap_position p, decimal settledAmount,
eod_swap_position? preEod, DateTime unwind, bool settled, decimal spread)
eod_swap_position? preEod, DateTime unwind, bool settled, decimal spread,
decimal interestPrincipal = 0m, bool maturityCalcLast = true)
{
var e = new swap_flow_event
{
PositionId = p.id, InterestAmount = settledAmount, InterestFee = 0m,
InterestDirection = 1, InterestClosePnL = settledAmount
InterestDirection = 1, InterestClosePnL = settledAmount,
InterestPrincipal = interestPrincipal // 复利主路径下=重放末次并本金后基数(=被平份额本金+①)
};
PenaltyInterestFeeMerger.Merge(
td, new List<swap_position> { p }, new List<swap_flow_event> { e },
unwind, AnnualDays, settled, maturityCalcLast: true,
unwind, AnnualDays, settled, maturityCalcLast: maturityCalcLast,
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
getSpread: _ => spread, getPreEod: _ => preEod, tryGetFixing: (d, c) => spread);
return e.InterestFee;
@@ -170,6 +172,26 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
"锚点偏离:罚息分段/重置日判定必须用 position.PosiStartDate 网格(误用 td.StartDate 网格必挂)");
}
[TestMethod]
public void preEod且此前已有重置_经事件基数兜底_恒等式精确成立()
{
// UAT 实测场景(tradeId=2447):环境无日终快照、起息后已发生 8/19 重置并本。
// 兜底① = normalEvent.InterestPrincipal 本金(复利重放末次并本金后基数);
// 修复前 ①=0 少算 ≈3.17 元(并入额×冻结利率×段尾天数),本用例钉死兜底路径的精确性。
var start = new DateTime(2026, 8, 5); var unwind = new DateTime(2026, 8, 20); var maturity = new DateTime(2026, 9, 30);
var hist = new decimal[] { 0.0216m, 0.0144m }; // 8/5 段 2.16% / 8/19 段 1.44%(=冻结)14 天重置
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.StartOnly, hist, period: 14); // 已结 [8/5..8/19]
var replayFinalBasis = Notional + AccrueOnGrid(start, new DateTime(2026, 8, 18), AccrualBoundary.Both, hist, period: 14); // 8/19 重置并本后基数
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, hist[^1], periodDays: 14), elapsed,
preEod: null, unwind: unwind, settled: false, spread: hist[^1],
interestPrincipal: replayFinalBasis, maturityCalcLast: false); // 不算尾合约、14天重置(对应 UAT tradeId=2447 口径)
var full = AccrueOnGrid(start, maturity, AccrualBoundary.StartOnly, hist, period: 14);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"无preEod+已有重置:兜底取事件基数后 ① 精确,全期=实结+罚息(修复前差≈3.17元)");
}
[TestMethod]
public void _无preEod_恒等式成立()
{