Files
zszq-trs/UnitTestProject/Modules/SwapModule/Penalty/PenaltyBoundaryMatrixTest.cs
T

212 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Modules.SwapModule.Penalty;
namespace UnitTestProject.Modules.SwapModule.Penalty
{
/// <summary>
/// EQD-6977 罚息边界矩阵测试(全部断言金标准恒等式:全期 = 实结 + 罚息)。
///
/// 覆盖易错边界:
/// ① 平仓日恰为重置日(算尾/不算尾)——重置日快照基数还是上一段的,①须取 InterestIncomeSum
/// ② 重置日前一日平仓(② 几乎整段、窗口首段 0 天);
/// ③ 到期日恰为重置日(末段 [到期,到期] 1 天);
/// ④ 锚点偏离(td.StartDate=7/31 但腿 PosiStartDate=8/3 的延期/存续腿——重置网格整体不同);
/// ⑤ 起息日当天平仓(无 preEod)。
///
/// 一致性前提(与现实世界对齐):冻结利率 = 当前重置区间(含 unwind-1 的区间)的在役利率,
/// 即"历史末段利率 = 冻结利率";历史各段定盘不同(体现真实 FR007 利率历史)。
/// </summary>
[TestClass]
public class PenaltyBoundaryMatrixTest
{
private const decimal Notional = 100_000_000m;
private const int AnnualDays = 365;
private static readonly decimal[] Hist = { 0.0310m, 0.0420m, 0.0530m, 0.0225m }; // 7/31 / 8/7 / 8/14 / 8/21 段
private static readonly decimal Frozen = Hist[^1]; // 冻结 = 当前区间在役利率 = 历史末段
/// <summary>指定重置网格上的复利重放 [gridStart, end];超出所给历史段后沿用冻结利率。</summary>
private static decimal AccrueOnGrid(DateTime gridStart, DateTime end, AccrualBoundary boundary,
decimal[] histRates, decimal notional = Notional, int period = 7)
{
var frozen = histRates[^1];
var segs = new List<(DateTime, decimal)>();
var i = 0;
for (var d = gridStart; d <= end; d = d.AddDays(period))
segs.Add((d, i < histRates.Length ? histRates[i++] : frozen));
return CompoundInterestAccrual.AccruePeriod(
notional: notional, segmentRates: segs,
startDate: gridStart, endDate: end,
boundary: boundary, annualDays: AnnualDays, isAnnualized: true,
resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
finalBasis: out _).Accrued;
}
private static trade CreateTrade(DateTime startDate, DateTime maturity)
=> new()
{
id = 1, TradeNumber = "UT-BOUNDARY", ClientId = 999998,
TradeType = "收益互换", TradeDate = startDate, StartDate = startDate,
ExerciseDate = maturity, TradeStatus = "确认成交", ValidState = "Valid"
};
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 = periodDays, interest_rule = 0,
FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
};
/// <summary>日终快照:TdInterestPrincipal=当日实际滚动基数、InterestIncomeSum=截至当日待实现利息。</summary>
private static eod_swap_position Snap(DateTime valueDate, decimal rollingBasis, decimal incomeSum)
=> new() { id = 9, PositionId = 1001, ValueDate = valueDate,
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,
decimal interestPrincipal = 0m, bool maturityCalcLast = true)
{
var e = new swap_flow_event
{
PositionId = p.id, InterestAmount = settledAmount, InterestFee = 0m,
InterestDirection = 1, InterestClosePnL = settledAmount,
InterestPrincipal = interestPrincipal // 复利主路径下=重放末次并本金后基数(=被平份额本金+①)
};
PenaltyInterestFeeMerger.Merge(
td, new List<swap_position> { p }, new List<swap_flow_event> { e },
unwind, AnnualDays, settled, maturityCalcLast: maturityCalcLast,
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
getSpread: _ => spread, getPreEod: _ => preEod, tryGetFixing: (d, c) => spread);
return e.InterestFee;
}
[TestMethod]
public void 平仓日恰为重置日_算尾_恒等式成立()
{
var start = new DateTime(2026, 7, 31); var unwind = new DateTime(2026, 8, 21); var maturity = new DateTime(2026, 8, 31);
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.Both, Hist); // [7/31..8/21](末日=重置日,1 天)
var basisThru813 = AccrueOnGrid(start, new DateTime(2026, 8, 13), AccrualBoundary.Both, Hist); // 8/14 起段基数
var incomeSum = AccrueOnGrid(start, unwind.AddDays(-1), AccrualBoundary.Both, Hist); // 8/20 待实现
// 前提自检:重置日快照基数(8/14段)≠今日应并入额(8/20待实现),旧公式(basis−P)必错——用例有鉴别力
Assert.AreNotEqual((double)basisThru813, (double)incomeSum, 1000d, "快照基数与重置日应并入额应显著不同");
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, Frozen), elapsed,
Snap(unwind.AddDays(-1), Notional + basisThru813, incomeSum), unwind, settled: true, spread: Frozen);
var full = AccrueOnGrid(start, maturity, AccrualBoundary.Both, Hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"重置日当天平仓(算尾):① 须取 InterestIncomeSum,全期=实结+罚息");
}
[TestMethod]
public void 平仓日恰为重置日_不算尾_恒等式成立()
{
var start = new DateTime(2026, 7, 31); var unwind = new DateTime(2026, 8, 21); var maturity = new DateTime(2026, 8, 31);
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.StartOnly, Hist); // [7/31..8/20]
var incomeSum = elapsed; // 不算尾时实结=8/20待实现
var basisThru813 = AccrueOnGrid(start, new DateTime(2026, 8, 13), AccrualBoundary.Both, Hist);
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, Frozen), elapsed,
Snap(unwind.AddDays(-1), Notional + basisThru813, incomeSum), unwind, settled: false, spread: Frozen);
var full = AccrueOnGrid(start, maturity, AccrualBoundary.Both, Hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"重置日当天平仓(不算尾):②=0,罚息含平仓日,全期=实结+罚息");
}
[TestMethod]
public void 重置日前一日平仓_段内几乎整段承接_恒等式成立()
{
var start = new DateTime(2026, 7, 31); var unwind = new DateTime(2026, 8, 27); var maturity = new DateTime(2026, 8, 31);
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.Both, Hist); // [7/31..8/27],段内已计 8/21..8/27
var basisThru820 = AccrueOnGrid(start, new DateTime(2026, 8, 20), AccrualBoundary.Both, Hist); // 8/21 起段基数
var incomeSum = AccrueOnGrid(start, unwind.AddDays(-1), AccrualBoundary.Both, Hist);
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, Frozen), elapsed,
Snap(unwind.AddDays(-1), Notional + basisThru820, incomeSum), unwind, settled: true, spread: Frozen);
var full = AccrueOnGrid(start, maturity, AccrualBoundary.Both, Hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"重置日前一日平仓:窗口首段 0 天、② 于 8/28 整段并入,全期=实结+罚息");
}
[TestMethod]
public void 到期日恰为重置日_末段一天_恒等式成立()
{
// 8/18 平仓:当前区间为 8/14 段(r3) → 冻结利率=r3=历史末段;到期 9/4 恰为重置日(末段 [9/4,9/4] 1 天)
var start = new DateTime(2026, 7, 31); var unwind = new DateTime(2026, 8, 18); var maturity = new DateTime(2026, 9, 4);
var hist = new decimal[] { 0.0310m, 0.0420m, 0.0530m }; // 7/31 / 8/7 / 8/14(=冻结 5.3%)
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.Both, hist);
var basisThru813 = AccrueOnGrid(start, new DateTime(2026, 8, 13), AccrualBoundary.Both, hist);
var incomeSum = AccrueOnGrid(start, unwind.AddDays(-1), AccrualBoundary.Both, hist);
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, hist[^1]), elapsed,
Snap(unwind.AddDays(-1), Notional + basisThru813, incomeSum), unwind, settled: true, spread: hist[^1]);
var full = AccrueOnGrid(start, maturity, AccrualBoundary.Both, hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"到期日=重置日:末段 [9/4,9/4] 1 天收尾,全期=实结+罚息");
}
[TestMethod]
public void 锚点偏离_延期腿按腿起息日网格_恒等式成立()
{
// 交易起始 7/31,但腿 PosiStartDate=8/3(延期/存续腿)→ 真实重置网格 8/10/8/17/8/24/8/31
var tradeStart = new DateTime(2026, 7, 31); var posiStart = new DateTime(2026, 8, 3);
var unwind = new DateTime(2026, 8, 19); var maturity = new DateTime(2026, 9, 3);
var hist = new decimal[] { 0.0300m, 0.0400m, 0.0225m }; // 8/3 / 8/10 / 8/17(=冻结) 三段历史
var elapsed = AccrueOnGrid(posiStart, unwind, AccrualBoundary.Both, hist);
var basisThru816 = AccrueOnGrid(posiStart, new DateTime(2026, 8, 16), AccrualBoundary.Both, hist); // 8/17 起段基数
var incomeSum = AccrueOnGrid(posiStart, unwind.AddDays(-1), AccrualBoundary.Both, hist);
var fee = RunFee(CreateTrade(tradeStart, maturity), CompoundLeg(posiStart, maturity, hist[^1]), elapsed,
Snap(unwind.AddDays(-1), Notional + basisThru816, incomeSum), unwind, settled: true, spread: hist[^1]);
var full = AccrueOnGrid(posiStart, maturity, AccrualBoundary.Both, hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"锚点偏离:罚息分段/重置日判定必须用 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_恒等式成立()
{
// 首日平仓:当前区间=首段(r1),无 preEod 时取价委托返回首段定盘 → 冻结利率=r1,全程恒率
var start = new DateTime(2026, 7, 31); var maturity = new DateTime(2026, 8, 31);
var hist = new decimal[] { 0.0310m };
var elapsed = AccrueOnGrid(start, start, AccrualBoundary.Both, hist); // 首日 1 天
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, hist[^1]), elapsed,
preEod: null, unwind: start, settled: true, spread: hist[^1]);
var full = AccrueOnGrid(start, maturity, AccrualBoundary.Both, hist);
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
"起息日当天平仓:①=0、②=首日利息于 8/7 并入,全期=实结+罚息");
}
}
}