refactor(accrual): 接线SimpleInterest/CompoundInterest内部 + 分段修复 + 去重

CalcDailySimpleInterest/CalcDailyCompoundInterest内部逐日循环→分段纯函数(签名不变):
- AccrueSimplePeriod: AccruedToday改未缩放累计(TdInterestAmount口径)
- AccrueCompoundPeriod: 新增out finalBasis供flowEvent.InterestPrincipal精确赋值
- 修复5处分段边界bug(includeStart/includeEnd/resetCarryInterest interestPeriod=1场景)
- 提取BuildSegmentRates消除SimpleInterest/CompoundInterest取率重复
- 影子测试补AccruedToday断言关闭测试盲区

SwapModule零回归(7基线/510通过)
This commit is contained in:
hjhan
2026-08-12 13:09:17 +08:00
parent 85746d7ee9
commit c86633479d
4 changed files with 224 additions and 147 deletions
@@ -93,11 +93,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
endDate: EndDate,
boundary: AccrualBoundary.StartOnly,
annualDays: AnnualDays,
isAnnualized: true);
isAnnualized: true,
resetCarryInterest: 0m,
realizedInterest: 0m,
unwindFraction: 1m,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -138,11 +143,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
isAnnualized: true,
resetCarryInterest: carry,
realizedInterest: consumed,
unwindFraction: closePct);
unwindFraction: closePct,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -177,11 +184,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
endDate: EndDate,
boundary: AccrualBoundary.Both,
annualDays: AnnualDays,
isAnnualized: true);
isAnnualized: true,
resetCarryInterest: 0m,
realizedInterest: 0m,
unwindFraction: 1m,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
}
}
@@ -64,6 +64,27 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
public StubSvc() : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
}
/// <summary>带浮动率 stub 的 SwapDealServiceoverride IndexFixer 注入预设 FR007 取价。</summary>
private sealed class FloatStubSvc : SwapDealService
{
private readonly IIndexFixer _fixer;
public FloatStubSvc(IIndexFixer fixer) : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest))
=> _fixer = fixer;
protected override IIndexFixer IndexFixer => _fixer;
}
/// <summary>Stub IIndexFixer:对所有查询返回固定 fixing(不依赖日期匹配,规避 QDP 日历差异)。</summary>
private sealed class StubIndexFixer : IIndexFixer
{
private readonly decimal _rate;
public StubIndexFixer(decimal rate) => _rate = rate;
public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate)
{
rate = _rate;
return true;
}
}
/// <summary>
/// 固定利率(无FR007)算头不算尾,全平,无历史归档。
/// </summary>
@@ -100,6 +121,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -145,6 +167,70 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
/// 单利 + FR007 浮动利率 + 多重置日 + 部分平仓 + 有历史归档。
/// 验证旧方法内部取价循环生成的 segmentRates 与手算一致——为抽取共享 SegmentRateBuilder 做安全网。
/// 场景:preEod.ValueDate=4/30,重置日 4/21(跳过取价), 4/28(跳过取价), 5/5(取 FR007 fixing)。
/// </summary>
[TestMethod]
public void _单利浮动_FR007_部分平仓_旧新一致()
{
const decimal floatRateIn = 0.0150m; // 入参 floateRate(上一次取到的浮动率 1.50%)
const decimal fixingAtReset = 0.0125m; // 5/5 重置日取到的 FR007 fixing1.25%
const decimal closePct = 0.5m;
var preEodDate = new DateTime(2026, 4, 30); // 上一日终=4/305/5 > 4/30 触发取价
var position = CreatePosition();
position.FloatRateUnderlyingCode = "FR007";
var preEod = new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = preEodDate,
TdInterestPrincipal = Notional,
InterestProfitSum = 200_000m,
PosiNotionalValue = Notional, FloatRate = 0m
};
var flowEvent = new swap_flow_event { InterestRate = Spread };
// 旧方法(通过 stub IndexFixer 注入 FR007 取价)
decimal oldI = 0, oldTd = 0;
var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset));
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
// 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑)
// 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
// 4/28 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
// 5/5 > preEodDate(4/30) → 取价,currentFloat 更新为 fixingAtReset
var segRates = new List<(DateTime, decimal)>
{
(StartDate, Spread + floatRateIn), // (4/21, 0.0175)
(StartDate.AddDays(7), Spread + floatRateIn), // (4/28, 0.0175)
(StartDate.AddDays(14), Spread + fixingAtReset), // (5/5, 0.0150)
};
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var result = FundingLegAccrual.AccrueSimplePeriod(
priorAccrued: 200_000m * closePct,
notional: accrualPrincipal,
unwindFraction: closePct,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
priorValueDate: preEodDate,
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 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
}
}
@@ -104,6 +104,7 @@ public static class FundingLegAccrual
/// <summary>
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
/// 本金全程恒定,按重置日分段取利率。
/// Accrued = 缩放累计(InterestAmount)AccruedToday = 未缩放累计(TdInterestAmount)。
/// </summary>
public static InterestResult AccrueSimplePeriod(
decimal priorAccrued,
@@ -119,12 +120,12 @@ public static class FundingLegAccrual
AccrualTrace? trace = null)
{
var displayBasis = notional * unwindFraction;
decimal accrued = priorAccrued;
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
var segStart = startDate;
var segIncludeStart = boundary.IncludeStart;
for (int si = 0; si < segmentRates.Count; si++)
{
@@ -135,22 +136,29 @@ public static class FundingLegAccrual
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
// 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。
// 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
// calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾,
// 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。
var isLastSegment = si == segmentRates.Count - 1;
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; }
if (days <= 0) { segStart = segEnd; continue; }
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
var segInterest = displayBasis * dailyRate * days;
accrued += segInterest;
accruedUnscaled += notional * dailyRate * days;
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
segStart = segEnd;
segIncludeStart = false;
}
var result = new InterestResult(
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accrued, Precision));
SwapInterest.Round(accruedUnscaled, Precision));
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
@@ -167,9 +175,10 @@ public static class FundingLegAccrual
AccrualBoundary boundary,
int annualDays,
bool isAnnualized,
decimal resetCarryInterest = 0m,
decimal realizedInterest = 0m,
decimal unwindFraction = 1m,
decimal resetCarryInterest,
decimal realizedInterest,
decimal unwindFraction,
out decimal finalBasis,
AccrualTrace? trace = null)
{
decimal accrualBasis = notional;
@@ -179,16 +188,19 @@ public static class FundingLegAccrual
for (int si = 0; si < segmentRates.Count; si++)
{
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
var isLastSegment = si == segmentRates.Count - 1;
var segEnd = isLastSegment
? endDate
: segmentRates[si + 1].StartDate;
// 重置日并本金
accrualBasis = si == 0 ? notional : notional + accrued;
// 末日重置且 carry 非零:用存量替代
// 末日恰好是重置且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
// 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
var usedCarry = false;
if (segEnd == endDate && si > 0 && resetCarryInterest != 0m)
if (isLastSegment && si > 0 && resetCarryInterest != 0m
&& segmentRates[si].StartDate == endDate)
{
accrualBasis = notional + resetCarryInterest;
usedCarry = true;
@@ -200,7 +212,7 @@ public static class FundingLegAccrual
// 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
var segIncludeEnd = (segEnd == endDate) ? boundary.IncludeEnd : false;
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
if (days <= 0) continue;
@@ -211,6 +223,8 @@ public static class FundingLegAccrual
trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
}
finalBasis = accrualBasis;
// 扣除历史已结利息
if (realizedInterest != 0m)
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
+94 -129
View File
@@ -1,4 +1,4 @@
using MoreLinq.Extensions;
using MoreLinq.Extensions;
using Newtonsoft.Json;
using YLErp.BLL;
using YLErp.BLL.Eod;
@@ -1325,6 +1325,40 @@ namespace YLErp.Modules.SwapModule
}
return interest;
}
/// <summary>
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
/// </summary>
private (List<(DateTime StartDate, decimal Rate)> Segments, decimal LastFloat) BuildSegmentRates(
DateTime startDate, DateTime endDate, int interestPeriod,
swap_position position, decimal spread, decimal initialFloat,
DateTime? fetchAfterDate)
{
var rates = new List<(DateTime, decimal)>();
var calcDays = (endDate - startDate).Days;
decimal currentFloat = initialFloat;
for (int i = 0; i <= calcDays; i += interestPeriod)
{
var resetDate = startDate.AddDays(i);
if ((fetchAfterDate == null || resetDate > fetchAfterDate.Value)
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) currentFloat = fixing;
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
rates.Add((resetDate, spread + currentFloat));
}
return (rates, currentFloat);
}
/// <summary>
/// 计算复利 盘中
/// </summary>
@@ -1341,85 +1375,36 @@ namespace YLErp.Modules.SwapModule
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
{
var startDate = position.PosiStartDate;
decimal interestProfitSum = 0;
decimal TdInterestPrincipal = 0;
decimal interest = interestProfitSum ;
decimal tdinterest = interestProfitSum ;
int interestPeriod = position.interest_rest_days ?? 1;
// 复利:只能用要平仓的名义本金从头开始算
decimal dynomicPrincipal = principal;
decimal tdDynomicPrincipal = dynomicPrincipal;
var calcDays = (endDate - startDate).Days;
double floatRate = Convert.ToDouble(floateRate);
for (int i = 0; i <= calcDays; i++)
{
var accrueDate = startDate.AddDays(i);
// 重置日取价必须在 calcFirst/calcLast 跳过之前完成:calcLast=false(不算尾) 只应跳过计息,
// 不应跳过重置日的 FR007 取价。否则平仓日=重置日时会沿用旧周期利率,
// 且 flowEvent.FloatRate 落库为旧值,传染后续 EODGLMS-JIATT-20260805 根因)。
if (accrueDate >= startDate && i % interestPeriod == 0
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
if (accrueDate >= startDate)
{
if (i % interestPeriod == 0)
{
// 每个重置节点 计息基数 = 前日本金 + 本期利息
// resetCarryInterest 是上一日终待实现按本次平仓比例分摊后的存量,
// 只能在 endDate 恰好是当前复利重置日时并入本金。历史重置点必须使用
// 重放到当时的 interest,否则会把上一日终存量反复注入历史本金,
// 例如 0007 的 5/11 部分平仓会由 84,090.95 被多算为 84,114.88。
var interestToReset = i > 0 && accrueDate == endDate && resetCarryInterest != 0m
? resetCarryInterest
: interest;
dynomicPrincipal = principal + interestToReset;
tdDynomicPrincipal = principal + interestToReset;
flowEvent.InterestPrincipal = tdDynomicPrincipal;
TdInterestPrincipal = tdDynomicPrincipal;
}
else
{
// 复利非重置日:利息不并入本金,不用closePercent缩放(principal已反映平仓比例)
flowEvent.InterestPrincipal = tdDynomicPrincipal;
TdInterestPrincipal = tdDynomicPrincipal;
}
}
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,重置本金已在上方完成)
if (accrueDate >= startDate)
{
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
if (position.IsAnnualized)
{
interest1 /= annualDays;
tdinterest1 /= annualDays;
}
interest += interest1;
tdinterest += tdinterest1;
}
}
// 兜底:若循环因 calcLast 跳过最后一天(重置日=平仓日)flowEvent.FloatRate 不会被循环内赋值,
// 用最终 floatRate 兜底,确保落库的 FloatRate 反映最后一个重置日的利率(GLMS-JIATT-20260805)。
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
// 复利从头重放得到的是"假设从未结出"的整段总利息,需扣除历史已通过互换结出的利息,
// 否则已结部分会重复计息(类比分红 PosiDividendSum = totalToDate RealizedDividend)。
// consumedInterest is full-position absolute interest; scale it to this close portion.
interest -= consumedInterest * closePercent;
tdinterest -= consumedInterest * closePercent;
InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
// 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007fetchAfterDate=null)。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: null);
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueCompoundPeriod(
notional: principal,
segmentRates: segmentRates,
startDate: startDate,
endDate: endDate,
boundary: AccrualBoundary.Of(calcFirst, calcLast),
annualDays: annualDays,
isAnnualized: position.IsAnnualized,
resetCarryInterest: resetCarryInterest,
realizedInterest: consumedInterest,
unwindFraction: closePercent,
finalBasis: out var finalBasis,
trace: interestTrace);
SwapCalcTrace.Write(interestTrace);
// flowEvent 副作用:FloatRate=末段浮动利率;InterestPrincipal=复利终期本金(最后一次并本金后的基数)。
flowEvent.FloatRate = currentFloat;
flowEvent.InterestPrincipal = finalBasis;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
}
/// <summary>
@@ -1428,59 +1413,39 @@ namespace YLErp.Modules.SwapModule
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
{
var startDate = position.PosiStartDate;
decimal interestProfitSum = preEodPosition.InterestProfitSum;
var TdInterestPrincipal = preEodPosition.TdInterestPrincipal;
decimal interest = interestProfitSum * closePercent;
decimal tdinterest = interestProfitSum * closePercent;
int interestPeriod = position.interest_rest_days ?? 1;
// 单利:可用上一日计息基数
decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
decimal tdDynomicPrincipal = dynomicPrincipal;
var calcDays = (endDate - startDate).Days;
double floatRate = Convert.ToDouble(floateRate);
for (int i = 0; i <= calcDays; i++)
{
var accrueDate = startDate.AddDays(i);
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾
if (accrueDate > preEodPosition.ValueDate)
{
// 重置日重新获取该段浮动利率;非重置日沿用上一段利率。
// 两分支唯一差异即"是否重取利率",本金口径(只缩放一次)完全一致,
// 合并后消除复制粘贴导致的 closePercent^N 类 bug(原非重置日分支多了一行
// tdDynomicPrincipal = flowEvent.InterestPrincipal 使本金累积乘 closePercent^N)。
if (i % interestPeriod == 0 && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
{
var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
{
if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
}
else
{
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
}
}
// 显示本金 = 计息基数 × closePercent(只缩放一次,与日终 ByEod 口径一致);
// 计息基数(tdDynomicPrincipal)逐日恒定、不缩放(单利特征)
flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
TdInterestPrincipal = tdDynomicPrincipal;
// orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金;保证金腿=上一日终保证金余额)。
// 单利差分:accrualBasis 全程恒定 = 昨日终滚动基数 + 当日名义本金 - 参考本金
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
if (position.IsAnnualized)
{
interest1 /= annualDays;
tdinterest1 /= annualDays;
}
interest += interest1;
tdinterest += tdinterest1;
}
}
InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
// 分段取率:仅 ValueDate 之后的重置日才取 FR007fetchAfterDate=ValueDate)。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: preEodPosition.ValueDate);
// 纯函数计息:Accrued=缩放累计(InterestAmount)AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueSimplePeriod(
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
notional: accrualBasis,
unwindFraction: closePercent,
segmentRates: segmentRates,
startDate: startDate,
endDate: endDate,
priorValueDate: preEodPosition.ValueDate,
boundary: AccrualBoundary.Of(calcFirst, calcLast),
annualDays: annualDays,
isAnnualized: position.IsAnnualized,
trace: interestTrace);
SwapCalcTrace.Write(interestTrace);
// flowEvent 副作用(下游 EOD 用 InterestPrincipal 播种次日 TdInterestPrincipal
flowEvent.InterestPrincipal = accrualBasis * closePercent;
flowEvent.FloatRate = currentFloat;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
}
/// <summary>