fix(swap): EQD-6977 罚息精确续接落地——承接量改用实际计息状态(preEod基数+事件实结,弃冻结重放)、锚点统一PosiStartDate、罚息并入既有利息流InterestFee(去独立事件/去is_penalty_interest列及DDL/回退closeList与EOD两处守护)、接缝加eventType==平仓守卫、trace逐腿全程落盘、新增多区间不同定盘恒等式测试(旧冻结重放实现必挂)
This commit is contained in:
@@ -1,112 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Modules.SwapModule.Penalty;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
{
|
||||
/// <summary>
|
||||
/// EQD-6977 罚息接缝 headless 测试(无 DB:spread/preEod/取价 全部以委托注入)。
|
||||
/// 锁定:Merge 把罚息金额并入既有利息事件的 InterestFee(不新增事件、不改 InterestAmount);
|
||||
/// 承接量取实际计息状态(preEod 基数 + 事件实结金额)——含【多区间不同定盘】恒等式钉死,
|
||||
/// 该用例在"冻结利率重放推导承接量"的旧实现下必挂(FR007 真实利率历史场景)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class PenaltyInterestFeeMergerTest
|
||||
{
|
||||
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 trade CreateTrade()
|
||||
=> new()
|
||||
{
|
||||
id = 1, TradeNumber = "UT-MERGE", ClientId = 999998,
|
||||
TradeType = "收益互换", StartDate = StartDate, TradeDate = StartDate,
|
||||
ExerciseDate = MaturityDate, TradeStatus = "确认成交", ValidState = "Valid"
|
||||
};
|
||||
|
||||
private static swap_position Leg(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 = "[]"
|
||||
};
|
||||
|
||||
/// <summary>正常平仓利息流(模拟 GetInterests 产出):InterestAmount=实结利息、InterestFee=0。</summary>
|
||||
private static swap_flow_event NormalEvent(decimal settledAmount)
|
||||
=> new() { PositionId = 1001, InterestAmount = settledAmount, InterestFee = 0m,
|
||||
InterestDirection = 1, InterestClosePnL = settledAmount }; // 模拟 GetInterests 已算好的 PnL(收取=+1)
|
||||
|
||||
private static eod_swap_position PreEod(decimal rollingBasis, decimal floatRate = 0m)
|
||||
=> new() { id = 9, PositionId = 1001, ValueDate = UnwindDate.AddDays(-1),
|
||||
TdInterestPrincipal = rollingBasis, FloatRate = floatRate };
|
||||
|
||||
private static void RunMerge(
|
||||
swap_position p, swap_flow_event normalEvent, eod_swap_position? preEod,
|
||||
Func<swap_position, decimal>? getSpread = null, Func<DateTime, string, decimal?>? tryGetFixing = null)
|
||||
{
|
||||
getSpread ??= _ => Rate;
|
||||
tryGetFixing ??= (d, code) => Rate;
|
||||
PenaltyInterestFeeMerger.Merge(
|
||||
CreateTrade(), new List<swap_position> { p }, new List<swap_flow_event> { normalEvent },
|
||||
UnwindDate, AnnualDays,
|
||||
unwindDaySettled: true, maturityCalcLast: true,
|
||||
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
|
||||
getSpread: getSpread,
|
||||
getPreEod: _ => preEod,
|
||||
tryGetFixing: tryGetFixing);
|
||||
}
|
||||
|
||||
/// <summary>复利重放 [StartDate, endDate],重置段=每 7 天;分段利率由 rates 决定(rates.Count=1 时为常率)。</summary>
|
||||
private static decimal CompoundAccruedTo(DateTime endDate, AccrualBoundary boundary, params decimal[] rates)
|
||||
{
|
||||
var segs = new List<(DateTime, decimal)>();
|
||||
var i = 0;
|
||||
for (var d = StartDate; d <= endDate; d = d.AddDays(7))
|
||||
// 超出所给历史段后沿用最后区间利率——即“未来段冻结为最后区间利率”的语义(勿循环回绕)
|
||||
segs.Add((d, rates.Length == 1 ? rates[0] : i < rates.Length ? rates[i++] : rates[^1]));
|
||||
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;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 单利固定腿_罚息并入InterestFee_不新增事件()
|
||||
{
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
RunMerge(Leg(InterestTypeEnum.单利), e, preEod: PreEod(Notional));
|
||||
|
||||
Assert.AreEqual(0d, (double)(e.InterestFee - Rate * Notional * 6m / AnnualDays), 0.0001,
|
||||
"罚息=利率×本金×6天/基准(窗口 (8/25, 8/31])");
|
||||
Assert.AreEqual(50_000d, (double)e.InterestAmount, 0.0001, "正常实结利息不受影响");
|
||||
Assert.AreEqual((double)(50_000m + e.InterestFee), (double)e.InterestClosePnL, 0.0001, "PnL=(实结+罚息)×方向(收取=+1)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 浮动腿_取价委托解析冻结率_并入费用()
|
||||
{
|
||||
var p = Leg(InterestTypeEnum.单利);
|
||||
p.FloatRateUnderlyingCode = "FR007";
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
// 无 preEod → 走取价委托:all-in = spread(0) + 定盘(Rate)
|
||||
RunMerge(p, e, preEod: null, getSpread: _ => 0m, tryGetFixing: (d, code) => Rate);
|
||||
|
||||
Assert.AreEqual(0d, (double)(e.InterestFee - Rate * Notional * 6m / AnnualDays), 0.0001,
|
||||
"浮动腿冻结率=取价委托值(零利差)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 复利常率_承接取实际状态_恒等式全期等于已结加罚息()
|
||||
{
|
||||
// 实际计息状态:preEod 滚动基数 = P + 已并入利息(截至 8/20);事件实结 = elapsed([7/31,8/25] Both)
|
||||
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both, Rate);
|
||||
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both, Rate);
|
||||
var e = NormalEvent(elapsed);
|
||||
RunMerge(Leg(InterestTypeEnum.复利), e, preEod: PreEod(Notional + capitalized));
|
||||
|
||||
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both, Rate);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + e.InterestFee), 0.0001,
|
||||
"常率下 全期 = 已结(事件实结) + 罚息(InterestFee)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 复利多区间不同定盘_承接取实际状态_恒等式仍成立()
|
||||
{
|
||||
// 真实 FR007 世界:四个历史重置区间定盘各不相同,冻结利率=最后区间(2.25%)
|
||||
var r1 = 0.0310m; var r2 = 0.0420m; var r3 = 0.0530m; var r4 = Rate; // r4=0.0225 冻结值
|
||||
var rates = new[] { r1, r2, r3, r4 };
|
||||
|
||||
// 实际计息状态(与 GetInterests 重放同源):
|
||||
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both, rates); // 已并入 8/21 重置日
|
||||
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both, rates); // 实结(含 8/21..8/25 段内利息)
|
||||
var e = NormalEvent(elapsed);
|
||||
RunMerge(Leg(InterestTypeEnum.复利), e, preEod: PreEod(Notional + capitalized));
|
||||
|
||||
// 全期参照:历史段按各自真实定盘、8/28 起的未来段按冻结利率(=r4,恰好同段延续)
|
||||
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both, rates);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + e.InterestFee), 0.01,
|
||||
"多区间不同定盘下 全期(历史实率+未来冻结) = 实结 + 罚息——承接量必须来自实际状态");
|
||||
// 反证旧缺陷:冻结重放推导的承接①(全程 r4)≠ 实际①(分段实率),差额显著
|
||||
var frozenReplayCapitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both, Rate);
|
||||
Assert.AreNotEqual((double)capitalized, (double)frozenReplayCapitalized, 1000d,
|
||||
"前提自检:分段实率与冻结重放的已并入利息应显著不同(否则用例失去鉴别力)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 复利无preEod_承接退化为实结全额_可计算不崩溃()
|
||||
{
|
||||
var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both, Rate);
|
||||
var e = NormalEvent(elapsed);
|
||||
RunMerge(Leg(InterestTypeEnum.复利), e, preEod: null);
|
||||
|
||||
Assert.IsTrue(e.InterestFee > 0m, "无 preEod(首日平仓等)仍可计算罚息");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 冻结利率解析失败_跳过该腿不阻断()
|
||||
{
|
||||
var p = Leg(InterestTypeEnum.单利);
|
||||
p.FloatRateUnderlyingCode = "FR007";
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
RunMerge(p, e, preEod: null, getSpread: _ => 0m, tryGetFixing: (d, code) => null);
|
||||
|
||||
Assert.AreEqual(0m, e.InterestFee, "缺价跳过:不加罚息、不抛异常");
|
||||
Assert.AreEqual(50_000d, (double)e.InterestAmount, 0.0001, "正常平仓不受影响");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ using YLErp.Modules.SwapModule.Penalty;
|
||||
namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
{
|
||||
/// <summary>
|
||||
/// EQD-6977 平仓罚息计算器契约测试。
|
||||
/// 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>
|
||||
@@ -25,14 +25,6 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
private static readonly DateTime UnwindDate = new(2026, 8, 25);
|
||||
private static readonly DateTime LastResetBeforeUnwind = new(2026, 8, 21);
|
||||
|
||||
private static trade CreateTrade()
|
||||
=> new()
|
||||
{
|
||||
id = 1, TradeNumber = "UT-EQD6977", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||||
ExerciseDate = MaturityDate, TradeStatus = "确认成交", ValidState = "Valid"
|
||||
};
|
||||
|
||||
private static swap_position CreatePosition(InterestTypeEnum interestType, SwapDirectionEnum direction)
|
||||
=> new()
|
||||
{
|
||||
@@ -65,16 +57,15 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
finalBasis: out _).Accrued;
|
||||
}
|
||||
|
||||
private static swap_flow_event CalcCompoundPenalty(
|
||||
private static decimal CalcCompoundPenalty(
|
||||
swap_position p, decimal closePrincipal, bool settled, decimal capitalized, decimal carryIn)
|
||||
=> SwapPenaltyInterestCalculator.CalcPenalty(
|
||||
CreateTrade(), p, closePrincipal: closePrincipal,
|
||||
=> 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,
|
||||
eventType: (int)SwapEventTypeEnum.平仓);
|
||||
policy: Policy(p), resetAnchor: StartDate);
|
||||
|
||||
[TestMethod]
|
||||
public void 金标准恒等式_复利_全期等于已结加罚息()
|
||||
@@ -84,11 +75,11 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both);
|
||||
var carryIn = elapsed - capitalized;
|
||||
|
||||
var e = CalcCompoundPenalty(p, Notional, settled: true, capitalized, carryIn);
|
||||
var penalty = CalcCompoundPenalty(p, Notional, settled: true, capitalized, carryIn);
|
||||
|
||||
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + e.InterestAmount), 0.0001,
|
||||
$"全期({full}) 应等于 已结({elapsed}) + 罚息({e.InterestAmount});承接①={capitalized} ②={carryIn}");
|
||||
Assert.AreEqual((double)full, (double)(elapsed + penalty), 0.0001,
|
||||
$"全期({full}) 应等于 已结({elapsed}) + 罚息({penalty});承接①={capitalized} ②={carryIn}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -100,10 +91,10 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
var capitalized = CompoundAccruedTo(LastResetBeforeUnwind.AddDays(-1), AccrualBoundary.Both);
|
||||
var carryIn = elapsed - capitalized;
|
||||
|
||||
var e = CalcCompoundPenalty(p, Notional, settled: false, capitalized, carryIn);
|
||||
var penalty = CalcCompoundPenalty(p, Notional, settled: false, capitalized, carryIn);
|
||||
|
||||
var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + e.InterestAmount), 0.0001,
|
||||
Assert.AreEqual((double)full, (double)(elapsed + penalty), 0.0001,
|
||||
"不算尾时罚息窗口须补回平仓日,恒等式仍成立");
|
||||
}
|
||||
|
||||
@@ -112,26 +103,23 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
{
|
||||
// 需求 2.2.1:剩余利息 = 固定 × 名义本金 × 剩余天数 / 计息基准
|
||||
// 算尾平仓日 + 到期算尾:窗口 (8/25, 8/31] = 6 天
|
||||
var e = SwapPenaltyInterestCalculator.CalcPenalty(
|
||||
CreateTrade(), CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付),
|
||||
closePrincipal: Notional,
|
||||
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,
|
||||
eventType: (int)SwapEventTypeEnum.平仓);
|
||||
resetAnchor: StartDate);
|
||||
|
||||
var expected = Rate * Notional * 6m / AnnualDays;
|
||||
Assert.AreEqual((double)expected, (double)e.InterestAmount, 0.0001, "6 天 = 8/26..8/31");
|
||||
Assert.AreEqual((double)expected, (double)amount, 0.0001, "6 天 = 8/26..8/31");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 边界四象限_剩余天数口径正确()
|
||||
{
|
||||
var p = CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付);
|
||||
var td = CreateTrade();
|
||||
// 8/26..8/31 共 6 个计息日候选;IncludeStart 加 8/25、IncludeEnd 加 8/31 由约定裁剪
|
||||
var cases = new (bool settled, bool calcLast, int days)[]
|
||||
{
|
||||
@@ -142,16 +130,15 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
};
|
||||
foreach (var (settled, calcLast, days) in cases)
|
||||
{
|
||||
var e = SwapPenaltyInterestCalculator.CalcPenalty(
|
||||
td, p, closePrincipal: Notional,
|
||||
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,
|
||||
eventType: (int)SwapEventTypeEnum.平仓);
|
||||
policy: Policy(p), resetAnchor: StartDate);
|
||||
var expected = Rate * Notional * days / AnnualDays;
|
||||
Assert.AreEqual((double)expected, (double)e.InterestAmount, 0.0001,
|
||||
Assert.AreEqual((double)expected, (double)amount, 0.0001,
|
||||
$"settled={settled}, calcLast={calcLast} → {days} 天");
|
||||
}
|
||||
}
|
||||
@@ -168,46 +155,22 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
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.InterestAmount * 0.3m), (double)partial.InterestAmount, 0.0001,
|
||||
Assert.AreEqual((double)(full * 0.3m), (double)partial, 0.0001,
|
||||
"被平 30%(本金与承接量同比)罚息应恰为全额的 30%");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 事件字段_与正常利息流同构_罚息原因与方向盈亏()
|
||||
{
|
||||
var e = SwapPenaltyInterestCalculator.CalcPenalty(
|
||||
CreateTrade(), CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付),
|
||||
closePrincipal: Notional,
|
||||
unwindDate: UnwindDate, maturityDate: MaturityDate,
|
||||
unwindDaySettled: true, maturityCalcLast: true,
|
||||
capitalizedInterest: 0m, carryInInterest: 0m,
|
||||
frozenRate: FundingLegRate.Fixed(Rate),
|
||||
policy: Policy(CreatePosition(InterestTypeEnum.单利, SwapDirectionEnum.支付)),
|
||||
resetAnchor: StartDate,
|
||||
eventType: (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
Assert.AreEqual(SwapPenaltyInterestCalculator.PenaltyEventReason, e.EventReason, "事件原因=罚息");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, e.EventType, "事件类型=平仓(下游聚合无差别)");
|
||||
Assert.AreEqual(UnwindDate, e.UnwindDate, "UnwindDate=平仓日(不伪造成到期日)");
|
||||
Assert.AreEqual((int)SwapFlowDateStateEnum.完成, e.DataState);
|
||||
Assert.AreEqual((double)e.InterestAmount, (double)(-e.InterestClosePnL), 0.0001,
|
||||
"支付方向:InterestClosePnL = InterestAmount × (-1)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 零剩余期限_金额为零()
|
||||
{
|
||||
var e = SwapPenaltyInterestCalculator.CalcPenalty(
|
||||
CreateTrade(), CreatePosition(InterestTypeEnum.复利, SwapDirectionEnum.支付),
|
||||
closePrincipal: Notional,
|
||||
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,
|
||||
eventType: (int)SwapEventTypeEnum.平仓);
|
||||
Assert.AreEqual(0m, e.InterestAmount, "平仓日=到期日无剩余期限,罚息为 0(承接量不产生利息)");
|
||||
resetAnchor: StartDate);
|
||||
Assert.AreEqual(0m, amount, "平仓日=到期日无剩余期限,罚息为 0(承接量不产生利息)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user