diff --git a/Framework/YLErp.Core/Interest/AccrualTrace.cs b/Framework/YLErp.Core/Interest/AccrualTrace.cs index d2c6d0bf..25775291 100644 --- a/Framework/YLErp.Core/Interest/AccrualTrace.cs +++ b/Framework/YLErp.Core/Interest/AccrualTrace.cs @@ -35,6 +35,17 @@ public sealed class AccrualTrace => Add(AccrualTraceEvent.DayAccrual, date, $" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); + /// 分段明细(多日计息,按重置日分段):段区间[segStart,segEnd)、天数、生效利率、计息基数、段利息、累计利息。 + /// 这是分段模型与旧逐日循环对账的核心证据——每段的"前后日期/基数/利率"一目了然。 + public void Segment(int idx, DateTime segStart, DateTime segEnd, int days, decimal rate, decimal basis, decimal segInterest, decimal accumulated) + => Add(AccrualTraceEvent.DayAccrual, segStart, + $" [seg{idx}] [{segStart:yyyy-MM-dd},{segEnd:yyyy-MM-dd}) days={days} rate={rate:P6} basis={basis:F4} segInterest={segInterest:F6} acc={accumulated:F6}"); + + /// EOD 上下文:计息日、重置日标志、平仓比例、差分基数公式各分量。供日终单日计息定位"重置日分支选对了吗"。 + public void EodContext(DateTime eodDate, bool isResetDay, decimal unwindFraction, decimal priorAccrued, decimal priorNotional, decimal notional, decimal baseNotional, decimal remainingFraction) + => Add(AccrualTraceEvent.Start, eodDate, + $" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} baseNotional={baseNotional:F4} remainingFrac={remainingFraction:P4}"); + /// 重置日:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。 public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore) => Add(AccrualTraceEvent.ResetBefore, resetDate, diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs index b2f4dab1..7ef4debd 100644 --- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs @@ -32,7 +32,11 @@ public static class FundingLegAccrual DateTime eodDate, AccrualTrace? trace = null) { - var basis = priorNotional + notional - baseNotional; + // 计息基数:从昨日滚动,并按"名义本金绝对增量"做加法式 carry(非比例缩放)。 + // EOD 路径:调用方传 baseNotional == notional → 增量为 0,basis 即 priorNotional(差分退化)。 + // unwind 路径:baseNotional = 上一日终浮动端名义本金(orginPv≠notional) → 增量生效。 + var notionalDelta = notional - baseNotional; // baseNotional = 上一日终浮动端名义本金 + var basis = priorNotional + notionalDelta; var displayBasis = basis * unwindFraction; var allInRate = rate.AllInRate; @@ -72,12 +76,17 @@ public static class FundingLegAccrual DateTime eodDate, AccrualTrace? trace = null) { + // 计息基数:从昨日滚动,并按"名义本金绝对增量"做加法式 carry(非比例缩放)。 + // EOD 路径:调用方传 baseNotional == notional → 增量为 0,basis 即 priorNotional(差分退化)。 + // unwind 路径:baseNotional = 上一日终浮动端名义本金(orginPv≠notional) → 增量生效。 + var notionalDelta = notional - baseNotional; // baseNotional = 上一日终浮动端名义本金 var basis = isResetDay - ? notional + priorAccrued * remainingFraction - : priorNotional + notional - baseNotional; + ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction)) // 重置日:利息按比例并入本金(对齐 legacy 的 [0,1] 钳制) + : priorNotional + notionalDelta; var displayBasis = basis * unwindFraction; var allInRate = rate.AllInRate; + trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, baseNotional, remainingFraction); var dayInterest = displayBasis * allInRate; var tdInterest = basis * allInRate; if (policy.IsAnnualized) @@ -110,11 +119,14 @@ public static class FundingLegAccrual DateTime priorValueDate, AccrualBoundary boundary, int annualDays, - bool isAnnualized) + bool isAnnualized, + AccrualTrace? trace = null) { var displayBasis = notional * unwindFraction; decimal accrued = priorAccrued; + trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized); + var segStart = startDate; var segIncludeStart = boundary.IncludeStart; @@ -132,15 +144,19 @@ public static class FundingLegAccrual if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; } var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; - accrued += displayBasis * dailyRate * days; + var segInterest = displayBasis * dailyRate * days; + accrued += segInterest; + trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued); segStart = segEnd; segIncludeStart = false; } - return new InterestResult( + var result = new InterestResult( SwapInterest.Round(accrued, Precision), SwapInterest.Round(accrued, Precision)); + trace?.MarkEnd(result.Accrued, result.AccruedToday); + return result; } /// @@ -157,11 +173,14 @@ public static class FundingLegAccrual bool isAnnualized, decimal resetCarryInterest = 0m, decimal realizedInterest = 0m, - decimal unwindFraction = 1m) + decimal unwindFraction = 1m, + 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 segEnd = si < segmentRates.Count - 1 @@ -172,8 +191,16 @@ public static class FundingLegAccrual accrualBasis = si == 0 ? notional : notional + accrued; // 末日重置且 carry 非零:用存量替代 + var usedCarry = false; if (segEnd == endDate && si > 0 && resetCarryInterest != 0m) + { accrualBasis = notional + resetCarryInterest; + usedCarry = true; + } + + // 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数) + if (si > 0) + trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis); // 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息) var segIncludeStart = (si == 0) ? boundary.IncludeStart : true; @@ -183,14 +210,20 @@ public static class FundingLegAccrual if (days <= 0) continue; var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; - accrued += accrualBasis * dailyRate * days; + var segInterest = accrualBasis * dailyRate * days; + accrued += segInterest; + trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued); } // 扣除历史已结利息 + if (realizedInterest != 0m) + trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction); accrued -= realizedInterest * unwindFraction; - return new InterestResult( + 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/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 48ff75c2..0a054b78 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1497,62 +1497,68 @@ 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) { - decimal interestProfitSum = preEodPosition.InterestProfitSum; - decimal interest = interestProfitSum * closePercent; - decimal tdinterest = interestProfitSum * closePercent; int interestPeriod = position.interest_rest_days ?? 1; - decimal tdDynomicPrincipal = posiPrincipal; - double floatRate = Convert.ToDouble(floateRate); - var days = (endDate - tradeDate).Days; - LogFactory.GetLogger("test").Error("lksafhasdhfjas"); - if (days % interestPeriod == 0) - { - LogFactory.GetLogger("test").Error("kluausdyfh"); - var remainingPercent = posiPrincipal > 0m - ? principal / posiPrincipal - : 1m; - remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent)); - tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum * remainingPercent; - if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) - { - var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - { - if (fixing != 0m) floatRate = Convert.ToDouble(fixing); - } - else - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); - } + var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0; - } - flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent; - var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - var tdinterest1 = tdDynomicPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - if (position.IsAnnualized) - { - interest1 /= annualDays; - tdinterest1 /= annualDays; - } - interest += interest1; - tdinterest = tdinterest1; - } - else + // 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因—— + // 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。 + decimal effectiveFloat = floateRate; + if (isResetDay && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) { - flowEvent.InterestPrincipal = (preEodPosition.TdInterestPrincipal + tdDynomicPrincipal - orginPv) * closePercent; - var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - var tdinterest1 = (preEodPosition.TdInterestPrincipal + tdDynomicPrincipal - orginPv) * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - if (position.IsAnnualized) + var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); + if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) { - interest1 /= annualDays; - tdinterest1 /= annualDays; + if (fixing != 0m) effectiveFloat = fixing; + } + else + { + throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); } - interest += interest1; - tdinterest = tdinterest1; } - flowEvent.FloatRate = Convert.ToDecimal(floatRate); - InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero); - TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero); + flowEvent.FloatRate = effectiveFloat; + + // remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。 + var remainingFraction = posiPrincipal > 0m + ? 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); + + // 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。 + var interestTrace = new AccrualTrace(); + var result = FundingLegAccrual.AccrueCompoundEod( + priorAccrued: preEodPosition.InterestProfitSum, + priorNotional: preEodPosition.TdInterestPrincipal, + notional: posiPrincipal, + unwindFraction: closePercent, + baseNotional: orginPv, + rate: legRate, + policy: accrualPolicy, + isResetDay: isResetDay, + remainingFraction: remainingFraction, + eodDate: endDate, + trace: interestTrace); + SwapCalcTrace.Write(interestTrace); + + // flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。 + // basis 公式与 AccrueCompoundEod 内部一致;纯函数不返回中间基数,调用方按需重算。 + var basis = isResetDay + ? posiPrincipal + preEodPosition.InterestProfitSum * remainingFraction + : preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; + flowEvent.InterestPrincipal = basis * closePercent; + + InterestAmount = result.Accrued; + TdInterestAmount = result.AccruedToday; } ///