Files
zszq-trs/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs
T
hjhan e2431e9d44 refactor(accrual): 计息类型整体迁入 DAL——新增 Accrual/InterestMath,删 Core 未接线孤儿
搬迁(算法体逐字未动,仅换命名空间与归属):
- 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 已留警告)。
2026-08-14 15:31:04 +08:00

98 lines
4.5 KiB
C#

using System.Text;
using YLErp.Modules.SwapModule.Accrual;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 计息计算过程追踪器。采用两层设计:
///
/// ① 常驻关键日志(Critical):记录"走了哪条分支 / orginPv 重映射 / preEod.id==0 播种 /
/// closePercent 语义翻转(A→B) / 计息基数 dynomicPrincipal 分解 / ValueDate 地板 /
/// 最终四舍五入输出 / 逐日累加明细"。这些信息**无条件**经 IYcLogger.Info 落盘,
/// 与开关无关——出问题时事后翻日志即可定位,不必事前开开关(否则"出问题了已经晚了")。
///
/// ② 开关态明细(Header/Line/Day,经 IsEnabled 或请求开关):仅在主动排障时把上述信息
/// 额外汇进内存 buffer,便于单元测试 Dump() 断言,或运行时在响应头 X-SwapCalc-Trace
/// 一次性看全貌(URL 追加 ?swaptrace=1 / 请求头 X-SwapCalc-Trace:1 / 标记文件
/// App_Data/swapcalctrace.on 三选一,无需重启)。开关态不影响日志落盘。
///
/// 关键日志常驻后,本类的"零成本"只针对开关态内存 buffer;Critical 的日志写入是常态成本,
/// 但因交易笔数极少、单行极小,可忽略。
/// </summary>
public static class SwapCalcTrace
{
private static readonly IYcLogger _logger = LogFactory.GetLogger("SwapCalc");
// ---- 全局模式(单元测试,保持 ThreadStatic 兼容既有测试) ----
public static bool IsEnabled { get; set; } = false;
[ThreadStatic]
private static List<string> _globalLines;
private static List<string> GlobalLines => _globalLines ??= new List<string>();
// ---- 请求模式(运行时排障,按请求异步流隔离,不会跨请求串扰) ----
private static readonly AsyncLocal<StringBuilder> _reqBuf = new AsyncLocal<StringBuilder>();
private static readonly AsyncLocal<bool> _reqOn = new AsyncLocal<bool>();
public static void EnableForRequest(bool on = true) => _reqOn.Value = on;
public static bool IsRequestEnabled => _reqOn.Value;
public static void ClearForRequest() => _reqBuf.Value = null;
public static void Reset()
{
GlobalLines.Clear();
_reqBuf.Value = null;
}
/// <summary>
/// 常驻关键日志:始终落盘(IYcLogger.Info);开关打开时额外进 buffer 供实时查看/单测断言。
/// 这是事后定位根因的主通道,不应依赖开关。
/// </summary>
public static void Critical(string s)
{
_logger.Info(s);
if (IsEnabled) GlobalLines.Add(s);
if (_reqOn.Value)
{
_reqBuf.Value ??= new StringBuilder();
_reqBuf.Value.AppendLine(s);
}
}
// ---- 以下为开关态(verbose)工具,仅用于单测 Dump 与 ?swaptrace=1 的逐日明细汇总 ----
public static void Header(string title) => Record($"== {title} ==");
public static void Line(string text) => Record(text);
/// <summary>逐日明细。同样常驻落盘——这是"为何 accrued N 天而非 M 天"的直接证据,必须事后可查。</summary>
public static void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated)
=> Critical($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}");
private static void Record(string s)
{
if (IsEnabled) GlobalLines.Add(s);
if (_reqOn.Value)
{
_reqBuf.Value ??= new StringBuilder();
_reqBuf.Value.AppendLine(s);
}
}
public static string Dump() => string.Join(Environment.NewLine, GlobalLines);
public static string DumpForRequest() => _reqBuf.Value?.ToString() ?? "";
/// <summary>
/// 把纯函数产出的 <see cref="AccrualTrace"/> 常驻落盘(关键路径日志)。
/// 每条目经 <see cref="Critical"/> 写出——<b>无条件</b>落盘,与开关无关;
/// 开关打开时同时进内存 buffer 供实时查看 / 单测断言。这是事后 diff 新旧引擎的主通道。
/// </summary>
public static void Write(AccrualTrace? trace)
{
if (trace == null) return;
foreach (var entry in trace.Entries)
Critical(entry.Line);
}
}
}