From 03861ea3690f22ac9411f507de27b87ed2ffc6b5 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 12 Aug 2026 06:05:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(interest):=20=E6=96=B0=E5=A2=9E=20Interest?= =?UTF-8?q?Trace=20=E7=BB=93=E6=9E=84=E5=8C=96=E8=BF=BD=E8=B8=AA=E6=94=B6?= =?UTF-8?q?=E9=9B=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 纯值对象(零日志依赖):计息纯函数把"发生了什么"记录为语义化条目 (Day/ResetBefore/ResetAfter/Rollover/Unwind),由适配器统一经 SwapCalcTrace 常驻落盘。ToString 产出稳定可 diff 的逐行文本,新旧引擎同一笔交易可直接 diff 定位"是计算变了还是重构引入漂移"。 --- .../YLErp.Core/Interest/InterestTrace.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Framework/YLErp.Core/Interest/InterestTrace.cs diff --git a/Framework/YLErp.Core/Interest/InterestTrace.cs b/Framework/YLErp.Core/Interest/InterestTrace.cs new file mode 100644 index 00000000..f27a5999 --- /dev/null +++ b/Framework/YLErp.Core/Interest/InterestTrace.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using YLErp.Derivatives.Interest; + +namespace YLErp.Core.Interest; + +/// +/// 计息过程追踪收集器(值对象,非日志)。 +/// +/// 为什么是收集器而不是日志调用:计息数学(SwapInterest / FundingLegAccrual)必须保持纯函数、 +/// 可单测、不依赖 NLog;但按工程铁律,关键路径日志须无条件常驻落盘(出问题时事后翻日志定位,不能依赖开关)。 +/// 折中:纯函数把"发生了什么"记录为结构化条目写入本收集器,由适配器(IO 边界)统一经 +/// SwapCalcTrace.Persist 常驻落盘。落盘职责归一处,计息代码零日志依赖、保持干净。 +/// +/// 可 diff 产出稳定、有序、与 SwapCalcTrace.Day 对齐的逐行文本, +/// 新旧引擎对同一笔交易跑出的 trace 可直接 diff,定位"是计算变了还是重构引入了漂移"。 +/// +/// 所有记录方法均为语义化命名(Day / ResetBefore / Rollover …),调用点一眼即懂,不污染数学可读性。 +/// +public sealed class InterestTrace +{ + private readonly List _entries = new(); + + /// 已记录的追踪条目(只读)。 + public IReadOnlyList Entries => _entries; + + /// 计息区间起点:标记本次计算的整体边界与年化口径。 + public void MarkStart(DateTime start, DateTime end, AccrualBoundary boundary, int annualDays, bool annualized) + => Add(InterestStep.Start, start, + $"START 区间[{start:yyyy-MM-dd},{end:yyyy-MM-dd}] {boundary} annualDays={annualDays} annualized={annualized}"); + + /// 逐日明细:当日生效利率、计息基数、当日利息、累计利息。这是"为何 accrued N 天而非 M 天"的直接证据。 + public void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated) + => Add(InterestStep.DayAccrual, date, + $" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); + + /// 重置日:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。 + public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore) + => Add(InterestStep.ResetBefore, resetDate, + $" RESET↓ {resetDate:yyyy-MM-dd} rate(old)={rateOld:P6} principal(before)={principalBefore:F4}"); + + /// 重置日:生效利率(新)与计息本金(滚动后,已并本金)。利率/本金切换的"果"。 + public void ResetAfter(DateTime resetDate, decimal rateNew, decimal principalAfter) + => Add(InterestStep.ResetAfter, resetDate, + $" RESET↑ {resetDate:yyyy-MM-dd} rate(new)={rateNew:P6} principal(after)={principalAfter:F4}"); + + /// 本金增加(利息滚入计息基数):复利段末并本金的瞬间,记录滚入额与并本金后的新基数。 + public void Rollover(DateTime resetDate, decimal accruedRolled, decimal newBasis) + => Add(InterestStep.Rollover, resetDate, + $" ROLLOVER {resetDate:yyyy-MM-dd} accrued(rolled)={accruedRolled:F6} newBasis={newBasis:F4}"); + + /// 平仓缩放:平仓比例、累计已实现、剩余未实现。 + public void Unwind(DateTime date, decimal unwindPercent, decimal realized, decimal remainingUnrealized) + => Add(InterestStep.Unwind, date, + $" UNWIND {date:yyyy-MM-dd} pct={unwindPercent:P2} realized={realized:F6} remaining={remainingUnrealized:F6}"); + + /// 收尾:最终累计利息与当日利息。 + public void MarkEnd(decimal totalAccrued, decimal totalToday) + => Add(InterestStep.End, default, + $"END accrued={totalAccrued:F6} today={totalToday:F6}"); + + private void Add(InterestStep step, DateTime date, string line) + => _entries.Add(new InterestTraceEntry(step, date, line)); + + /// 稳定可 diff 的逐行文本(与 SwapCalcTrace.Day 格式对齐)。 + public override string ToString() + => _entries.Count == 0 ? "" : string.Join(Environment.NewLine, _entries.Select(e => e.Line)); +} + +/// 追踪条目的语义类别,便于程序化筛选(如"只看重置日")。 +public enum InterestStep +{ + Start, DayAccrual, ResetBefore, ResetAfter, Rollover, Unwind, End +} + +/// 单条追踪记录:类别 + 日期 + 已渲染文本。 +public readonly record struct InterestTraceEntry(InterestStep Step, DateTime Date, string Line);