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

113 lines
5.7 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 罚息接缝 headless 测试(无 DBspread/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;
}
}
}