feat(swap): EQD-6977 复利纯函数支持 carryInInterest——窗口前已结利息随首个重置日资本化并持续留在基数(与全窗口重放轨迹严格一致);默认0零行为变化,含数学不变量测试
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// EQD-6977 carryInInterest 契约测试:
|
||||
/// 1) 默认 0 与旧逐日循环逐位一致(加参零行为变化的安全证明);
|
||||
/// 2) carry-in 仅在【首个重置日】并入计息基数(非窗口首日起息)——
|
||||
/// 与"持有至到期"全期轨迹对齐的数学不变量:增量 = carryIn × 后续段日利率 × 后续段天数。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CompoundCarryInTest
|
||||
{
|
||||
private const decimal Notional = 100_000_000m;
|
||||
private const decimal Spread = 0.0025m;
|
||||
private const int AnnualDays = 365;
|
||||
private static readonly DateTime StartDate = new(2026, 4, 21);
|
||||
private static readonly DateTime EndDate = new(2026, 5, 11); // 21天 = 3×7,末日是重置日
|
||||
|
||||
private static swap_position CreatePosition()
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
id = 1001, SwapTradeId = 1, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||||
InterestRateDefault = Spread,
|
||||
InterestPrincipalFix = Notional,
|
||||
PosiStartDate = StartDate, PosiMatuirityDate = StartDate.AddYears(1),
|
||||
IsInitial = true, Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||||
FloatRateUnderlyingCode = null,
|
||||
InterestSwapInterval = "[]"
|
||||
};
|
||||
}
|
||||
|
||||
private static List<(DateTime, decimal)> Segments()
|
||||
=> new()
|
||||
{
|
||||
(StartDate, Spread),
|
||||
(StartDate.AddDays(7), Spread),
|
||||
(StartDate.AddDays(14), Spread),
|
||||
};
|
||||
|
||||
private sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(CompoundCarryInTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void carryIn_默认省略_与旧逐日循环一致()
|
||||
{
|
||||
var position = CreatePosition();
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
new StubSvc().CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, 0m, 1m, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 省略 carryInInterest(默认 0)
|
||||
var r1 = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional, segmentRates: Segments(),
|
||||
startDate: StartDate, endDate: EndDate,
|
||||
boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true,
|
||||
resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
|
||||
finalBasis: out _);
|
||||
// 显式传 0 与省略等价
|
||||
var r2 = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional, segmentRates: Segments(),
|
||||
startDate: StartDate, endDate: EndDate,
|
||||
boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true,
|
||||
resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
|
||||
finalBasis: out _, trace: null, carryInInterest: 0m);
|
||||
|
||||
Assert.AreEqual((double)oldI, (double)r1.Accrued, 0.0000001, "省略 carryIn 与旧实现一致");
|
||||
Assert.AreEqual((double)r1.Accrued, (double)r2.Accrued, 0.0000001, "省略与显式0一致");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void carryIn_仅在首重置日起息_增量等于后续两段复利()
|
||||
{
|
||||
const decimal carryIn = 1_000_000m;
|
||||
|
||||
decimal Accrued(decimal c)
|
||||
=> CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional, segmentRates: Segments(),
|
||||
startDate: StartDate, endDate: EndDate,
|
||||
boundary: AccrualBoundary.Both, annualDays: AnnualDays, isAnnualized: true,
|
||||
resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
|
||||
finalBasis: out _, trace: null, carryInInterest: c).Accrued;
|
||||
|
||||
var delta = Accrued(carryIn) - Accrued(0m);
|
||||
|
||||
// Both 边界下三段各 7 天。carryIn 于 4/28(首个重置日)并入基数:
|
||||
// 首段 [4/21,4/28] 不含 carryIn;其后两段 carryIn 自身起息且其首段利息再复利。
|
||||
// 精确增量 = c×d + (c + c×d)×d = c×(2d + d²) = c×((1+d)² − 1),d = 7天利率因子。
|
||||
var d = Spread * 7m / AnnualDays;
|
||||
var expected = carryIn * (2m * d + d * d);
|
||||
Assert.AreEqual((double)expected, (double)delta, 0.001,
|
||||
"carryIn 增量 = 首个重置日起息的两段复利,首段不含 carryIn");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,11 @@ public static class CompoundInterestAccrual
|
||||
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
|
||||
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// </summary>
|
||||
/// <param name="carryInInterest">
|
||||
/// 窗口前已计未结转利息(EQD-6977 罚息承接):在【首个重置日】并入计息基数——
|
||||
/// 与"持有至到期"全期轨迹严格对齐(恒等式:全期复利 = 平仓日已结利息 + 罚息窗口利息)。
|
||||
/// 默认 0 时与旧行为逐位一致。与 resetCarryInterest 互斥使用(后者是全平重放的末段存量替代)。
|
||||
/// </param>
|
||||
public static InterestResult AccruePeriod(
|
||||
decimal notional,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
@@ -73,10 +78,14 @@ public static class CompoundInterestAccrual
|
||||
decimal realizedInterest,
|
||||
decimal unwindFraction,
|
||||
out decimal finalBasis,
|
||||
AccrualTrace? trace = null)
|
||||
AccrualTrace? trace = null,
|
||||
decimal carryInInterest = 0m)
|
||||
{
|
||||
decimal accrualBasis = notional;
|
||||
decimal accrued = 0m;
|
||||
// carryInInterest 是"窗口前已计未结转利息":先并入 accrued,随首个重置日的
|
||||
// basis = notional + accrued 一并资本化,并在其后每个重置日持续留在基数里
|
||||
//(与全窗口重放时 accrued 含全部历史利息的轨迹严格一致);最终报告时扣除。
|
||||
decimal accrued = carryInInterest;
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
@@ -116,6 +125,9 @@ public static class CompoundInterestAccrual
|
||||
|
||||
finalBasis = accrualBasis;
|
||||
|
||||
// 报告口径只含窗口内增量(carryIn 是窗口前已结利息,由正常平仓流单独结算)
|
||||
accrued -= carryInInterest;
|
||||
|
||||
if (realizedInterest != 0m)
|
||||
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
|
||||
accrued -= realizedInterest * unwindFraction;
|
||||
|
||||
Reference in New Issue
Block a user