From 7243684839af1a6bf5dc1925388a2764a4d84864 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 13 Aug 2026 09:59:48 +0800 Subject: [PATCH] =?UTF-8?q?test(margin):=20=E6=96=B0=E5=A2=9E=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=BA=93=E9=BB=84=E9=87=91=E5=9B=9E=E6=94=BE=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E2=80=94=E2=80=9496=E5=BA=9360=E6=9D=A1EOD=E9=80=90?= =?UTF-8?q?=E6=97=A5=E6=AF=94=E5=AF=B9=EF=BC=8C=E6=96=B0=E6=97=A7=E7=AE=A1?= =?UTF-8?q?=E7=BA=BF0=E5=B7=AE=E5=BC=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MarginInterestGoldenReplayTest:连 192.168.2.96 测试库,对真实保证金交易(GLMS-20260701-0008/-0013/-0006)逐日 EOD 比对旧管线 GetInterests(settment=true) vs 新方法 CalcMarginInterest - 结果:60 条腿×日比对,InterestAmount/TdInterestAmount 严格一致(容差1e-6),0 不一致 - 覆盖真实边界:追加预付金多行(base 135555/200000 各自计息)、InterestProfitSum 累计滚动、跨 EOD 续接 - 为提交2(切换生产+删 orginPv hack)建立真实数据安全网 --- .../Margin/MarginInterestGoldenReplayTest.cs | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs new file mode 100644 index 00000000..c9c226d3 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs @@ -0,0 +1,165 @@ +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 → CalcEodInterest → CalcDailySimpleInterestByEod → AccrueEod) + /// vs 新方法 CalcMarginInterest(settment=true),验证消除 orginPv/差分后数值仍与旧管线一致。 + /// + /// 这是 MarginInterestShadowTest(合成数据)的真实库版本:数据来自 96 库的真实保证金交易, + /// 覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、跨 EOD 续接等单元测试够不到的边界。 + /// 通过则可安全进入提交2(切换生产 + 删 orginPv hack)。 + /// + [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逐日新旧比对() + { + 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, 0m, 0m, 1.0m, + (int)SwapEventTypeEnum.自动互换, tdClose: false, needPrice: false, + grossPrice: 0m, 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, 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 前必须解决(详见输出)"); + } + } +}