搬迁(算法体逐字未动,仅换命名空间与归属): - SwapInterest.Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult → YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs - AccrualTrace → Accrual/AccrualTrace.cs(被迫同迁:其 MarkStart 引用 AccrualBoundary, Core 不能反向依赖 DAL) - 引用切换:Simple/CompoundInterestAccrual、AccrualPolicy、SwapCalcTrace、SwapDealService (保留 using YLErp.Derivatives.Interest——IIndexFixer/IndexFixerBase 留 Core) 删除(零生产引用,孤儿清零): - Core:SwapInterest.cs 算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/ AccrueUnrealized/ToInterestRate,未接线且与 DAL 生产实现舍入/rollover 口径已分叉)、 AccrualContext.cs、InterestRate.cs - DAL:AccrualState.cs(零引用死类) - 测试:SwapInterest_CompoundInArrears_RolloverTimingTests.cs(仅测已删原语) 验证:两解决方案 Rebuild 0 错误;磁盘 SwapInterest. 残留 0;影子/分红/场景 86/86 通过 (含 Accrual 3 影子对账、Margin 影子、divPower 新增 AutoUnwindMultiPartial)。 注:AccrualContext 默认精度 11 与生产 12 的分叉隐患随删除一并消除; 已删原语若将来重建须先补对账测试,勿凭记忆复原(ARCHITECTURE.md 已留警告)。
86 lines
6.1 KiB
C#
86 lines
6.1 KiB
C#
namespace YLErp.Modules.SwapModule.Accrual;
|
||
|
||
/// <summary>
|
||
/// 计息过程追踪收集器(值对象,非日志)。2026-08 自 Core 层(YLErp.Core.Interest)迁入 DAL,
|
||
/// 与 Simple/CompoundInterestAccrual、AccrualBoundary 同处一域,Core 不再持有计息类型。
|
||
///
|
||
/// <para><b>为什么是收集器而不是日志调用</b>:计息数学(Simple/CompoundInterestAccrual)必须保持纯函数、
|
||
/// 可单测、不依赖 NLog;但按工程铁律,关键路径日志须<b>无条件常驻落盘</b>(出问题时事后翻日志定位,不能依赖开关)。
|
||
/// 折中:纯函数把"发生了什么"记录为结构化条目写入本收集器,由<b>适配器(IO 边界)</b>统一经
|
||
/// <c>SwapCalcTrace.Write</c> 常驻落盘。落盘职责归一处,计息代码零日志依赖、保持干净。</para>
|
||
///
|
||
/// <para><b>可 diff</b>:<see cref="ToString"/> 产出稳定、有序、与 SwapCalcTrace.Day 对齐的逐行文本,
|
||
/// 新旧引擎对同一笔交易跑出的 trace 可直接 diff,定位"是计算变了还是重构引入了漂移"。</para>
|
||
///
|
||
/// <para>所有记录方法均为语义化命名(Day / ResetBefore / Rollover …),调用点一眼即懂,不污染数学可读性。</para>
|
||
/// </summary>
|
||
public sealed class AccrualTrace
|
||
{
|
||
private readonly List<AccrualTraceEntry> _entries = new();
|
||
|
||
/// <summary>已记录的追踪条目(只读)。</summary>
|
||
public IReadOnlyList<AccrualTraceEntry> Entries => _entries;
|
||
|
||
/// <summary>计息区间起点:标记本次计算的整体边界与年化口径。</summary>
|
||
public void MarkStart(DateTime start, DateTime end, AccrualBoundary boundary, int annualDays, bool annualized)
|
||
=> Add(AccrualTraceEvent.Start, start,
|
||
$"START 区间[{start:yyyy-MM-dd},{end:yyyy-MM-dd}] {boundary} annualDays={annualDays} annualized={annualized}");
|
||
|
||
/// <summary>逐日明细:当日生效利率、计息基数、当日利息、累计利息。这是"为何 accrued N 天而非 M 天"的直接证据。</summary>
|
||
public void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated)
|
||
=> 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 remainingFraction)
|
||
=> Add(AccrualTraceEvent.Start, eodDate,
|
||
$" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} remainingFrac={remainingFraction:P4}");
|
||
|
||
/// <summary>重置日<b>前</b>:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。</summary>
|
||
public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore)
|
||
=> Add(AccrualTraceEvent.ResetBefore, resetDate,
|
||
$" RESET↓ {resetDate:yyyy-MM-dd} rate(old)={rateOld:P6} principal(before)={principalBefore:F4}");
|
||
|
||
/// <summary>重置日<b>后</b>:生效利率(新)与计息本金(滚动后,已并本金)。利率/本金切换的"果"。</summary>
|
||
public void ResetAfter(DateTime resetDate, decimal rateNew, decimal principalAfter)
|
||
=> Add(AccrualTraceEvent.ResetAfter, resetDate,
|
||
$" RESET↑ {resetDate:yyyy-MM-dd} rate(new)={rateNew:P6} principal(after)={principalAfter:F4}");
|
||
|
||
/// <summary>本金增加(利息滚入计息基数):复利段末并本金的瞬间,记录滚入额与并本金后的新基数。</summary>
|
||
public void Rollover(DateTime resetDate, decimal accruedRolled, decimal newBasis)
|
||
=> Add(AccrualTraceEvent.Rollover, resetDate,
|
||
$" ROLLOVER {resetDate:yyyy-MM-dd} accrued(rolled)={accruedRolled:F6} newBasis={newBasis:F4}");
|
||
|
||
/// <summary>平仓缩放:平仓比例、累计已实现、剩余未实现。</summary>
|
||
public void Unwind(DateTime date, decimal unwindPercent, decimal realized, decimal remainingUnrealized)
|
||
=> Add(AccrualTraceEvent.Unwind, date,
|
||
$" UNWIND {date:yyyy-MM-dd} pct={unwindPercent:P2} realized={realized:F6} remaining={remainingUnrealized:F6}");
|
||
|
||
/// <summary>收尾:最终累计利息与当日利息。</summary>
|
||
public void MarkEnd(decimal totalAccrued, decimal totalToday)
|
||
=> Add(AccrualTraceEvent.End, default,
|
||
$"END accrued={totalAccrued:F6} today={totalToday:F6}");
|
||
|
||
private void Add(AccrualTraceEvent step, DateTime date, string line)
|
||
=> _entries.Add(new AccrualTraceEntry(step, date, line));
|
||
|
||
/// <summary>稳定可 diff 的逐行文本(与 SwapCalcTrace.Day 格式对齐)。</summary>
|
||
public override string ToString()
|
||
=> _entries.Count == 0 ? "<empty trace>" : string.Join(Environment.NewLine, _entries.Select(e => e.Line));
|
||
}
|
||
|
||
/// <summary>追踪条目的语义类别(对应 QuantLib/Strata 的"事件"概念),便于程序化筛选(如"只看重置日")。</summary>
|
||
public enum AccrualTraceEvent
|
||
{
|
||
Start, DayAccrual, ResetBefore, ResetAfter, Rollover, Unwind, End
|
||
}
|
||
|
||
/// <summary>单条追踪记录:类别 + 日期 + 已渲染文本。</summary>
|
||
public readonly record struct AccrualTraceEntry(AccrualTraceEvent Step, DateTime Date, string Line);
|