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:
@@ -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 → 增量为 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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user