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;
}
///