feat(accrual): 新增 AccrueSimplePeriod + 影子测试

第2步迁移: CalcDailySimpleInterest(盘中单利多日)。

新增纯函数 AccrueSimplePeriod:
- 单利特征: 计息本金恒定(差分 = priorAccrualPrincipal + positionPrincipal - originalPv)
- 按重置日分段, 每段用 AccrualDays 算天数×日利息(无逐日循环)
- 续接 priorValueDate 之后的日期

修复: SwapInterest.Round 是 private, 新方法改用 Math.Round。
修复: IReadOnlyList 无 IndexOf, 改用 for 循环索引。

影子测试(2个,全过):
- 固定利率无归档: 旧新一致(差分本金=0,利息=0,符合旧逻辑)
- 有归档续接+部分平仓50%: 旧新一致

验证: 编译0错误, 全量517测试7失败(基线一致)。
This commit is contained in:
hjhan
2026-08-12 09:27:20 +08:00
parent 217c2d5826
commit 963b5e66af
2 changed files with 221 additions and 0 deletions
@@ -100,4 +100,75 @@ public static class FundingLegAccrual
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 单利多日计息(纯函数,替换 CalcDailySimpleInterest 的"纯数学"部分)。
///
/// 单利特征:计息本金全程恒定(差分公式 = priorAccrualPrincipal + positionPrincipal - originalPv)。
/// 按重置日分段,每段用对应利率算天数×日利息(无逐日循环,等价于 SwapInterest.AccrueSimple 分段累加)。
///
/// 利率变化点由调用方通过 segmentRates 传入(已取好 FR007),本方法不取价。
/// </summary>
/// <param name="priorUnrealized">上一日终累计待实现利息(preEod.InterestProfitSum × closeRatio)。</param>
/// <param name="accrualPrincipal">计息本金(差分,全程恒定)。</param>
/// <param name="closeRatio">平仓比例。</param>
/// <param name="segmentRates">分段利率表:(段起日, all-in利率),按日期升序。</param>
/// <param name="startDate">计息开始日(PosiStartDate)。</param>
/// <param name="endDate">计息结束日(平仓日)。</param>
/// <param name="priorValueDate">上一日终归档日(只算此日之后的利息)。</param>
/// <param name="boundary">算头算尾。</param>
/// <param name="annualDays">年化天数。</param>
/// <param name="isAnnualized">是否年化。</param>
public static InterestResult AccrueSimplePeriod(
decimal priorUnrealized,
decimal accrualPrincipal,
decimal closeRatio,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
DateTime priorValueDate,
AccrualBoundary boundary,
int annualDays,
bool isAnnualized)
{
var displayPrincipal = accrualPrincipal * closeRatio;
decimal interest = priorUnrealized;
decimal tdInterest = priorUnrealized;
var precision = SwapInterest.FundingLegPrecision;
// 按段累加:每段内利率恒定,用 AccrualDays 算天数 × 日利息
var segStart = startDate;
var segIncludeStart = boundary.IncludeStart;
for (int si = 0; si < segmentRates.Count; si++)
{
var (segRateStart, segRate) = segmentRates[si];
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
// 跳过 priorValueDate 之前的日期(续接上一日终)
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
// 算头算尾:首段用 boundary.IncludeStart,后续段不算头
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; }
var dailyRate = isAnnualized ? segRate / annualDays : segRate;
var daily = Math.Round(displayPrincipal * dailyRate, precision, MidpointRounding.AwayFromZero);
var segInterest = Math.Round(daily * days, precision, MidpointRounding.AwayFromZero);
interest += segInterest;
tdInterest += segInterest;
segStart = segEnd;
segIncludeStart = false;
}
return new InterestResult(
Math.Round(interest, precision, MidpointRounding.AwayFromZero),
Math.Round(tdInterest, precision, MidpointRounding.AwayFromZero));
}
}