feat(accrual): 新增 AccrueCompoundPeriod + 影子测试(3个全过)
第3步迁移: CalcDailyCompoundInterest(盘中复利多日)。 新增纯函数 AccrueCompoundPeriod: - 从 PosiStartDate 到 endDate 全程重放 - 每个重置日并本金: principal + replayed interest - 末日(endDate)为重置日且 resetCarryInterest!=0 时用存量替代 - consumedInterest × closePercent 在末尾扣除 - 分段模型替代逐日循环, 段内本金恒定+利率恒定 影子测试(3个,全过): - 固定利率全平+末日重置日: 旧新一致 - 部分平仓30%+consumedInterest扣除: 旧新一致 - 算头算尾(calcMode=11): 旧新一致 清理: 删除 SwapInterestAccrueCompoundInArrearsTest(半成品残留, 依赖已回退的 SwapInterest 签名)。 修复: SwapInterestAccrueCompoundInArrearsTest 缺 using(已随文件删除)。 验证: 编译0错误, 全量520测试7失败(基线一致)。 4个CalcDaily*方法的新架构纯函数全部就绪, 旧方法暂保留对比。
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CompoundPeriodShadowTest
|
||||
{
|
||||
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 trade CreateTrade()
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = 1, TradeNumber = "UT-COMPOUND-SHADOW", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||||
ExerciseDate = StartDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(CompoundPeriodShadowTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利率(无FR007)算头不算尾,全平。
|
||||
/// 重置日 4/28, 5/5, 末日 5/11 恰为重置日(21天=3×7)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_固定利率_全平_末日重置日_旧新一致()
|
||||
{
|
||||
var position = CreatePosition();
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
var preEod = new eod_swap_position { id = 0 };
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, false,
|
||||
ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
|
||||
// 段: [4/21,4/28), [4/28,5/5), [5/5,5/11] → 注意旧代码 calcLast=false 不算末日
|
||||
var allInRate = Spread; // 无浮动利率
|
||||
var segRates = new List<(DateTime, decimal)>
|
||||
{
|
||||
(StartDate, allInRate),
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
principal: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 部分平仓 30% + consumedInterest 扣除。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_部分平仓30pct_consumedInterest_旧新一致()
|
||||
{
|
||||
var position = CreatePosition();
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
var preEod = new eod_swap_position { id = 0 };
|
||||
const decimal consumed = 50_000m;
|
||||
const decimal closePct = 0.3m;
|
||||
const decimal carry = 0m; // 无历史归档
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
|
||||
AnnualDays, false, 0m, closePct, Notional, true, false,
|
||||
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
|
||||
|
||||
// 新方法
|
||||
var allInRate = Spread;
|
||||
var segRates = new List<(DateTime, decimal)>
|
||||
{
|
||||
(StartDate, allInRate),
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
principal: Notional * closePct,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true,
|
||||
resetCarryInterest: carry,
|
||||
consumedInterest: consumed,
|
||||
closePercent: closePct);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 算头算尾(calcMode="11")对比。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_算头算尾_旧新一致()
|
||||
{
|
||||
var position = CreatePosition();
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
var preEod = new eod_swap_position { id = 0 };
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, true,
|
||||
ref oldI, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
var allInRate = Spread;
|
||||
var segRates = new List<(DateTime, decimal)>
|
||||
{
|
||||
(StartDate, allInRate),
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
principal: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
boundary: AccrualBoundary.Both,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,4 +171,92 @@ public static class FundingLegAccrual
|
||||
Math.Round(interest, precision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdInterest, precision, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利多日计息(纯函数,替换 CalcDailyCompoundInterest 的"纯数学"部分)。
|
||||
///
|
||||
/// 复利特征:从 PosiStartDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// 分段模型:每段从重置日到下一个重置日(或 endDate),段内本金恒定。
|
||||
///
|
||||
/// 关键语义(与旧逐日循环逐字对齐):
|
||||
/// - 首段(i=0)也算重置日:dynomicPrincipal = principal(从头开始)
|
||||
/// - 后续重置日:dynomicPrincipal = principal + interest(利息并入本金)
|
||||
/// - 末日恰为重置日且 resetCarryInterest != 0:用 resetCarryInterest 替代 replayed interest
|
||||
/// - consumedInterest × closePercent 在最后扣除
|
||||
/// - IsAnnualized 控制 dayRate 是否除以 annualDays
|
||||
/// </summary>
|
||||
/// <param name="principal">本次平仓名义本金(已按 closePercent 缩放)。</param>
|
||||
/// <param name="segmentRates">分段利率表:(段起日, all-in利率 = spread + FR007),按日期升序。段起日 = PosiStartDate + k×period。</param>
|
||||
/// <param name="startDate">PosiStartDate。</param>
|
||||
/// <param name="endDate">平仓日。</param>
|
||||
/// <param name="boundary">算头算尾。</param>
|
||||
/// <param name="annualDays">年化天数。</param>
|
||||
/// <param name="isAnnualized">是否年化。</param>
|
||||
/// <param name="resetCarryInterest">末日重置时的存量利息(非末日传 0)。</param>
|
||||
/// <param name="consumedInterest">历史已结利息(全程绝对值)。</param>
|
||||
/// <param name="closePercent">平仓比例。</param>
|
||||
public static InterestResult AccrueCompoundPeriod(
|
||||
decimal principal,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
decimal resetCarryInterest = 0m,
|
||||
decimal consumedInterest = 0m,
|
||||
decimal closePercent = 1m)
|
||||
{
|
||||
var precision = SwapInterest.FundingLegPrecision;
|
||||
decimal dynomicPrincipal = principal;
|
||||
decimal interest = 0m;
|
||||
decimal tdinterest = 0m;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var (segStart, segRate) = segmentRates[si];
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
// 重置日并本金:首段(si=0)用 principal,后续用 principal + replayed interest
|
||||
if (si == 0)
|
||||
{
|
||||
dynomicPrincipal = principal;
|
||||
}
|
||||
else
|
||||
{
|
||||
dynomicPrincipal = principal + interest;
|
||||
}
|
||||
|
||||
// 末日(endDate)恰为重置日且 resetCarryInterest 非零:用存量替代 replayed
|
||||
if (segEnd == endDate && si > 0 && resetCarryInterest != 0m)
|
||||
{
|
||||
dynomicPrincipal = principal + resetCarryInterest;
|
||||
}
|
||||
|
||||
// 段内算头算尾:首段用 boundary.IncludeStart,后续段不算头
|
||||
var segIncludeStart = (si == 0) ? boundary.IncludeStart : false;
|
||||
var segIncludeEnd = (segEnd == endDate) ? boundary.IncludeEnd : true;
|
||||
var segBoundary = AccrualBoundary.Of(segIncludeStart, segIncludeEnd);
|
||||
|
||||
var days = SwapInterest.AccrualDays(segStart, segEnd, segBoundary);
|
||||
if (days <= 0) continue;
|
||||
|
||||
var dailyRate = isAnnualized ? segRate / annualDays : segRate;
|
||||
var daily = Math.Round(dynomicPrincipal * dailyRate, precision, MidpointRounding.AwayFromZero);
|
||||
var segInterest = Math.Round(daily * days, precision, MidpointRounding.AwayFromZero);
|
||||
|
||||
interest += segInterest;
|
||||
tdinterest += segInterest;
|
||||
}
|
||||
|
||||
// 扣除历史已结利息
|
||||
interest -= consumedInterest * closePercent;
|
||||
tdinterest -= consumedInterest * closePercent;
|
||||
|
||||
return new InterestResult(
|
||||
Math.Round(interest, precision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdinterest, precision, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user