using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using YLErp; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Margin; namespace UnitTestProject.Modules.SwapModule.Margin { /// /// 黄金回放验证(连真实测试库 192.168.2.96):对真实保证金交易逐日 EOD 比对 /// GetInterests(settment=true,保证金分支现走 CalcMarginInterest) vs 直接调 CalcMarginInterest, /// 验证 GetInterests→CalcMarginInterest 接线的参数对齐(rate/posiPrincipal/preEod 等)正确。 /// /// 数据来自 96 库的真实保证金交易,覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、 /// 跨 EOD 续接等单元测试够不到的边界。作为保证金计息迁移后的真实库回归守护。 /// [TestClass] public class MarginInterestGoldenReplayTest { // 96 库里已确认含真实保证金腿的交易 private static readonly string[] TradeNumbers = { "GLMS-20260701-0008", "GLMS-20260701-0013", "GLMS-20260701-0006", }; private sealed class StubSvc : SwapDealService { public StubSvc() : base(new OptUserInfo(0, nameof(MarginInterestGoldenReplayTest), OptUserFrom.UnitTest)) { } } /// /// 逐交易、逐 EOD 日,比对保证金腿新旧计息 InterestAmount/TdInterestAmount。 /// 入参对齐口径(与 GetInterests 内部一致): /// posiPrincipal = InterestPrincipalFix;closePrincipal = Fix×closePercent(EOD=1); /// rate = oldEvt.InterestRate(严格取旧管线算出的 rate,消除 GetFixedRate 差异); /// 方向 = FlipDirection(position.InterestDirection)(GetInterests:742 对保证金翻转); /// preEod = 该 PositionId 上一日终 eod_swap_position;annualDays/calcFirst/calcLast 来自 trade_extend。 /// [TestMethod] [TestCategory("DbDiagnose")] public void 保证金腿_真实库_EOD逐日新旧比对() { DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(192.168.2.96):{ex.Message}"); return; } var svc = new StubSvc(); int totalCompared = 0, mismatches = 0, skipped = 0; var diffLog = new StringBuilder(); foreach (var tradeNumber in TradeNumbers) { var td = db.trade.FirstOrDefault(t => t.TradeNumber == tradeNumber); if (td == null) { Console.WriteLine($"跳过:库无 {tradeNumber}"); skipped++; continue; } var extend = db.trade_extend.FirstOrDefault(x => x.TradeId == td.id); int annualDays = extend?.ExtendObj.AnnualDays ?? 365; bool calcFirst = extend?.ExtendObj.InterestCalcMode?.StartsWith("1") ?? true; bool calcLast = extend?.ExtendObj.InterestCalcMode?.EndsWith("1") ?? true; var marginPositions = db.swap_position .Where(p => p.SwapTradeId == td.id && !p.Invalid && (p.InterestMode == (int)InterestModeEnum.初始预付金 || p.InterestMode == (int)InterestModeEnum.追加预付金)) .ToList(); if (marginPositions.Count == 0) { Console.WriteLine($"跳过:{tradeNumber} 无保证金腿"); skipped++; continue; } // 该交易保证金腿的 EOD 日期序列 var eodDates = db.eod_swap_position .Where(e => e.SwapTradeId == td.id && (e.InterestMode == 5 || e.InterestMode == 6)) .Select(e => e.ValueDate).Distinct().OrderBy(d => d).ToList(); Console.WriteLine($"===== {tradeNumber} (id={td.id}):{marginPositions.Count} 条保证金腿,{eodDates.Count} 个 EOD 日 ====="); foreach (var valueDate in eodDates) { // 上一日终 preEod(取 eod_swap 最近 < valueDate 的日期) var preDate = db.eod_swap .Where(e => e.SwapTradeId == td.id && e.ValueDate < valueDate) .OrderByDescending(e => e.ValueDate) .Select(e => (DateTime?)e.ValueDate).FirstOrDefault(); var preEods = preDate == null ? new List() : db.eod_swap_position .Where(e => e.SwapTradeId == td.id && e.ValueDate == preDate.Value && (e.InterestMode == 5 || e.InterestMode == 6)) .ToList(); // 旧管线:GetInterests(settment=true)。保证金分支不用 posiNotionalValue/closePosiNotionalValue/grossPrice/orginPv,传 0。 List oldList; try { oldList = svc.GetInterests(td, extend, valueDate, valueDate, preEods, marginPositions, 0m, 0m, 1.0m, (int)SwapEventTypeEnum.自动互换, tdClose: false, orginPv: 0m, add: false, settment: true, newCalcLast: false, closeList: null); } catch (Exception ex) { Console.WriteLine($" {tradeNumber} @ {valueDate:yyyy-MM-dd} 旧管线异常:{ex.GetType().Name} {ex.Message}"); continue; } // 新方法:逐保证金腿 foreach (var pos in marginPositions) { var oldEvt = oldList.FirstOrDefault(i => i.PositionId == pos.id); if (oldEvt == null) continue; var preEod = preEods.FirstOrDefault(e => e.PositionId == pos.id) ?? new eod_swap_position { id = 0 }; var posClone = pos.Clone(); posClone.InterestDirection = MarginCalc.FlipDirection(pos.InterestDirection); decimal rate = oldEvt.InterestRate; // 严格对齐旧管线 rate(含 GetFixedRate + Round(12)) swap_flow_event newEvt; try { newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, posClone, rate, pos.InterestPrincipalFix, pos.InterestPrincipalFix, 1.0m, annualDays, calcFirst, calcLast, preEod, (int)SwapEventTypeEnum.自动互换, add: false, settment: true, swap: false); } catch (Exception ex) { mismatches++; diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} @ {valueDate:yyyy-MM-dd} 新方法异常:{ex.GetType().Name} {ex.Message}"); continue; } totalCompared++; decimal diffI = Math.Abs(newEvt.InterestAmount - oldEvt.InterestAmount); decimal diffTd = Math.Abs(newEvt.TdInterestAmount - oldEvt.TdInterestAmount); const decimal tol = 0.000001m; if (diffI > tol || diffTd > tol) { mismatches++; diffLog.AppendLine($"✗ {tradeNumber} PosId={pos.id} Mode={pos.InterestMode} @ {valueDate:yyyy-MM-dd}: " + $"旧 I={oldEvt.InterestAmount} Td={oldEvt.TdInterestAmount} | " + $"新 I={newEvt.InterestAmount} Td={newEvt.TdInterestAmount} | " + $"diffI={diffI} diffTd={diffTd} | " + $"preEod.id={preEod.id} TdIntPrin={preEod.TdInterestPrincipal} ProfitSum={preEod.InterestProfitSum} | " + $"Fix={pos.InterestPrincipalFix} rate={rate} IntType={pos.InterestType} IsAnnualized={pos.IsAnnualized}"); } } } } Console.WriteLine($"\n===== 比对汇总:共 {totalCompared} 条,不一致 {mismatches} 条,跳过 {skipped} 个交易 ====="); if (diffLog.Length > 0) Console.WriteLine(diffLog.ToString()); Assert.IsTrue(mismatches == 0, $"保证金新旧管线 EOD 真实库比对有 {mismatches}/{totalCompared} 条不一致——提交2 前必须解决(详见输出)"); } } }