diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
new file mode 100644
index 00000000..bcba5199
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
@@ -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
+{
+ ///
+ /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
+ ///
+ [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)) { }
+ }
+
+ ///
+ /// 固定利率(无FR007)算头不算尾,全平。
+ /// 重置日 4/28, 5/5, 末日 5/11 恰为重置日(21天=3×7)。
+ ///
+ [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 一致");
+ }
+
+ ///
+ /// 部分平仓 30% + consumedInterest 扣除。
+ ///
+ [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 一致");
+ }
+
+ ///
+ /// 算头算尾(calcMode="11")对比。
+ ///
+ [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 一致");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
index 183edd75..bf0c88cc 100644
--- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
+++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
@@ -171,4 +171,92 @@ public static class FundingLegAccrual
Math.Round(interest, precision, MidpointRounding.AwayFromZero),
Math.Round(tdInterest, precision, MidpointRounding.AwayFromZero));
}
+
+ ///
+ /// 复利多日计息(纯函数,替换 CalcDailyCompoundInterest 的"纯数学"部分)。
+ ///
+ /// 复利特征:从 PosiStartDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
+ /// 分段模型:每段从重置日到下一个重置日(或 endDate),段内本金恒定。
+ ///
+ /// 关键语义(与旧逐日循环逐字对齐):
+ /// - 首段(i=0)也算重置日:dynomicPrincipal = principal(从头开始)
+ /// - 后续重置日:dynomicPrincipal = principal + interest(利息并入本金)
+ /// - 末日恰为重置日且 resetCarryInterest != 0:用 resetCarryInterest 替代 replayed interest
+ /// - consumedInterest × closePercent 在最后扣除
+ /// - IsAnnualized 控制 dayRate 是否除以 annualDays
+ ///
+ /// 本次平仓名义本金(已按 closePercent 缩放)。
+ /// 分段利率表:(段起日, all-in利率 = spread + FR007),按日期升序。段起日 = PosiStartDate + k×period。
+ /// PosiStartDate。
+ /// 平仓日。
+ /// 算头算尾。
+ /// 年化天数。
+ /// 是否年化。
+ /// 末日重置时的存量利息(非末日传 0)。
+ /// 历史已结利息(全程绝对值)。
+ /// 平仓比例。
+ 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));
+ }
}