using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// DealInterests Golden 回放测试 /// ============================================================================ /// 用 golden JSON 存"输入数据 + 期望输出的精确字段值", /// 回放时从 JSON 重跑,逐字段精确对比。 /// /// 两类方法: /// - Record*: 连库录制/生成 golden(标 Ignore,手动跑) /// - Replay*: 读 golden 重跑对比(进 CI) /// /// 价值:重构时如果任何一步的输出变了(哪怕第8位小数),立刻失败。 /// 守恒测试验证"大方向对",golden 验证"精确值对"。 /// ============================================================================ [TestClass] public class DealInterestsGoldenReplayTest { private static readonly string GoldenDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "DealInterestsGolden"); #region Stub(复用 DealInterestsScenarioTest 的模式) private sealed class StubEodService : TestableSwapEodPositionService { public StubEodService() : base(nameof(DealInterestsGoldenReplayTest)) { } public void ExecuteSaveEodInterestPosition( eod_swap_position eodPayPosition, swap_position position, trade td, DateTime valueDate, List flowEvents) { SaveEodInterestPosition(eodPayPosition, null, position, td, valueDate, flowEvents); } // public 包装:直接调用 protected virtual DealInterests(录制场景2用) public void ExecuteDealInterestsForRecord( List interestList, List eodPositions, DateTime settleDate, trade td, decimal posiLongNational, decimal grossPrice, decimal orginPv) { DealInterests(interestList, eodPositions, new List(), settleDate, td, new List(), new List(), null, posiLongNational, 0m, 0m, grossPrice, orginPv); } } #endregion #region 录制:生成 golden JSON(标 Ignore,手动跑) /// /// 生成所有 golden JSON 文件。 /// 手动取消 [Ignore] 运行,会覆盖 bin 目录下的 golden 文件。 /// 生成后复制到 UnitTestProject/Resources/GoldenFiles/ 持久化。 /// [TestMethod] [Ignore] [TestCategory("GoldenRecord")] public void Record_AllGoldenScenarios() { Directory.CreateDirectory(GoldenDir); Record_SwapSettleZeroInterestIncomeSum(); Record_NormalDayIncrement(); Console.WriteLine($"\n录制完成,输出目录: {GoldenDir}"); } /// 场景1:互换结清后 InterestIncomeSum≈当天新计 private void Record_SwapSettleZeroInterestIncomeSum() { const decimal Principal = 10000m; const decimal Rate = 0.03m; const int AnnualDays = 365; var startDate = new DateTime(2026, 4, 27); var td = new trade { id = 1, TradeNumber = "GOLDEN-001", ClientId = 999998, TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } }; var position = new swap_position { id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, InterestPrincipalFix = Principal, PosiStartDate = startDate, PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null }; var settleDate = startDate.AddDays(10); int days = (settleDate - startDate).Days; decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); var preEod = new eod_swap_position { id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestIncomeSum = accumulated, InterestProfitSum = accumulated, InterestRateDefault = Rate, TdInterestPrincipal = Principal, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 }; var swapEvent = new swap_flow_event { EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001, InterestAmount = accumulated, InterestClosePnL = accumulated, InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价, InterestPrincipal = Principal, FloatRate = 0m, DataState = (int)SwapFlowDateStateEnum.完成 }; var service = new StubEodService(); service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List { swapEvent }); var result = service.PersistedPositions[0]; var golden = new GoldenScenarioModel { Scenario = "互换结清后待实现归零", Description = $"攒{days}天后互换,InterestIncomeSum应≈当天新计", Input = new GoldenInput { SettleDate = settleDate, PosiLongNotional = Principal, OrginPv = Principal }, Expected = new GoldenExpected { PositionCount = 1, EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) } } }; string json = JsonConvert.SerializeObject(golden, Formatting.Indented); string path = Path.Combine(GoldenDir, "golden_互换结清后待实现归零.json"); File.WriteAllText(path, json); Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}"); Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}"); Console.WriteLine($" TdCloseInterest={result.TdCloseInterest:F11}"); Console.WriteLine($" RealizedInterest={result.RealizedInterest:F11}"); } /// 场景2:普通日 InterestIncomeSum 递增 private void Record_NormalDayIncrement() { const decimal Principal = 10000m; const decimal Rate = 0.03m; const int AnnualDays = 365; var startDate = new DateTime(2026, 4, 27); decimal dailyInc = Math.Round(Principal * Rate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); var td = new trade { id = 1, TradeNumber = "GOLDEN-002", ClientId = 999998, TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } }; var position = new swap_position { id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, InterestPrincipalFix = Principal, PosiStartDate = startDate, PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, FloatRateUnderlyingCode = null, InterestSwapInterval = null }; // 用 DealInterests 走 copy 分支 var settleDate = startDate.AddDays(2); // 第3天 var preEod = new eod_swap_position { id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestIncomeSum = dailyInc, InterestProfitSum = dailyInc, InterestRateDefault = Rate, TdInterestPrincipal = Principal, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 }; var service = new StubEodService(); // 直接调用 protected virtual DealInterests(copy 分支需要 CalcSwapInterests) service.ExecuteDealInterestsForRecord( new List { position }, new List { preEod }, settleDate, td, Principal, 1m, Principal); if (service.PersistedPositions.Count == 0) { Console.WriteLine("⚠ 场景2未生成eod(CalcSwapInterests可能需要接缝),跳过"); return; } var result = service.PersistedPositions[0]; var golden = new GoldenScenarioModel { Scenario = "普通日归档递增", Description = "第3天收盘,InterestIncomeSum应=2天+1天=3天利息", Expected = new GoldenExpected { PositionCount = 1, EodPositions = new JArray { GoldenAssert.EodPositionToJson(result) } } }; string json = JsonConvert.SerializeObject(golden, Formatting.Indented); string path = Path.Combine(GoldenDir, "golden_普通日归档递增.json"); File.WriteAllText(path, json); Console.WriteLine($"✅ 录制: {Path.GetFileName(path)}"); Console.WriteLine($" InterestIncomeSum={result.InterestIncomeSum:F11}"); } #endregion #region 回放:读 golden 重跑+精确对比(进 CI) /// /// 回放所有 golden 文件,逐字段精确对比。 /// 如果任何字段变了(哪怕是第8位小数),测试失败。 /// [TestMethod] public void Replay_AllGoldenFiles() { if (!Directory.Exists(GoldenDir)) { Assert.Inconclusive($"golden 目录不存在: {GoldenDir}(请先跑 Record_AllGoldenScenarios)"); return; } var files = Directory.GetFiles(GoldenDir, "*.json").OrderBy(f => f).ToArray(); Assert.IsTrue(files.Length > 0, "应至少有1个golden文件"); int passed = 0, failed = 0; foreach (var file in files) { try { var golden = JsonConvert.DeserializeObject(File.ReadAllText(file)); Console.WriteLine($"\n回放: {Path.GetFileName(file)} - {golden.Scenario}"); // 回放互换场景(场景1的模式) if (golden.Scenario?.Contains("互换结清") == true) { ReplaySwapSettle(golden); } else { Console.WriteLine($" (场景类型'{golden.Scenario}'暂不支持自动回放,跳过)"); continue; } passed++; Console.WriteLine($" ✅ 通过"); } catch (Exception ex) { failed++; Console.WriteLine($" ❌ 失败: {ex.Message}"); } } Console.WriteLine($"\n回放结果: {passed}通过 {failed}失败 / {files.Length}总"); Assert.AreEqual(0, failed, $"{failed}个golden文件回放失败"); } private void ReplaySwapSettle(GoldenScenarioModel golden) { const decimal Principal = 10000m; const decimal Rate = 0.03m; const int AnnualDays = 365; var startDate = new DateTime(2026, 4, 27); var settleDate = golden.Input.SettleDate ?? startDate.AddDays(10); int days = (settleDate - startDate).Days; decimal accumulated = Math.Round(Principal * Rate * days / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); var td = new trade { id = 1, TradeNumber = "GOLDEN-REPLAY", ClientId = 999998, TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } }; var position = new swap_position { id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, InterestPrincipalFix = Principal, PosiStartDate = startDate, PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1, interest_rule = 0 }; var preEod = new eod_swap_position { id = 100, PositionId = 1001, ValueDate = settleDate.AddDays(-1), InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestIncomeSum = accumulated, InterestProfitSum = accumulated, InterestRateDefault = Rate, TdInterestPrincipal = Principal, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 }; var swapEvent = new swap_flow_event { EventType = (int)SwapFlowEventTypeEnum.互换, PositionId = 1001, InterestAmount = accumulated, InterestClosePnL = accumulated, InterestRate = Rate, InterestMode = (int)InterestModeEnum.标的期初全价, InterestPrincipal = Principal, DataState = (int)SwapFlowDateStateEnum.完成 }; var service = new StubEodService(); service.ExecuteSaveEodInterestPosition(preEod, position, td, settleDate, new List { swapEvent }); // 对比 golden 期望 Assert.AreEqual(golden.Expected.PositionCount ?? 1, service.PersistedPositions.Count, "持仓数量"); var expectedEods = golden.Expected.EodPositions?.ToObject>() ?? new List(); foreach (var expected in expectedEods) { var pid = expected["PositionId"]?.Value() ?? 1001; var actual = service.PersistedPositions.FirstOrDefault(x => x.PositionId == pid); Assert.IsNotNull(actual, $"未找到PositionId={pid}"); GoldenAssert.AssertEodPosition(expected, actual); } } #endregion } }