diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
index 4c2af32c..c3b75ee7 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
@@ -13,7 +13,7 @@ using YLErp.Core.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
+ /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
///
[TestClass]
@@ -99,7 +99,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
- var result = FundingLegAccrual.AccrueCompoundEod(
+ var result = CompoundInterestAccrual.AccrueEod(
50_000m, Notional, Notional, 1m, rate, policy,
isResetDay: true, remainingPercent, EodDate);
@@ -131,7 +131,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(
+ var result = CompoundInterestAccrual.AccrueEod(
30_000m, Notional, Notional, 1m, rate, policy,
isResetDay: false, 0m, nonResetDate);
diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
index ce55867b..dad0831c 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
@@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
+ /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。
///
[TestClass]
public class CompoundPeriodShadowTest
@@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
@@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional * closePct,
segmentRates: segRates,
startDate: StartDate,
@@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
index d932e035..283ca087 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
@@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
+ /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
///
[TestClass]
public class SimplePeriodShadowTest
@@ -106,7 +106,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 0m,
notional: 0m,
unwindFraction: 1m,
@@ -152,7 +152,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * 0.5m,
notional: accrualPrincipal,
unwindFraction: 0.5m,
@@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * closePct,
notional: accrualPrincipal,
unwindFraction: closePct,
diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
index e6442d6c..5e18c5d1 100644
--- a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
+++ b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
@@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
/// ============================================================================
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
- /// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中
+ /// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
new file mode 100644
index 00000000..9418aa46
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
@@ -0,0 +1,132 @@
+using YLErp.Core.Interest;
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule.Accrual;
+
+///
+/// 复利计息纯函数——EOD 单日 + intraday 多日。
+/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
+///
+public static class CompoundInterestAccrual
+{
+ private const int Precision = SwapInterest.FundingLegPrecision;
+
+ /// 复利日终计息基数(单一真相源,纯函数与调用方共用):
+ /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
+ /// remainingFraction 对齐 legacy 钳制到 [0,1]。
+ public static decimal EodBasis(
+ bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
+ => isResetDay
+ ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
+ : priorNotional;
+
+ ///
+ /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
+ /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
+ /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
+ ///
+ public static InterestResult AccrueEod(
+ decimal priorAccrued,
+ decimal priorNotional,
+ decimal notional,
+ decimal unwindFraction,
+ FundingLegRate rate,
+ AccrualPolicy policy,
+ bool isResetDay,
+ decimal remainingFraction,
+ DateTime eodDate,
+ AccrualTrace? trace = null)
+ {
+ var basis = EodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
+ var displayBasis = basis * unwindFraction;
+
+ var allInRate = rate.AllInRate;
+ trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
+ var dayInterest = displayBasis * allInRate;
+ var tdInterest = basis * allInRate;
+ if (policy.IsAnnualized)
+ {
+ dayInterest /= policy.AnnualDays;
+ tdInterest /= policy.AnnualDays;
+ }
+
+ var totalAccrued = priorAccrued * unwindFraction + dayInterest;
+ var result = new InterestResult(
+ SwapInterest.Round(totalAccrued, Precision),
+ SwapInterest.Round(tdInterest, Precision));
+
+ trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+
+ ///
+ /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
+ /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
+ ///
+ public static InterestResult AccruePeriod(
+ decimal notional,
+ IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
+ DateTime startDate,
+ DateTime endDate,
+ AccrualBoundary boundary,
+ int annualDays,
+ bool isAnnualized,
+ decimal resetCarryInterest,
+ decimal realizedInterest,
+ decimal unwindFraction,
+ out decimal finalBasis,
+ AccrualTrace? trace = null)
+ {
+ decimal accrualBasis = notional;
+ decimal accrued = 0m;
+
+ trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
+
+ for (int si = 0; si < segmentRates.Count; si++)
+ {
+ var isLastSegment = si == segmentRates.Count - 1;
+ var segEnd = isLastSegment
+ ? endDate
+ : segmentRates[si + 1].StartDate;
+
+ // 重置日并本金
+ accrualBasis = si == 0 ? notional : notional + accrued;
+
+ // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
+ var usedCarry = false;
+ if (isLastSegment && si > 0 && resetCarryInterest != 0m
+ && segmentRates[si].StartDate == endDate)
+ {
+ accrualBasis = notional + resetCarryInterest;
+ usedCarry = true;
+ }
+
+ if (si > 0)
+ trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
+
+ var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
+ var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
+ var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
+ AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
+ if (days <= 0) continue;
+
+ var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
+ var segInterest = accrualBasis * dailyRate * days;
+ accrued += segInterest;
+ 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);
+ accrued -= realizedInterest * unwindFraction;
+
+ var result = new InterestResult(
+ SwapInterest.Round(accrued, Precision),
+ SwapInterest.Round(accrued, Precision));
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
deleted file mode 100644
index 90d3500d..00000000
--- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
+++ /dev/null
@@ -1,239 +0,0 @@
-using YLErp.Core.Interest;
-using YLErp.Derivatives.Interest;
-
-namespace YLErp.Modules.SwapModule.Accrual;
-
-///
-/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
-///
-/// 命名规范(对齐 QuantLib / Strata):
-/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准)
-/// - accrued → 累计应计利息
-/// - unwindFraction → 平仓比例(0~1)
-/// - realizedInterest → 历史已结利息(legacy: consumedInterest)
-/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal)
-///
-public static class FundingLegAccrual
-{
- private const int Precision = SwapInterest.FundingLegPrecision;
-
- /// 复利日终计息基数(单一真相源,纯函数与调用方共用):
- /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
- /// remainingFraction 对齐 legacy 钳制到 [0,1]。
- public static decimal CompoundEodBasis(
- bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
- => isResetDay
- ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
- : priorNotional;
-
- ///
- /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
- /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
- ///
- public static InterestResult AccrueSimpleEod(
- decimal priorAccrued,
- decimal priorNotional,
- decimal unwindFraction,
- FundingLegRate rate,
- AccrualPolicy policy,
- DateTime eodDate,
- AccrualTrace? trace = null)
- {
- var basis = priorNotional;
- var displayBasis = basis * unwindFraction;
-
- var allInRate = rate.AllInRate;
- var dayInterest = displayBasis * allInRate;
- var tdInterest = basis * allInRate;
- if (policy.IsAnnualized)
- {
- dayInterest /= policy.AnnualDays;
- tdInterest /= policy.AnnualDays;
- }
-
- var totalAccrued = priorAccrued + dayInterest;
- var result = new InterestResult(
- SwapInterest.Round(totalAccrued, Precision),
- SwapInterest.Round(tdInterest, Precision));
-
- trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
- /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
- /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
- ///
- public static InterestResult AccrueCompoundEod(
- decimal priorAccrued,
- decimal priorNotional,
- decimal notional,
- decimal unwindFraction,
- FundingLegRate rate,
- AccrualPolicy policy,
- bool isResetDay,
- decimal remainingFraction,
- DateTime eodDate,
- AccrualTrace? trace = null)
- {
- var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
- var displayBasis = basis * unwindFraction;
-
- var allInRate = rate.AllInRate;
- trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
- var dayInterest = displayBasis * allInRate;
- var tdInterest = basis * allInRate;
- if (policy.IsAnnualized)
- {
- dayInterest /= policy.AnnualDays;
- tdInterest /= policy.AnnualDays;
- }
-
- var totalAccrued = priorAccrued * unwindFraction + dayInterest;
- var result = new InterestResult(
- SwapInterest.Round(totalAccrued, Precision),
- SwapInterest.Round(tdInterest, Precision));
-
- trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
- /// 本金全程恒定,按重置日分段取利率。
- /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
- ///
- public static InterestResult AccrueSimplePeriod(
- decimal priorAccrued,
- decimal notional,
- decimal unwindFraction,
- IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
- DateTime startDate,
- DateTime endDate,
- DateTime priorValueDate,
- AccrualBoundary boundary,
- int annualDays,
- bool isAnnualized,
- AccrualTrace? trace = null)
- {
- var displayBasis = notional * unwindFraction;
- decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
- decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
-
- trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
-
- var segStart = startDate;
-
- for (int si = 0; si < segmentRates.Count; si++)
- {
- var segEnd = si < segmentRates.Count - 1
- ? segmentRates[si + 1].StartDate
- : endDate;
-
- var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
- if (effectiveStart > segEnd) { segStart = segEnd; continue; }
-
- // 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; 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;
- }
-
- var result = new InterestResult(
- SwapInterest.Round(accrued, Precision),
- SwapInterest.Round(accruedUnscaled, Precision));
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
- /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
- ///
- public static InterestResult AccrueCompoundPeriod(
- decimal notional,
- IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
- DateTime startDate,
- DateTime endDate,
- AccrualBoundary boundary,
- int annualDays,
- bool isAnnualized,
- decimal resetCarryInterest,
- decimal realizedInterest,
- decimal unwindFraction,
- out decimal finalBasis,
- AccrualTrace? trace = null)
- {
- decimal accrualBasis = notional;
- decimal accrued = 0m;
-
- trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
-
- for (int si = 0; si < segmentRates.Count; si++)
- {
- var isLastSegment = si == segmentRates.Count - 1;
- var segEnd = isLastSegment
- ? endDate
- : segmentRates[si + 1].StartDate;
-
- // 重置日并本金
- accrualBasis = si == 0 ? notional : notional + accrued;
-
- // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
- // 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
- var usedCarry = false;
- if (isLastSegment && si > 0 && resetCarryInterest != 0m
- && segmentRates[si].StartDate == endDate)
- {
- accrualBasis = notional + resetCarryInterest;
- usedCarry = true;
- }
-
- // 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数)
- if (si > 0)
- trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
-
- // 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
- var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
- var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
- var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
- AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
- if (days <= 0) continue;
-
- var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
- var segInterest = accrualBasis * dailyRate * days;
- accrued += segInterest;
- 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);
- accrued -= realizedInterest * unwindFraction;
-
- var result = new InterestResult(
- SwapInterest.Round(accrued, Precision),
- SwapInterest.Round(accrued, Precision));
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs
new file mode 100644
index 00000000..fb1378ab
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs
@@ -0,0 +1,106 @@
+using YLErp.Core.Interest;
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule.Accrual;
+
+///
+/// 单利计息纯函数——EOD 单日 + intraday 多日。
+/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。
+///
+public static class SimpleInterestAccrual
+{
+ private const int Precision = SwapInterest.FundingLegPrecision;
+
+ ///
+ /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
+ /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
+ ///
+ public static InterestResult AccrueEod(
+ decimal priorAccrued,
+ decimal priorNotional,
+ decimal unwindFraction,
+ FundingLegRate rate,
+ AccrualPolicy policy,
+ DateTime eodDate,
+ AccrualTrace? trace = null)
+ {
+ var basis = priorNotional;
+ var displayBasis = basis * unwindFraction;
+
+ var allInRate = rate.AllInRate;
+ var dayInterest = displayBasis * allInRate;
+ var tdInterest = basis * allInRate;
+ if (policy.IsAnnualized)
+ {
+ dayInterest /= policy.AnnualDays;
+ tdInterest /= policy.AnnualDays;
+ }
+
+ var totalAccrued = priorAccrued + dayInterest;
+ var result = new InterestResult(
+ SwapInterest.Round(totalAccrued, Precision),
+ SwapInterest.Round(tdInterest, Precision));
+
+ trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+
+ ///
+ /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
+ /// 本金全程恒定,按重置日分段取利率。
+ /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
+ ///
+ public static InterestResult AccruePeriod(
+ decimal priorAccrued,
+ decimal notional,
+ decimal unwindFraction,
+ IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
+ DateTime startDate,
+ DateTime endDate,
+ DateTime priorValueDate,
+ AccrualBoundary boundary,
+ int annualDays,
+ bool isAnnualized,
+ AccrualTrace? trace = null)
+ {
+ var displayBasis = notional * unwindFraction;
+ decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
+ decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
+
+ trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
+
+ var segStart = startDate;
+
+ for (int si = 0; si < segmentRates.Count; si++)
+ {
+ var segEnd = si < segmentRates.Count - 1
+ ? segmentRates[si + 1].StartDate
+ : endDate;
+
+ var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
+ if (effectiveStart > segEnd) { segStart = segEnd; continue; }
+
+ // calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
+ var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
+ 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; 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;
+ }
+
+ var result = new InterestResult(
+ SwapInterest.Round(accrued, Precision),
+ SwapInterest.Round(accruedUnscaled, Precision));
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index 376aa685..154533a3 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -1386,7 +1386,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: principal,
segmentRates: segmentRates,
startDate: startDate,
@@ -1428,7 +1428,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
notional: accrualBasis,
unwindFraction: closePercent,
@@ -1477,7 +1477,7 @@ namespace YLErp.Modules.SwapModule
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
: 1m;
- // 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。
+ // 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
var legRate = isFixedLeg
? FundingLegRate.Fixed(flowEvent.InterestRate)
@@ -1491,7 +1491,7 @@ namespace YLErp.Modules.SwapModule
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueCompoundEod(
+ var result = CompoundInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
@@ -1506,7 +1506,7 @@ namespace YLErp.Modules.SwapModule
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
- flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
+ flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
preEodPosition.TdInterestPrincipal) * closePercent;
@@ -1533,7 +1533,7 @@ namespace YLErp.Modules.SwapModule
flowEvent.FloatRate = effectiveFloat;
- // 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
+ // 纯数学下沉至 SimpleInterestAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
// 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
var legRate = isFixedLeg
@@ -1547,7 +1547,7 @@ namespace YLErp.Modules.SwapModule
isAnnualized: position.IsAnnualized);
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueSimpleEod(
+ var result = SimpleInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
unwindFraction: closePercent,