test(swap): EQD-6977 罚息接缝 headless 单测——固定/浮动腿追加 IsPenaltyInterest 标记 + 承接恒等式(全期=已结+罚息)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Modules.SwapModule.Penalty;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
{
|
||||
/// <summary>
|
||||
/// EQD-6977 罚息接缝 headless 测试(无 DB:spread/preEod/取价 全部以委托注入)。
|
||||
/// 锁定:Append 在融资腿上追加 IsPenaltyInterest=1 的同构罚息流;承接恒等式(全期=已结+罚息)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class PenaltyInterestAppenderTest
|
||||
{
|
||||
private const decimal Notional = 100_000_000m;
|
||||
private const decimal Rate = 0.0225m; // 冻结 all-in 年化
|
||||
private const int AnnualDays = 365;
|
||||
private static readonly DateTime StartDate = new(2026, 7, 31);
|
||||
private static readonly DateTime MaturityDate = new(2026, 8, 31);
|
||||
private static readonly DateTime UnwindDate = new(2026, 8, 25);
|
||||
|
||||
private static trade CreateTrade()
|
||||
=> new()
|
||||
{
|
||||
id = 1, TradeNumber = "UT-APPEND", ClientId = 999998,
|
||||
TradeType = "收益互换", StartDate = StartDate, TradeDate = StartDate,
|
||||
ExerciseDate = MaturityDate, TradeStatus = "确认成交", ValidState = "Valid"
|
||||
};
|
||||
|
||||
private static swap_position FixedLeg(InterestTypeEnum interestType)
|
||||
=> new()
|
||||
{
|
||||
id = 1001, SwapTradeId = 1, PosiDirection = 0, InterestDirection = 1,
|
||||
InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = Rate,
|
||||
InterestPrincipalFix = Notional, PosiStartDate = StartDate, PosiMatuirityDate = MaturityDate,
|
||||
IsInitial = true, Invalid = false, InterestType = (int)interestType,
|
||||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||||
FloatRateUnderlyingCode = null, InterestSwapInterval = "[]"
|
||||
};
|
||||
|
||||
private static void RunAppend(swap_position p, out List<swap_flow_event> interests,
|
||||
Func<swap_position, decimal>? getSpread = null)
|
||||
{
|
||||
getSpread ??= _ => Rate;
|
||||
interests = new List<swap_flow_event>();
|
||||
PenaltyInterestAppender.Append(
|
||||
CreateTrade(), new List<swap_position> { p }, interests, UnwindDate, AnnualDays,
|
||||
unwindDaySettled: true, maturityCalcLast: true,
|
||||
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
|
||||
getSpread: getSpread,
|
||||
getPreEodFloatRate: _ => null,
|
||||
tryGetFixing: (d, code) => (decimal?)Rate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 固定腿_single利_追加罚息流且标记列()
|
||||
{
|
||||
RunAppend(FixedLeg(InterestTypeEnum.单利), out var interests);
|
||||
|
||||
Assert.AreEqual(1, interests.Count, "应恰好追加 1 笔罚息");
|
||||
var e = interests[0];
|
||||
Assert.AreEqual(1, e.IsPenaltyInterest, "IsPenaltyInterest 应置 1");
|
||||
Assert.AreEqual(SwapPenaltyInterestCalculator.PenaltyEventReason, e.EventReason, "事件原因=罚息");
|
||||
// 窗口 (8/25, 8/31] = 6 天(算尾平仓日 + 到期算尾)
|
||||
var expected = Rate * Notional * 6m / AnnualDays;
|
||||
Assert.AreEqual((double)expected, (double)e.InterestAmount, 0.0001, "单利罚息=利率×本金×天数/基准");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 浮动腿_经取价委托解析冻结率并追加()
|
||||
{
|
||||
var p = FixedLeg(InterestTypeEnum.单利);
|
||||
p.FloatRateUnderlyingCode = "FR007"; // 浮动腿:走 tryGetFixing
|
||||
p.InterestMode = (int)InterestModeEnum.标的期初全价;
|
||||
// 浮动腿 all-in = 加点利差(spread) + 指数定盘(fixing);零利差时与固定腿同值
|
||||
RunAppend(p, out var interests, getSpread: _ => 0m);
|
||||
|
||||
Assert.AreEqual(1, interests.Count);
|
||||
Assert.AreEqual(1, interests[0].IsPenaltyInterest);
|
||||
// 取价委托恒返回 Rate → all-in = 0 + Rate,与固定腿同值
|
||||
var expected = Rate * Notional * 6m / AnnualDays;
|
||||
Assert.AreEqual((double)expected, (double)interests[0].InterestAmount, 0.0001, "浮动腿冻结率=取价委托值(零利差)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 复利_承接恒等式_全期等于已结加罚息()
|
||||
{
|
||||
var p = FixedLeg(InterestTypeEnum.复利);
|
||||
RunAppend(p, out var interests);
|
||||
|
||||
Assert.AreEqual(1, interests.Count);
|
||||
Assert.AreEqual(1, interests[0].IsPenaltyInterest);
|
||||
|
||||
// 与金标准测试同款 CompoundAccruedTo(AccrualBoundary.Both):全期=已结+罚息 恒等式
|
||||
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both);
|
||||
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + interests[0].InterestAmount), 0.0001,
|
||||
"全期(冻结率重放) 应等于 已结 + 罚息;承接量推导正确");
|
||||
}
|
||||
|
||||
/// <summary>常率复利重放 [StartDate, endDate],重置段 = 每 7 天(与金标准测试一致)。</summary>
|
||||
private static decimal CompoundAccruedTo(DateTime endDate, AccrualBoundary boundary)
|
||||
{
|
||||
var segs = new List<(DateTime, decimal)>();
|
||||
for (var d = StartDate; d <= endDate; d = d.AddDays(7)) segs.Add((d, Rate));
|
||||
return CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional, segmentRates: segs,
|
||||
startDate: StartDate, endDate: endDate,
|
||||
boundary: boundary, annualDays: AnnualDays, isAnnualized: true,
|
||||
resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
|
||||
finalBasis: out _).Accrued;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user