refactor(accrual): EOD去退化差分——删除baseNotional死参数

EOD路径 orginPv≡posiPrincipal(调用点line1091/1096实证),notional==baseNotional恒成立,
差分项 notional-baseNotional 恒为0,baseNotional 是死参数。

简化:
- AccrueSimpleEod: 删 notional+baseNotional 两参数,basis=priorNotional 直赋值
- AccrueCompoundEod: 删 baseNotional 参数,非重置日 basis=priorNotional
- SimpleEodBasis 整个删除(不再需要)
- CompoundEodBasis 去掉 baseNotional 参数
- EodContext trace 去掉 baseNotional
- 差分注释块删除(EOD不再有差分)

差分逻辑 notional-baseNotional 只该存在于 unwind 专用路径(Period函数本就没有baseNotional)
净减20行(18增/38删),SwapModule零回归(7基线/508通过/8跳过)
This commit is contained in:
hjhan
2026-08-12 10:58:53 +08:00
parent ffd539d8c3
commit ff937f24e7
4 changed files with 18 additions and 38 deletions
@@ -41,10 +41,10 @@ public sealed class AccrualTrace
=> Add(AccrualTraceEvent.DayAccrual, segStart,
$" [seg{idx}] [{segStart:yyyy-MM-dd},{segEnd:yyyy-MM-dd}) days={days} rate={rate:P6} basis={basis:F4} segInterest={segInterest:F6} acc={accumulated:F6}");
/// <summary>EOD 上下文:计息日、重置日标志、平仓比例、差分基数公式各分量。供日终单日计息定位"重置日分支选对了吗"。</summary>
public void EodContext(DateTime eodDate, bool isResetDay, decimal unwindFraction, decimal priorAccrued, decimal priorNotional, decimal notional, decimal baseNotional, decimal remainingFraction)
/// <summary>EOD 上下文:计息日、重置日标志、平仓比例、基数各分量。供日终单日计息定位"重置日分支选对了吗"。</summary>
public void EodContext(DateTime eodDate, bool isResetDay, decimal unwindFraction, decimal priorAccrued, decimal priorNotional, decimal notional, decimal remainingFraction)
=> Add(AccrualTraceEvent.Start, eodDate,
$" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} baseNotional={baseNotional:F4} remainingFrac={remainingFraction:P4}");
$" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} remainingFrac={remainingFraction:P4}");
/// <summary>重置日<b>前</b>:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。</summary>
public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore)
@@ -100,7 +100,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
var result = FundingLegAccrual.AccrueCompoundEod(
50_000m, Notional, Notional, 1m, Notional, rate, policy,
50_000m, Notional, Notional, 1m, rate, policy,
isResetDay: true, remainingPercent, EodDate);
Console.WriteLine($"重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
@@ -132,7 +132,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var result = FundingLegAccrual.AccrueCompoundEod(
30_000m, Notional, Notional, 1m, Notional, rate, policy,
30_000m, Notional, Notional, 1m, rate, policy,
isResetDay: false, 0m, nonResetDate);
Console.WriteLine($"非重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
@@ -11,51 +11,35 @@ namespace YLErp.Modules.SwapModule.Accrual;
/// - accrued → 累计应计利息
/// - unwindFraction → 平仓比例(0~1
/// - realizedInterest → 历史已结利息(legacy: consumedInterest
/// - baseNotional → 差分公式基数(legacy: orginPv/originalPv
/// - accrualBasis → 逐段滚动的计息基数(legacy: dynomicPrincipal=dynamicPrincipal typo
/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal
/// </summary>
public static class FundingLegAccrual
{
private const int Precision = SwapInterest.FundingLegPrecision;
// ───────────────────────────────────────────────────────────────────
// 计息基数(basis)——单一真相源,纯函数与调用方共用,消除公式重复。
//
// 差分语义:basis = priorNotional + (notional baseNotional)
// • priorNotional = 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal
// • notional baseNotional = 名义本金绝对增量(加法式 carry,非比例缩放)
// • EOD 路径:baseNotional == notional → 增量 0basis 退化 = priorNotional
// • unwind 路径:baseNotional = 上一日终浮动端名义本金(orginPv) ≠ notional → 增量生效
// ───────────────────────────────────────────────────────────────────
/// <summary>单利日终计息基数(差分公式)。调用方与 AccrueSimpleEod 共用。</summary>
public static decimal SimpleEodBasis(decimal priorNotional, decimal notional, decimal baseNotional)
=> priorNotional + (notional - baseNotional);
/// <summary>复利日终计息基数:重置日 = notional + 累计利息×剩余比例(并本金);
/// 非重置日 = 差分公式。remainingFraction 对齐 legacy 钳制到 [0,1]。调用方与 AccrueCompoundEod 共用。</summary>
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
public static decimal CompoundEodBasis(
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction,
decimal priorNotional, decimal baseNotional)
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
=> isResetDay
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
: priorNotional + (notional - baseNotional);
: priorNotional;
/// <summary>
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueSimpleEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
decimal baseNotional,
FundingLegRate rate,
AccrualPolicy policy,
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = SimpleEodBasis(priorNotional, notional, baseNotional);
var basis = priorNotional;
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
@@ -80,14 +64,13 @@ public static class FundingLegAccrual
/// <summary>
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
/// 非重置日:basis = 差分公式(priorNotional + notional baseNotional)。
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
public static InterestResult AccrueCompoundEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
decimal baseNotional,
FundingLegRate rate,
AccrualPolicy policy,
bool isResetDay,
@@ -95,11 +78,11 @@ public static class FundingLegAccrual
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional, baseNotional);
var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, baseNotional, remainingFraction);
trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
@@ -1541,7 +1541,6 @@ namespace YLErp.Modules.SwapModule
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
unwindFraction: closePercent,
baseNotional: orginPv,
rate: legRate,
policy: accrualPolicy,
isResetDay: isResetDay,
@@ -1551,10 +1550,10 @@ namespace YLErp.Modules.SwapModule
SwapCalcTrace.Write(interestTrace);
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式),不再重复三元表达式
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
preEodPosition.TdInterestPrincipal, orginPv) * closePercent;
preEodPosition.TdInterestPrincipal) * closePercent;
InterestAmount = result.Accrued;
TdInterestAmount = result.AccruedToday;
@@ -1608,9 +1607,7 @@ namespace YLErp.Modules.SwapModule
var result = FundingLegAccrual.AccrueSimpleEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
unwindFraction: closePercent,
baseNotional: orginPv,
rate: legRate,
policy: accrualPolicy,
eodDate: endDate,