fix(swap): EQD-6977 无日终快照时承接①兜底取事件基数——复利重放把 normalEvent.InterestPrincipal 写为末次并本金后基数(=被平份额本金+①),同为实际值;UAT实测(无快照+已有8/19重置)原①=0少算≈3.17元已消除;重置日+无快照退化场景补告警trace;新增兜底路径恒等式测试(23/23)
This commit is contained in:
@@ -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_恒等式成立()
|
||||
{
|
||||
|
||||
@@ -89,8 +89,10 @@ public static class PenaltyInterestFeeMerger
|
||||
}
|
||||
|
||||
// 复利承接:实际滚动基数中已并入部分(①)+ 段内实际已计利息(②)。单利无并本金语义恒 0。
|
||||
// ① 的取值依赖平仓日是否为重置日(数据契约):
|
||||
// 段中平仓:昨日快照 TdInterestPrincipal 即当前段滚动基数(=本金+①),直接作差;
|
||||
// ① 的取值依赖平仓日是否为重置日、有无日终快照(数据契约):
|
||||
// 段中平仓 + 有快照:TdInterestPrincipal 即当前段滚动基数(=本金+①),直接作差;
|
||||
// 段中平仓 + 无快照:兜底取 normalEvent.InterestPrincipal——复利重放(CalcDailyCompoundInterest)
|
||||
// 会把它写为末次并本金后的基数(=被平份额本金+①),同样是实际值而非推导值;
|
||||
// 重置日当天平仓:快照基数仍是【上一段】的(今日并入尚未发生),须改取
|
||||
// preEod.InterestIncomeSum(昨日全部待实现利息 = 今日并入新段基数的那部分)。
|
||||
decimal capitalized = 0m, carryIn = 0m;
|
||||
@@ -98,9 +100,20 @@ public static class PenaltyInterestFeeMerger
|
||||
{
|
||||
var periodDays = position.interest_rest_days ?? 1;
|
||||
var unwindOnResetDay = SwapDealService.IsResetDay(unwindDate, position.PosiStartDate, periodDays);
|
||||
capitalized = unwindOnResetDay
|
||||
? (preEod?.InterestIncomeSum ?? 0m) * share
|
||||
: Math.Max(0m, (preEod?.TdInterestPrincipal ?? 0m) * share - closePrincipal);
|
||||
if (unwindOnResetDay)
|
||||
{
|
||||
capitalized = (preEod?.InterestIncomeSum ?? 0m) * share;
|
||||
if (preEod == null && (unwindDate - position.PosiStartDate).Days >= periodDays)
|
||||
trace?.Note($"PENALTY|p{position.id} 注意 无preEod且平仓日=重置日:①退化0(此前重置并入额缺失,请核对日终归档完整性)");
|
||||
}
|
||||
else if (preEod != null)
|
||||
{
|
||||
capitalized = Math.Max(0m, preEod.TdInterestPrincipal * share - closePrincipal);
|
||||
}
|
||||
else
|
||||
{
|
||||
capitalized = Math.Max(0m, normalEvent.InterestPrincipal - closePrincipal);
|
||||
}
|
||||
// ① 不得超过实结金额(数据异常时钳制并留痕,避免负②进入计息)
|
||||
if (capitalized > Math.Max(0m, normalEvent.InterestAmount))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user