From 46cf7025a4a067f2c6f9f1a4dde19a7268a652ec Mon Sep 17 00:00:00 2001 From: hjhan Date: Sat, 8 Aug 2026 16:32:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(trace):=20=E8=AE=A1=E6=81=AF=E5=85=A8?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=9F=8B=E7=82=B9=20+=20=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B8=B8=E9=A9=BB=E8=90=BD=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖补全 CalcDailyCompoundInterest / CalcDailySimpleInterestByEod / CalcDailyCompoundInterestByEod / InitSwapDealInterest 分支与复利跨期直取 / orginPv 重映射 / preEod.id==0 播种 / ToRemainingClosePercent 语义翻转。 关键决策点改用 Critical() 无条件落盘(IYcLogger.Info, 类别 SwapCalc), 与开关解耦——出问题事后翻日志即可定位,不必事前开开关。 开关态(Header/Line/Day)仅作排障便利(?swaptrace=1 / 单测 Dump)。 纯增日志不改控制流;YLErpDAL 与 UnitTestProject 编译0错, PrepaidPrincipal trace 测试 2/2 通过(SKIP_INITIALIZATION)。 --- YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs | 43 +++++++++++++++---- .../Modules/SwapModule/SwapDealService.cs | 41 ++++++++++++------ 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs index 2a8c8843..98d4378e 100644 --- a/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs +++ b/YLErpDAL/Modules/SwapModule/SwapCalcTrace.cs @@ -1,21 +1,30 @@ using System; using System.Collections.Generic; using System.Text; +using YLErp.Helpers; namespace YLErp.Modules.SwapModule { /// - /// 计息计算过程追踪器。默认完全关闭,零运行时成本;开启后逐日记录: - /// 计息起点、日终归档 ValueDate 地板、计息基数 dynomicPrincipal、当日利率、当日利息、累计利息。 - /// 便于定位"算出来一个数却不对"的根因,也便于把一次真实平仓的逐步过程打印出来与 Excel 对账。 + /// 计息计算过程追踪器。采用两层设计: /// - /// 两种开启方式(互不冲突): - /// 1) 全局(单元测试):SwapCalcTrace.IsEnabled = true; SwapCalcTrace.Reset(); ...; SwapCalcTrace.Dump(); - /// 2) 请求(运行时排障,无需重启):经 SwapCalcTraceMiddleware,在 URL 追加 ?swaptrace=1 - /// 或请求头 X-SwapCalc-Trace: 1 即可,追踪文本会出现在响应头 X-SwapCalc-Trace(超长落 logs/ 并在头部给路径)。 + /// ① 常驻关键日志(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 的日志写入是常态成本, + /// 但因交易笔数极少、单行极小,可忽略。 /// public static class SwapCalcTrace { + private static readonly IYcLogger _logger = LogFactory.GetLogger("SwapCalc"); + // ---- 全局模式(单元测试,保持 ThreadStatic 兼容既有测试) ---- public static bool IsEnabled { get; set; } = false; @@ -38,12 +47,28 @@ namespace YLErp.Modules.SwapModule _reqBuf.Value = null; } + /// + /// 常驻关键日志:始终落盘(IYcLogger.Info);开关打开时额外进 buffer 供实时查看/单测断言。 + /// 这是事后定位根因的主通道,不应依赖开关。 + /// + 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); - /// 记录某一计息日的明细。 + /// 逐日明细。同样常驻落盘——这是"为何 accrued N 天而非 M 天"的直接证据,必须事后可查。 public static void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated) - => Record($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); + => Critical($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}"); private static void Record(string s) { diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 3bec844e..eb218cb9 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -944,6 +944,7 @@ namespace YLErp.Modules.SwapModule { if (posiNotionalValue <= 0) return originalClosePercent; var remaining = originalClosePercent * notionalValue / posiNotionalValue; + SwapCalcTrace.Critical($" [平仓比例语义翻转 A→B] original(占期初)={originalClosePercent} notional={notionalValue} posiNotional={posiNotionalValue} → remaining(占剩余)={remaining}"); return remaining > 1 ? 1 : remaining; } @@ -1123,6 +1124,7 @@ namespace YLErp.Modules.SwapModule { if (preEod.id == 0) { + SwapCalcTrace.Critical($" [preEod.id==0 播种] 无上一日终归档→以交易起始日初始化: TdInterestPrincipal={posiPrincipal:F4} ValueDate={td.StartDate.Value:yyyy-MM-dd}{(calcFirst ? " (calcFirst→-1天)" : "")}"); preEod.FloatRate = floatRate; preEod.TdInterestPrincipal = posiPrincipal; preEod.PosiNotionalValue = posiPrincipal; @@ -1197,6 +1199,7 @@ namespace YLErp.Modules.SwapModule if (position.InterestMode == (int)InterestModeEnum.初始预付金 || position.InterestMode == (int)InterestModeEnum.追加预付金) { + SwapCalcTrace.Critical($" [orginPv重映射] InterestMode={position.InterestMode} 预付金腿: orginPv {orginPv:F4} → InterestPrincipalFix({position.InterestPrincipalFix:F4})"); orginPv = position.InterestPrincipalFix; } @@ -1213,14 +1216,20 @@ namespace YLErp.Modules.SwapModule decimal TdInterestAmount = 0; var interestRatio = position.InterestDirection == 1 ? 1m : -1m; var floateRate = preEodPosition.FloatRate; + SwapCalcTrace.Critical($"InitSwapDealInterest posId={position.id} mode={position.InterestMode} type={(position.InterestType == (int)InterestTypeEnum.复利 ? "复利" : "单利")} swap={swap}"); + SwapCalcTrace.Critical($" preEod.id={preEodPosition.id} ValueDate={preEodPosition.ValueDate:yyyy-MM-dd} FloatRate={preEodPosition.FloatRate}"); + SwapCalcTrace.Critical($" endDate={endDate:yyyy-MM-dd} closePercent={closePrecent} posiNotional={posiNotionalValue:F4} closeNotional={closePosiNotionalValue:F4} orginPv={orginPv:F4} calcFirst={calcFirst} calcLast={calcLast}"); if (position.InterestType == (int)InterestTypeEnum.复利) { var daysFromStart = (endDate - position.PosiStartDate).Days; var daysFromPreEod = preEodPosition.id != 0 ? (endDate - preEodPosition.ValueDate).Days : 0; + SwapCalcTrace.Critical($" 复利: daysFromStart={daysFromStart} daysFromPreEod={daysFromPreEod} interest_rest_days={position.interest_rest_days ?? 1}"); if (!calcLast && daysFromPreEod == 1 && daysFromStart % (position.interest_rest_days ?? 1) != 0) { + SwapCalcTrace.Critical($" [复利跨期直取] 直接用 preEod: InterestAmount={preEodPosition.InterestIncomeSum * closePrecent:F4} (不走重放)"); + interest.InterestPrincipal = preEodPosition.TdInterestPrincipal * closePrecent; interest.FloatRate = preEodPosition.FloatRate; InterestAmount = preEodPosition.InterestIncomeSum * closePrecent; @@ -1278,6 +1287,9 @@ namespace YLErp.Modules.SwapModule decimal tdDynomicPrincipal = dynomicPrincipal; var calcDays = (endDate - startDate).Days; double floatRate = Convert.ToDouble(floateRate); + SwapCalcTrace.Critical($"CalcDailyCompoundInterest(盘中复利) posId={position.id} principal={principal:F4} calcFirst={calcFirst} calcLast={calcLast}"); + SwapCalcTrace.Critical($" PosiStartDate={startDate:yyyy-MM-dd} calcDays={calcDays} interest_rest_days={interestPeriod} orginPv={orginPv:F4}"); + SwapCalcTrace.Critical($" consumedInterest={consumedInterest:F4} resetCarryInterest={resetCarryInterest:F4} closePercent={closePercent}"); for (int i = 0; i <= calcDays; i++) { var accrueDate = startDate.AddDays(i); @@ -1360,15 +1372,12 @@ namespace YLErp.Modules.SwapModule decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; decimal tdDynomicPrincipal = dynomicPrincipal; var calcDays = (endDate - startDate).Days; - if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled) - { - SwapCalcTrace.Header($"CalcDailySimpleInterest posId={position.id} mode={position.InterestMode} type={(position.InterestType == (int)InterestTypeEnum.复利 ? "复利" : "单利")}"); - SwapCalcTrace.Line($" PosiStartDate={startDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} calcDays={calcDays} calcFirst={calcFirst} calcLast={calcLast}"); - SwapCalcTrace.Line($" preEod.id={preEodPosition.id} ValueDate={preEodPosition.ValueDate:yyyy-MM-dd} TdInterestPrincipal={preEodPosition.TdInterestPrincipal:F4} InterestProfitSum={interestProfitSum:F4}"); - SwapCalcTrace.Line($" dynomicPrincipal = TdInterestPrincipal({preEodPosition.TdInterestPrincipal:F4}) + posiPrincipal({posiPrincipal:F4}) - orginPv({orginPv:F4}) = {dynomicPrincipal:F4}"); - SwapCalcTrace.Line($" 已结扣除(InterestProfitSum*closePercent)={interest:F4} consumedInterest={consumedInterest:F4} closePercent={closePercent}"); - SwapCalcTrace.Line($" 逐日累加上限: accrueDate > preEod.ValueDate({preEodPosition.ValueDate:yyyy-MM-dd}) 才计息"); - } + SwapCalcTrace.Critical($"CalcDailySimpleInterest posId={position.id} mode={position.InterestMode} type={(position.InterestType == (int)InterestTypeEnum.复利 ? "复利" : "单利")}"); + SwapCalcTrace.Critical($" PosiStartDate={startDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} calcDays={calcDays} calcFirst={calcFirst} calcLast={calcLast}"); + SwapCalcTrace.Critical($" preEod.id={preEodPosition.id} ValueDate={preEodPosition.ValueDate:yyyy-MM-dd} TdInterestPrincipal={preEodPosition.TdInterestPrincipal:F4} InterestProfitSum={interestProfitSum:F4}"); + SwapCalcTrace.Critical($" dynomicPrincipal = TdInterestPrincipal({preEodPosition.TdInterestPrincipal:F4}) + posiPrincipal({posiPrincipal:F4}) - orginPv({orginPv:F4}) = {dynomicPrincipal:F4}"); + SwapCalcTrace.Critical($" 已结扣除(InterestProfitSum*closePercent)={interest:F4} consumedInterest={consumedInterest:F4} closePercent={closePercent}"); + SwapCalcTrace.Critical($" 逐日累加上限: accrueDate > preEod.ValueDate({preEodPosition.ValueDate:yyyy-MM-dd}) 才计息"); double floatRate = Convert.ToDouble(floateRate); for (int i = 0; i <= calcDays; i++) { @@ -1409,8 +1418,7 @@ namespace YLErp.Modules.SwapModule } interest += interest1; tdinterest += tdinterest1; - if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled) - SwapCalcTrace.Day(i, accrueDate, (decimal)floatRate, flowEvent.InterestPrincipal, interest1, interest); + SwapCalcTrace.Day(i, accrueDate, (decimal)floatRate, flowEvent.InterestPrincipal, interest1, interest); } } InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero); @@ -1438,9 +1446,12 @@ namespace YLErp.Modules.SwapModule decimal tdDynomicPrincipal = posiPrincipal; double floatRate = Convert.ToDouble(floateRate); var days = (endDate - tradeDate).Days; + SwapCalcTrace.Critical($"CalcDailyCompoundInterestByEod(收盘复利) posId={position.id}"); + SwapCalcTrace.Critical($" preEod.id={preEodPosition.id} ValueDate={preEodPosition.ValueDate:yyyy-MM-dd} TdInterestPrincipal={preEodPosition.TdInterestPrincipal:F4} InterestProfitSum={preEodPosition.InterestProfitSum:F4}"); + SwapCalcTrace.Critical($" tradeDate={tradeDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} days={days} interest_rest_days={interestPeriod} closePercent={closePercent} orginPv={orginPv:F4} principal={principal:F4} posiPrincipal={posiPrincipal:F4}"); + SwapCalcTrace.Critical($" {(days % interestPeriod == 0 ? "重置日分支: 利息并入本金" : "非重置日分支: base=TdInterestPrincipal+posiPrincipal-orginPv")}"); if (days % interestPeriod == 0) { - LogFactory.GetLogger("test").Error("kluausdyfh"); var remainingPercent = posiPrincipal > 0m ? principal / posiPrincipal : 1m; @@ -1531,6 +1542,12 @@ namespace YLErp.Modules.SwapModule flowEvent.FloatRate = Convert.ToDecimal(floatRate); var baseTdInterestPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; var baseInterestPrincipal = baseTdInterestPrincipal * closePercent; + SwapCalcTrace.Critical($"CalcDailySimpleInterestByEod(收盘单利) posId={position.id}"); + SwapCalcTrace.Critical($" preEod.id={preEodPosition.id} ValueDate={preEodPosition.ValueDate:yyyy-MM-dd} TdInterestPrincipal={preEodPosition.TdInterestPrincipal:F4} InterestProfitSum={preEodPosition.InterestProfitSum:F4}"); + SwapCalcTrace.Critical($" tradeDate={tradeDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} calcDays={calcDays} closePercent={closePercent} orginPv={orginPv:F4} posiPrincipal={posiPrincipal:F4}"); + SwapCalcTrace.Critical($" baseTdInterestPrincipal = TdInterestPrincipal({preEodPosition.TdInterestPrincipal:F4}) + posiPrincipal({posiPrincipal:F4}) - orginPv({orginPv:F4}) = {baseTdInterestPrincipal:F4}"); + SwapCalcTrace.Critical($" baseInterestPrincipal(=base*closePercent)={baseInterestPrincipal:F4} floatRate={floatRate} rate={flowEvent.InterestRate} annualized={position.IsAnnualized}"); + SwapCalcTrace.Critical($" 输出 InterestAmount = InterestProfitSum({preEodPosition.InterestProfitSum:F4}) + interest"); // 修复:正确计算本次利息(基于实际持仓本金) decimal interest = baseInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));