diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
new file mode 100644
index 00000000..36f6d3ac
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using YLErp;
+using YLErp.DBModels;
+using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule;
+using YLErp.Modules.SwapModule.Accrual;
+using YLErp.Derivatives.Interest;
+
+namespace UnitTestProject.Modules.SwapModule.Accrual
+{
+ ///
+ /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
+ ///
+ [TestClass]
+ public class SimplePeriodShadowTest
+ {
+ 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天, 7天周期→重置日 4/28, 5/5
+
+ private static trade CreateTrade()
+ {
+ return new trade
+ {
+ id = 1, TradeNumber = "UT-SIMPLE-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(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
+ }
+
+ ///
+ /// 固定利率(无FR007)算头不算尾,全平,无历史归档。
+ ///
+ [TestMethod]
+ public void 影子_固定利率_无归档_旧新一致()
+ {
+ var td = CreateTrade();
+ var position = CreatePosition();
+ var flowEvent = new swap_flow_event { InterestRate = Spread };
+ var preEod = new eod_swap_position { id = 0, TdInterestPrincipal = 0, InterestProfitSum = 0 };
+
+ // 旧方法
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
+ AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
+
+ // 新方法:固定利率全段相同
+ // 旧代码差分: 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(
+ priorUnrealized: 0m,
+ accrualPrincipal: 0m, // 差分=0(无归档时 preEod.TdInterestPrincipal=0)
+ closeRatio: 1m,
+ segmentRates: segRates,
+ startDate: StartDate,
+ endDate: EndDate,
+ priorValueDate: DateTime.MinValue,
+ 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 一致");
+ }
+
+ ///
+ /// 有历史归档(preEod.id != 0),续接上一日终。
+ ///
+ [TestMethod]
+ public void 影子_有归档_续接_旧新一致()
+ {
+ var position = CreatePosition();
+ var preEodDate = new DateTime(2026, 5, 4); // 上一日终 = 第14天
+ 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 };
+
+ // 旧方法
+ decimal oldI = 0, oldTd = 0;
+ var svc = new StubSvc();
+ svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
+ AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
+
+ // 新方法
+ // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
+ var accrualPrincipal = Notional + Notional - Notional;
+ var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
+ var result = FundingLegAccrual.AccrueSimplePeriod(
+ priorUnrealized: 200_000m * 0.5m, // InterestProfitSum × closePercent
+ accrualPrincipal: accrualPrincipal,
+ closeRatio: 0.5m,
+ 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 一致");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
index 9e2e6da5..183edd75 100644
--- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
+++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
@@ -100,4 +100,75 @@ public static class FundingLegAccrual
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
+
+ ///
+ /// 单利多日计息(纯函数,替换 CalcDailySimpleInterest 的"纯数学"部分)。
+ ///
+ /// 单利特征:计息本金全程恒定(差分公式 = priorAccrualPrincipal + positionPrincipal - originalPv)。
+ /// 按重置日分段,每段用对应利率算天数×日利息(无逐日循环,等价于 SwapInterest.AccrueSimple 分段累加)。
+ ///
+ /// 利率变化点由调用方通过 segmentRates 传入(已取好 FR007),本方法不取价。
+ ///
+ /// 上一日终累计待实现利息(preEod.InterestProfitSum × closeRatio)。
+ /// 计息本金(差分,全程恒定)。
+ /// 平仓比例。
+ /// 分段利率表:(段起日, all-in利率),按日期升序。
+ /// 计息开始日(PosiStartDate)。
+ /// 计息结束日(平仓日)。
+ /// 上一日终归档日(只算此日之后的利息)。
+ /// 算头算尾。
+ /// 年化天数。
+ /// 是否年化。
+ 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));
+ }
}