refactor(accrual): 接线CalcDailyCompoundInterestByEod→AccrueCompoundEod + 全链路Trace

- AccrualTrace新增Segment(分段区间/天数/基数/利率)与EodContext(重置日/平仓比例/基数分量)语义方法
- AccrueSimplePeriod/AccrueCompoundPeriod补全AccrualTrace?参数:MarkStart→逐段Segment→复利Rollover→Unwind→MarkEnd
- CalcDailyCompoundInterestByEod内部实现下沉至FundingLegAccrual.AccrueCompoundEod纯函数
  删除调试垃圾LogFactory('test').Error,取率/重置日/remainingFraction逻辑保留
- flowEvent.InterestPrincipal/FloatRate副作用在调用方保留(下游EOD播种次日TdInterestPrincipal)
- 全量SwapModule测试零回归(7基线失败/508通过/8跳过)
This commit is contained in:
hjhan
2026-08-12 10:37:52 +08:00
parent 2f7caa7a1f
commit 3b02440979
3 changed files with 109 additions and 59 deletions
@@ -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}");
/// <summary>分段明细(多日计息,按重置日分段):段区间[segStart,segEnd)、天数、生效利率、计息基数、段利息、累计利息。
/// 这是分段模型与旧逐日循环对账的核心证据——每段的"前后日期/基数/利率"一目了然。</summary>
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}");
/// <summary>EOD 上下文:计息日、重置日标志、平仓比例、差分基数公式各分量。供日终单日计息定位"重置日分支选对了吗"。</summary>
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}");
/// <summary>重置日<b>前</b>:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。</summary>
public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore)
=> Add(AccrualTraceEvent.ResetBefore, resetDate,
@@ -32,7 +32,11 @@ public static class FundingLegAccrual
DateTime eodDate,
AccrualTrace? trace = null)
{
var basis = priorNotional + notional - baseNotional;
// 计息基数:从昨日滚动,并按"名义本金绝对增量"做加法式 carry(非比例缩放)。
// EOD 路径:调用方传 baseNotional == notional → 增量为 0basis 即 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 → 增量为 0basis 即 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;
}
/// <summary>
@@ -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;
}
}
+56 -50
View File
@@ -1497,62 +1497,68 @@ namespace YLErp.Modules.SwapModule
/// <returns></returns>
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.AccrueCompoundEodDDD 命名 + 末位生产精度 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;
}
/// <summary>