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)); + } }