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

177 lines
9.1 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;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Modules.SwapModule.Penalty;
namespace UnitTestProject.Modules.SwapModule.Penalty
{
/// <summary>
/// EQD-6977 平仓罚息计算器契约测试(返回罚息金额)。
///
/// 金标准恒等式(需求核心语义,2026-08-20 裁定的精确续接口径):
/// 全期利息 = 平仓日已结利息 + 罚息金额
/// 历史口径:7/31 起息、8/31 到期、7 天重置(8/7/8/14/8/21/8/28)、8/25 提前终止
/// (平仓日落在 8/21–8/28 重置段中间——复利承接两分量的关键场景)。
/// </summary>
[TestClass]
public class SwapPenaltyInterestCalculatorTest
{
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 readonly DateTime LastResetBeforeUnwind = new(2026, 8, 21);
private static swap_position CreatePosition(InterestTypeEnum interestType, SwapDirectionEnum direction)
=> new()
{
id = 1001, SwapTradeId = 1, PosiDirection = 0,
InterestDirection = (int)direction,
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 AccrualPolicy Policy(swap_position p)
=> AccrualPolicy.BuildEod(p, AnnualDays, p.InterestType == (int)InterestTypeEnum.复利);
/// <summary>常率复利重放 [7/31, 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;
}
private static decimal CalcCompoundPenalty(
swap_position p, decimal closePrincipal, bool settled, decimal capitalized, decimal carryIn)
=> SwapPenaltyInterestCalculator.CalcPenaltyAmount(
p, closePrincipal,
unwindDate: UnwindDate, maturityDate: MaturityDate,
unwindDaySettled: settled, maturityCalcLast: true,
capitalizedInterest: capitalized, carryInInterest: carryIn,
frozenRate: FundingLegRate.Fixed(Rate),
policy: Policy(p), resetAnchor: StartDate);
[TestMethod]
public void 金标准恒等式_复利_全期等于已结加罚息()
{
var p = CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付);
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both);
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both);
var carryIn = elapsed - capitalized;
var penalty = CalcCompoundPenalty(p, Notional, settled: true, capitalized, carryIn);
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both);
Assert.AreEqual((double)full, (double)(elapsed + penalty), 0.0001,
$"全期({full}) 应等于 已结({elapsed}) + 罚息({penalty});承接①={capitalized} ②={carryIn}");
}
[TestMethod]
public void 金标准恒等式_复利_不算尾平仓日()
{
// 不算尾:正常结算未计 8/25 → 罚息含 8/25IncludeStart=true),承接②少一天
var p = CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付);
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.StartOnly);
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both);
var carryIn = elapsed - capitalized;
var penalty = CalcCompoundPenalty(p, Notional, settled: false, capitalized, carryIn);
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both);
Assert.AreEqual((double)full, (double)(elapsed + penalty), 0.0001,
"不算尾时罚息窗口须补回平仓日,恒等式仍成立");
}
[TestMethod]
public void 单利固定腿_剩余期限利息等于公式()
{
// 需求 2.2.1:剩余利息 = 固定 × 名义本金 × 剩余天数 / 计息基准
// 算尾平仓日 + 到期算尾:窗口 (8/25, 8/31] = 6 天
var amount = SwapPenaltyInterestCalculator.CalcPenaltyAmount(
CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付), Notional,
unwindDate: UnwindDate, maturityDate: MaturityDate,
unwindDaySettled: true, maturityCalcLast: true,
capitalizedInterest: 0m, carryInInterest: 0m,
frozenRate: FundingLegRate.Fixed(Rate),
policy: Policy(CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付)),
resetAnchor: StartDate);
var expected = Rate * Notional * 6m / AnnualDays;
Assert.AreEqual((double)expected, (double)amount, 0.0001, "6 天 = 8/26..8/31");
}
[TestMethod]
public void 边界四象限_剩余天数口径正确()
{
var p = CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付);
// 8/26..8/31 共 6 个计息日候选;IncludeStart 加 8/25、IncludeEnd 加 8/31 由约定裁剪
var cases = new (bool settled, bool calcLast, int days)[]
{
(true, true, 6), // (8/25, 8/31] 8/26..8/31
(true, false, 5), // (8/25, 8/31) 8/26..8/30
(false, true, 7), // [8/25, 8/31] 8/25..8/31
(false, false, 6), // [8/25, 8/31) 8/25..8/30
};
foreach (var (settled, calcLast, days) in cases)
{
var amount = SwapPenaltyInterestCalculator.CalcPenaltyAmount(
p, Notional,
unwindDate: UnwindDate, maturityDate: MaturityDate,
unwindDaySettled: settled, maturityCalcLast: calcLast,
capitalizedInterest: 0m, carryInInterest: 0m,
frozenRate: FundingLegRate.Fixed(Rate),
policy: Policy(p), resetAnchor: StartDate);
var expected = Rate * Notional * days / AnnualDays;
Assert.AreEqual((double)expected, (double)amount, 0.0001,
$"settled={settled}, calcLast={calcLast} → {days} 天");
}
}
[TestMethod]
public void 部分平仓_仅被平份额计罚息()
{
var p = CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付);
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both);
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both);
var carryIn = elapsed - capitalized;
// 被平 30%:本金与两承接量同比缩放,罚息应恰为全额的 30%
var full = CalcCompoundPenalty(p, Notional, true, capitalized, carryIn);
var partial = CalcCompoundPenalty(p, Notional * 0.3m, true, capitalized * 0.3m, carryIn * 0.3m);
Assert.AreEqual((double)(full * 0.3m), (double)partial, 0.0001,
"被平 30%(本金与承接量同比)罚息应恰为全额的 30%");
}
[TestMethod]
public void 零剩余期限_金额为零()
{
var amount = SwapPenaltyInterestCalculator.CalcPenaltyAmount(
CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付), Notional,
unwindDate: MaturityDate, maturityDate: MaturityDate,
unwindDaySettled: true, maturityCalcLast: true,
capitalizedInterest: 90_000m, carryInInterest: 10_000m,
frozenRate: FundingLegRate.Fixed(Rate),
policy: Policy(CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付)),
resetAnchor: StartDate);
Assert.AreEqual(0m, amount, "平仓日=到期日无剩余期限,罚息为 0(承接量不产生利息)");
}
}
}