diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs index 4c2af32c..a904af3a 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] @@ -92,14 +92,14 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldInterest = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position, - Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional, + Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, ref oldInterest, ref oldTd); // 新方法 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); @@ -125,13 +125,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldInterest = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position, - Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional, + Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, ref oldInterest, ref oldTd); // 新方法 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..855419c5 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 @@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 1m, Notional, true, false, + AnnualDays, false, 0m, 1m, true, false, ref oldI, ref oldTd); // 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7 @@ -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, @@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent, - AnnualDays, false, 0m, closePct, Notional, true, false, + AnnualDays, false, 0m, closePct, true, false, ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry); // 新方法 @@ -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, @@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 1m, Notional, true, true, + AnnualDays, false, 0m, 1m, true, true, ref oldI, ref oldTd); // 新方法 @@ -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/ConsumedInterestScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs index 6ad2800d..9b70262f 100644 --- a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs @@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule decimal tdInterestAmount = 0m; service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position, - principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, principal, + principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, ref interestAmount, ref tdInterestAmount); AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal, diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs index 29c1b37a..ad142e51 100644 --- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -1725,14 +1725,14 @@ namespace YLErp.Modules.SwapModule decimal expectedTdAmountAtEnd = 0m; dealService.CalcDailyCompoundInterest( finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false, - intermediateEod.FloatRate, 1m, originalNotional, true, false, + intermediateEod.FloatRate, 1m, true, false, ref expectedAmountAtEnd, ref expectedTdAmountAtEnd); var expectedPreviousFlow = new swap_flow_event { InterestRate = spread }; decimal expectedAmountAtPreviousEod = 0m; decimal expectedTdAmountAtPreviousEod = 0m; dealService.CalcDailyCompoundInterest( intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false, - intermediateEod.FloatRate, 1m, originalNotional, true, true, + intermediateEod.FloatRate, 1m, true, true, ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod); var expectedFinalInterest = intermediateEod.InterestIncomeSum + expectedAmountAtEnd - expectedAmountAtPreviousEod; 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/Margin/MarginModes.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs index 7232022f..359e3fc9 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs @@ -41,4 +41,17 @@ public static class MarginModes /// 判断 mode 是否属于保证金(非 LINQ 场景用)。 public static bool Contains(int interestMode) => All.Contains(interestMode); + + /// 固定值 + 保证金 mode 集合(固定值/初始预付金/追加预付金)。 + /// 用于 EOD 场景判断"计息基数取 InterestPrincipalFix 而非持仓名义本金"的腿。 + /// 替代 SwapEodPositionService 中 3 处内联 new List{固定值, 初始预付金, 追加预付金}。 + public static readonly IReadOnlyCollection FixedAmountAndMargin = new HashSet + { + (int)InterestModeEnum.固定值, + (int)InterestModeEnum.初始预付金, + (int)InterestModeEnum.追加预付金, + }; + + /// 判断 mode 是否为固定值或保证金。 + public static bool IsFixedAmountOrMargin(int interestMode) => FixedAmountAndMargin.Contains(interestMode); } diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs index 13808855..33d80a87 100644 --- a/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs +++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs @@ -1,5 +1,6 @@ using YLErp.DBModels; using YLErp.DBModels.Enums; +using YLErp.Modules.SwapModule.Margin; namespace YLErp.Modules.SwapModule.ReturnLegs; @@ -19,4 +20,17 @@ public static class DirectionRatio /// 收付方向因子。收取=+1, 支付=-1。 public static int ReceivePay(int direction) => direction == (int)SwapDirectionEnum.收取 ? 1 : -1; + + /// 利息腿 PnL 方向因子。收取=+1/支付=-1;保证金腿翻转(利息现金流与本金方向相反)。 + /// 原 7 处内联 `收取?1:-1; if(MarginModes) ratio=-ratio` 收口到此。 + public static int InterestLegPnl(int interestDirection, int interestMode) + { + var ratio = ReceivePay(interestDirection); + return MarginModes.Contains(interestMode) ? -ratio : ratio; + } + + /// 按收付方向选汇率类型。收取→Buy, 支付→Sell。 + /// 原 7 处内联 `收取 ? Buy : Sell` 收口到此。 + public static CurrencyRateType RateType(int direction) + => direction == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell; } diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs new file mode 100644 index 00000000..8f1b2aa4 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs @@ -0,0 +1,21 @@ +namespace YLErp.Modules.SwapModule.ReturnLegs; + +/// +/// EOD 利息腿展示值计算——轻量单日公式(非 SwapDealService 的分段计息引擎)。 +/// +public static class InterestIncomeCalc +{ + /// 日应计利息 = 本金 × (固定利率 + 浮动利差) ÷ 年化天数(若年化)。 + /// 原 3 处内联 `principal*(rate+float); if(annualized) /=annualDays` 收口到此。 + public static decimal DailyAccrual(decimal principal, decimal rate, decimal floatRate, bool isAnnualized, int annualDays) + { + var amount = principal * (rate + floatRate); + return isAnnualized ? amount / annualDays : amount; + } + + /// 已实现利息滚存。(prev + today×ratio, prevFee + todayFee)。 + /// 原 4 处内联 2 行赋值收口到此。 + public static (decimal Interest, decimal Fee) RollRealized( + decimal prevInterest, decimal prevFee, decimal closeInterest, decimal closeInterestFee, int ratio) + => (prevInterest + closeInterest * ratio, prevFee + closeInterestFee); +} diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs index 4bbb5620..7dedab2c 100644 --- a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs +++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs @@ -19,4 +19,12 @@ public static class MtmCalc /// 收取=1, 支付=-1。 public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio) => (price - costGrossPrice) * qty * contractSize * shortRatio * ratio; + + /// 浮动端总未实现盈亏 = 盯市盈亏 + 分红 + 待结费用。原 4 处内联收口到此。 + public static decimal ReturnLegProfitSum(decimal mtmPnl, decimal dividendSum, decimal feePending) + => mtmPnl + dividendSum + feePending; + + /// 加权均价混合:(昨日均价×昨日量 + 今日∑(量×均额)) / 总量。原 4 处内联收口到此。 + public static decimal BlendPrice(decimal prevPrice, decimal prevQty, decimal sumQtyTimesPrice, decimal totalQty) + => (prevPrice * prevQty + sumQtyTimesPrice) / totalQty; } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 307f3b5f..00f3419d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1096,12 +1096,12 @@ namespace YLErp.Modules.SwapModule if (position.InterestType == (int)InterestTypeEnum.复利) { // 复利计算 - CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount); + CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); } else { // 单利计算 - CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount); + CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); } } @@ -1272,7 +1272,7 @@ namespace YLErp.Modules.SwapModule // 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。 var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent; CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, - floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, + floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest, resetCarryInterest); if (preEodPosition.id != 0 && closePrecent == 1m) { @@ -1299,7 +1299,7 @@ namespace YLErp.Modules.SwapModule } // 计算截至本次平仓日的累计利息 amountAtEnd CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue, - interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv, + interestAtEnd, annualDays, needPrice, floateRate, closePrecent, calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest); var interestAtPreviousEod = new swap_flow_event { InterestRate = rate }; decimal amountAtPreviousEod = 0m; @@ -1308,7 +1308,7 @@ namespace YLErp.Modules.SwapModule // 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。 // 计算截至上一日终累积的利息 amountAtPreviousEod CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue, - interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv, + interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest); // 例如 0004:5/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760, // 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。 @@ -1334,6 +1334,29 @@ namespace YLErp.Modules.SwapModule return interest; } + /// + /// 按 interest_rule 取 FR007 定盘价。无浮动标的时返回 fallback;取不到抛异常。 + /// EOD 单日取率 + BuildSegmentRates 多日取率共用此方法,FR007 定盘逻辑收口到一处。 + /// + private decimal ResolveFloatRate(swap_position position, DateTime date, decimal fallback) + { + if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback; + var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule); + if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) + return fixing != 0m ? fixing : fallback; + throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); + } + + /// 构造利率值对象:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。 + private static FundingLegRate BuildLegRate(swap_position position, decimal spread, decimal effectiveFloat) + => string.IsNullOrEmpty(position.FloatRateUnderlyingCode) + ? FundingLegRate.Fixed(spread) + : FundingLegRate.Floating(spread, effectiveFloat); + + /// 构造 EOD 计息政策(算头算尾,重置周期取 position.interest_rest_days)。 + private static AccrualPolicy BuildEodPolicy(swap_position position, int annualDays, bool isCompound) + => new AccrualPolicy(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized); + /// /// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。 /// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。 @@ -1349,19 +1372,8 @@ namespace YLErp.Modules.SwapModule for (int i = 0; i <= calcDays; i += interestPeriod) { var resetDate = startDate.AddDays(i); - if ((fetchAfterDate == null || resetDate > fetchAfterDate.Value) - && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) - { - var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - { - if (fixing != 0m) currentFloat = fixing; - } - else - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); - } - } + if (fetchAfterDate == null || resetDate > fetchAfterDate.Value) + currentFloat = ResolveFloatRate(position, resetDate, currentFloat); rates.Add((resetDate, spread + currentFloat)); } return (rates, currentFloat); @@ -1379,7 +1391,7 @@ namespace YLErp.Modules.SwapModule /// 年化天数 /// public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, - int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, + int annualDays, bool needPrice, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m) { var startDate = position.PosiStartDate; @@ -1392,7 +1404,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, @@ -1434,7 +1446,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, @@ -1468,26 +1480,14 @@ namespace YLErp.Modules.SwapModule /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) { int interestPeriod = position.interest_rest_days ?? 1; var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0; // 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因—— // 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。 - decimal effectiveFloat = floateRate; - if (isResetDay && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) - { - var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - { - if (fixing != 0m) effectiveFloat = fixing; - } - else - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); - } - } + var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate; flowEvent.FloatRate = effectiveFloat; // remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。 @@ -1495,21 +1495,13 @@ namespace YLErp.Modules.SwapModule ? Math.Max(0m, Math.Min(1m, principal / posiPrincipal)) : 1m; - // 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。 - var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode); - var legRate = isFixedLeg - ? FundingLegRate.Fixed(flowEvent.InterestRate) - : FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat); - var accrualPolicy = new AccrualPolicy( - convention: AccrualBoundary.Both, - isCompound: true, - resetPeriodDays: interestPeriod, - annualDays: annualDays, - isAnnualized: position.IsAnnualized); + // 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。 + var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat); + var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: true); // 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。 var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueCompoundEod( + var result = CompoundInterestAccrual.AccrueEod( priorAccrued: preEodPosition.InterestProfitSum, priorNotional: preEodPosition.TdInterestPrincipal, notional: posiPrincipal, @@ -1524,7 +1516,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; @@ -1535,7 +1527,7 @@ namespace YLErp.Modules.SwapModule /// /// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率) /// - public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) { // 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。 if (preEodPosition.id == 0) @@ -1545,39 +1537,18 @@ namespace YLErp.Modules.SwapModule // 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因—— // 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。 - decimal effectiveFloat = floateRate; int interestPeriod = position.interest_rest_days ?? 1; - if ((endDate - tradeDate).Days % interestPeriod == 0 - && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) - { - var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - { - if (fixing != 0m) effectiveFloat = fixing; - } - else - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); - } - } + var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0; + var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate; flowEvent.FloatRate = effectiveFloat; - // 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。 - // 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。 - var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode); - var legRate = isFixedLeg - ? FundingLegRate.Fixed(flowEvent.InterestRate) - : FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat); - var accrualPolicy = new AccrualPolicy( - convention: AccrualBoundary.Both, - isCompound: false, - resetPeriodDays: position.interest_rest_days ?? 1, - annualDays: annualDays, - isAnnualized: position.IsAnnualized); + // 纯数学下沉至 SimpleInterestAccrual(末位生产精度 12 舍入)。 + var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat); + var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: false); // 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。 var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueSimpleEod( + var result = SimpleInterestAccrual.AccrueEod( priorAccrued: preEodPosition.InterestProfitSum, priorNotional: preEodPosition.TdInterestPrincipal, unwindFraction: closePercent, diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index f8d9dabb..e445a357 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -662,7 +662,7 @@ namespace YLErp.Modules.SwapModule decimal premiumTotal = 0; premiumInterests.ForEach(x => { - var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1; + var ratio = -DirectionRatio.ReceivePay(x.InterestDirection); premiumTotal += x.InterestClosePnL * ratio; }); unwindData.SwapMarginRebatePnl = premiumTotal; @@ -670,7 +670,7 @@ namespace YLErp.Modules.SwapModule decimal interestTotal = 0; interestLegs.ForEach(x => { - var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; + var ratio = DirectionRatio.ReceivePay(x.InterestDirection); interestTotal += x.InterestClosePnL * ratio; }); unwindData.SwapCloseAmount = interestTotal ; @@ -1041,11 +1041,7 @@ namespace YLErp.Modules.SwapModule eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; } var tradeExtend = td.trade_extend.ExtendObj; - decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 - if (MarginModes.Contains(position.InterestMode)) - { - ratio = -ratio; - } + var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); if (newEodPayPosition == null) { newEodPayPosition = new eod_swap_position(); @@ -1085,11 +1081,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; //持仓内容-利息腿-损益统计(本方视角) - var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - intersetAcmount /= tradeExtend.AnnualDays; - } + var intersetAcmount = InterestIncomeCalc.DailyAccrual( + newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息 var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome; var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee; @@ -1115,11 +1109,12 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); + newEodPayPosition.RealizedInterest = rolled.Interest; + newEodPayPosition.RealizedInterestFee = rolled.Fee; SetFixedLegRealizedPnl(newEodPayPosition); var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + DirectionRatio.RateType(position.InterestDirection)); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); PersistEodSwapPosition(newEodPayPosition); } @@ -1176,11 +1171,7 @@ namespace YLErp.Modules.SwapModule var tradeExtend = td.trade_extend.ExtendObj; decimal posiNotionalValue = posiLongNotional + posiShortNational; decimal closePercent = 1; - decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 - if (MarginModes.Contains(position.InterestMode)) - { - ratio = -ratio; - } + var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); if (eodPayPosition == null) { eodPayPosition = new eod_swap_position(); @@ -1198,7 +1189,7 @@ namespace YLErp.Modules.SwapModule positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); - var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; + var interestModes = MarginModes.FixedAmountAndMargin; if (interestModes.Contains(position.InterestMode)) { orginPv = eodPayPosition.InterestPrincipalFix; @@ -1258,11 +1249,12 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); + newEodPayPosition.RealizedInterest = rolled.Interest; + newEodPayPosition.RealizedInterestFee = rolled.Fee; SetFixedLegRealizedPnl(newEodPayPosition); var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + DirectionRatio.RateType(position.InterestDirection)); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); PersistEodSwapPosition(newEodPayPosition); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); @@ -1301,11 +1293,7 @@ namespace YLErp.Modules.SwapModule decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational; decimal posiNotionalValue = posiLongNotional + posiShortNational; // ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。 - decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 - if (MarginModes.Contains(position.InterestMode)) - { - ratio = -ratio; - } + var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); // 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。 // 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum // 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。 @@ -1339,7 +1327,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition = eodPayPosition.Clone(); newEodPayPosition.id = 0; } - var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; + var interestModes = MarginModes.FixedAmountAndMargin; if (interestModes.Contains(position.InterestMode)) { orginPv = eodPayPosition.InterestPrincipalFix; @@ -1381,7 +1369,7 @@ namespace YLErp.Modules.SwapModule var autoInterest = interests[0]; autoInterest.InterestAmount = autoSettledInterestAmount; autoInterest.InterestClosePnL = autoSettledInterestAmount - * (autoInterest.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m); + * DirectionRatio.ReceivePay(autoInterest.InterestDirection); } newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; @@ -1427,11 +1415,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount; // intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用 // 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。 - var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - intersetAcmount /= tradeExtend.AnnualDays; - } + var intersetAcmount = InterestIncomeCalc.DailyAccrual( + newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = autoSwap ? intersetAcmount : !hasPreviousEod @@ -1481,12 +1467,9 @@ namespace YLErp.Modules.SwapModule var accrualPrincipal = calcLast ? fullPrincipal : newEodPayPosition.TdInterestPrincipal; - newEodPayPosition.TdInterestIncome = accrualPrincipal - * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays; - } + newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual( + accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); } if (!autoSwap && closePercent > 0m && closePercent < 1m @@ -1532,11 +1515,12 @@ namespace YLErp.Modules.SwapModule //累计已实现 // RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。 // 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); + newEodPayPosition.RealizedInterest = rolled.Interest; + newEodPayPosition.RealizedInterestFee = rolled.Fee; SetFixedLegRealizedPnl(newEodPayPosition); var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + DirectionRatio.RateType(position.InterestDirection)); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition); @@ -1556,7 +1540,7 @@ namespace YLErp.Modules.SwapModule Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); List intervals = position.SwapIntervalList; var tradeExtend = td.trade_extend.ExtendObj; - var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; + var interestModes = MarginModes.FixedAmountAndMargin; if (eodPayPosition == null) { //if (position.PosiStartDate > valueDate) @@ -1611,11 +1595,7 @@ namespace YLErp.Modules.SwapModule { closePercent = 1; } - decimal ratio = eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 - if (MarginModes.Contains(position.InterestMode)) - { - ratio = -ratio; - } + var ratio = DirectionRatio.InterestLegPnl(eodPayPosition.InterestDirection, position.InterestMode); List positions = new List { position @@ -1654,11 +1634,12 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); + newEodPayPosition.RealizedInterest = rolled.Interest; + newEodPayPosition.RealizedInterestFee = rolled.Fee; SetFixedLegRealizedPnl(newEodPayPosition); var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + DirectionRatio.RateType(eodPayPosition.InterestDirection)); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition); @@ -1699,7 +1680,7 @@ namespace YLErp.Modules.SwapModule bool open) { payQty = Math.Abs(payQty); - int ratio = eventFlow.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负 + int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负 int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType); newEodPayPosition.ValueDate = eventFlow.PayDate.Value; newEodPayPosition.PositionId = eventFlow.PositionId; @@ -1747,7 +1728,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2); newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio); newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2); - newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending; + newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending); //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum); @@ -1799,7 +1780,7 @@ namespace YLErp.Modules.SwapModule } var dealDate = curretEod.ValueDate; int shortRatio = DirectionRatio.LongShort(eod.PositionType); - int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; + int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); @@ -1820,7 +1801,7 @@ namespace YLErp.Modules.SwapModule curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); //curretEod.TdPosiDividend = 0; //curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend; - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.TdCloseFee = 0; curretEod.TdCloseQty = 0; curretEod.TdCloseMtmPnl = 0; @@ -1831,7 +1812,7 @@ namespace YLErp.Modules.SwapModule curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; SetFloatingRealizedPnl(curretEod); var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value - , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); @@ -1891,7 +1872,7 @@ namespace YLErp.Modules.SwapModule } var dealDate = curretEod.ValueDate; int shortRatio = DirectionRatio.LongShort(eod.PositionType); - int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; + int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents); var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice; @@ -1933,13 +1914,13 @@ namespace YLErp.Modules.SwapModule SetFloatingRealizedPnl(curretEod); curretEod.SwapPositionValue -= curretEod.TdCloseDividend; - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); if (curretEod.PosiStatus == 1) { curretEod.PosiNotionalValue = 0; } var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value - , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); @@ -1979,7 +1960,7 @@ namespace YLErp.Modules.SwapModule return; } int shortRatio = DirectionRatio.LongShort(eod.PositionType); - int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; + int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection); var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList(); decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity); @@ -2012,16 +1993,16 @@ namespace YLErp.Modules.SwapModule { posiQty = 0; } - curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty); + curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty); curretEod.PosiGrossPrice = Math.Round( curretEod.PosiGrossPrice, GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode), MidpointRounding.AwayFromZero); - curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty); + curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty); curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty); + curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty); curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - curretEod.PosiNetFeePrice = (eod.PosiNetFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetFeeAvg)) / (eod.PosiQuantity + openQty); + curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty); curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize; @@ -2060,7 +2041,7 @@ namespace YLErp.Modules.SwapModule curretEod.PositionId = position.id; curretEod.ClientId = td.ClientId; int shortRatio = DirectionRatio.LongShort(position.PositionType); - int directionRatio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; + int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection); curretEod.PositionType = position.PositionType; var eod = new eod_swap_position() { @@ -2106,7 +2087,7 @@ namespace YLErp.Modules.SwapModule curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio); curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = curretEod.TdCloseDividend; curretEod.RealizedFee = curretEod.TdCloseFee; @@ -2119,7 +2100,7 @@ namespace YLErp.Modules.SwapModule //持仓价值 curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum); var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value - , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); + , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); UpdateDbOption(curretEod); curretEod.Invalid = false; @@ -2194,8 +2175,6 @@ namespace YLErp.Modules.SwapModule var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 // 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模, // 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。 - eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); eod_Swap.SwapTradeId = td.id; eod_Swap.SwapTradeNo = td.TradeNumber; @@ -2203,23 +2182,8 @@ namespace YLErp.Modules.SwapModule eod_Swap.BookId = td.AssetId; eod_Swap.ValueDate = settleDate; eod_Swap.StructureType = td.StructureType; - eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); - eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); - eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); - eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); - decimal interestPnL = 0; - // 利息腿按我方视角归集。保证金腿的利息现金流方向与普通利息腿相反, - // 因此保证金腿需要额外反转符号,确保 InterestPnL 表示我方的合约利率端收益。 - interestPositions.ForEach(x => - { - decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负 - if (MarginModes.Contains(x.InterestMode)) - { - ratio = -ratio; - } - interestPnL += x.InterestProfitSum * ratio; - }); - eod_Swap.InterestPnL = interestPnL; + FillPositionLegSummary(eod_Swap, positions); + eod_Swap.InterestPnL = SumInterestPnL(interestPositions); eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue); // 保证金腿的利息现金流方向与保证金本金方向相反。 // 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的 @@ -2272,33 +2236,15 @@ namespace YLErp.Modules.SwapModule var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿 var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); - eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); - eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); - eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); - interestPositions.ForEach(x => - { - decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负 - if (MarginModes.Contains(x.InterestMode)) - { - ratio = -ratio; - } - eod_Swap.InterestPnL += x.InterestProfitSum * ratio; - }); + FillPositionLegSummary(eod_Swap, positions); + eod_Swap.InterestPnL += SumInterestPnL(interestPositions); eodSwapPositions.ForEach(x => { - decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负 - if (MarginModes.Contains(x.InterestMode)) - { - ratio = -ratio; - } + var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee; }); eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue); eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl); - eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty); var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id); var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.HappenDate == settleDate).ToList(); var addMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.HappenDate == settleDate).ToList(); @@ -2319,11 +2265,7 @@ namespace YLErp.Modules.SwapModule /// public static decimal CalculateSwapRealizedPnl(eod_swap_position position) { - var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; - if (MarginModes.Contains(position.InterestMode)) - { - interestRatio = -interestRatio; - } + var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); return position.RealizedMtmPnL + position.RealizedDividend @@ -2332,6 +2274,28 @@ namespace YLErp.Modules.SwapModule + position.RealizedInterestFee; } + /// 填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。 + /// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。 + private static void FillPositionLegSummary(eod_swap eod_Swap, List positions) + { + eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); + eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); + eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); + eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); + eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty); + } + + /// 利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。 + private static decimal SumInterestPnL(List interestPositions) + { + decimal interestPnL = 0; + foreach (var x in interestPositions) + interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); + return interestPnL; + } + /// /// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest /// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负; @@ -2343,15 +2307,11 @@ namespace YLErp.Modules.SwapModule { if (position.InterestDirection <= 0) return; - var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; - if (MarginModes.Contains(position.InterestMode)) - { - interestRatio = -interestRatio; - } - else if (position.InterestMode == (int)InterestModeEnum.标的期初全价) + if (position.InterestMode == (int)InterestModeEnum.标的期初全价) { return; } + var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio; position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio; // 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。 @@ -2845,11 +2805,11 @@ namespace YLErp.Modules.SwapModule var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault(); item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode; item.position.FloatRate = floatRateInterest?.FloatRate ?? 0; - item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1)); + item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins); - item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1)); + item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins); - item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1)); + item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection))); item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault); // 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。 var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m; @@ -2892,7 +2852,7 @@ namespace YLErp.Modules.SwapModule public static decimal CalculateWeightedMarginInterest(IEnumerable margins) { return margins.Sum(x => - x.InterestIncomeSum * (x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1)); + x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection)); } ///