using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// GLMS-20260105-0007 分红精度差异 - golden 录制/回放测试 /// ============================================================================ /// 用真实测试库数据录制 → golden JSON → 回放复现 0.36 差异 /// /// 问题:原有待实现分红-90400,互换支付-400后应为-90000,实际-89999.64 /// 根因:UpdateEodPosition cs:1635 从头重算 PosiDividendSum, /// 与 CopyEodPosition 逐天递增的舍入累积不一致 /// ============================================================================ [TestClass] public class GLMS20260105GoldenTest { private const string TradeNumber = "GLMS-20260105-0007"; private static readonly string GoldenDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "GLMS20260105"); #region 录制:从真实库读取数据,序列化为 golden /// /// 从真实测试库录制 GLMS-20260105-0007 的完整数据快照。 /// 标 [Ignore],手动跑一次生成 golden JSON。 /// [TestMethod] [Ignore] [TestCategory("GoldenRecord")] public void Record_FromRealDB() { YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } try { Directory.CreateDirectory(GoldenDir); var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); Assert.IsNotNull(td, $"交易 {TradeNumber} 不存在"); var floatEods = db.eod_swap_position .Where(x => x.SwapTradeId == td.id && x.PosiDirection > 0 && !x.Invalid) .OrderBy(x => x.ValueDate).ToList(); var flows = db.swap_flow_event .Where(x => x.SwapTradeId == td.id && x.DataState == (int)SwapFlowDateStateEnum.完成) .OrderBy(x => x.EventDate).ThenBy(x => x.id).ToList(); var positions = db.swap_position .Where(x => x.SwapTradeId == td.id && !x.Invalid).ToList(); var swapDate = flows.First(f => f.EventType == (int)SwapFlowEventTypeEnum.互换).EventDate; var keyDates = new[] { swapDate.AddDays(-1), swapDate, swapDate.AddDays(1) }; var keyFloatEods = floatEods.Where(x => keyDates.Contains(x.ValueDate)).ToList(); // 录制 UpdateEodPosition 的输入(互换前日eod + 互换flow_event + 持仓 + 交易) var preSwapEod = floatEods.FirstOrDefault(x => x.ValueDate == swapDate.AddDays(-1)); var swapFlowEvents = flows.Where(x => x.EventDate == swapDate && x.PositionId == preSwapEod?.PositionId).ToList(); var swapPosition = positions.FirstOrDefault(x => x.id == preSwapEod?.PositionId); var golden = new JObject { ["TradeNumber"] = TradeNumber, ["TradeId"] = td.id, ["SwapDate"] = swapDate.ToString("yyyy-MM-dd"), ["Description"] = "分红精度差异:互换前-90400,互换后应为-90000,实际-89999.64" }; // 关键3天的浮动腿eod(含精确字段值) var keyArray = new JArray(); foreach (var e in keyFloatEods) { keyArray.Add(new JObject { ["ValueDate"] = e.ValueDate.ToString("yyyy-MM-dd"), ["PositionId"] = e.PositionId, ["PosiDividendSum"] = e.PosiDividendSum, ["TdPosiDividend"] = e.TdPosiDividend, ["TdCloseDividend"] = e.TdCloseDividend, ["RealizedDividend"] = e.RealizedDividend, ["PosiQuantity"] = e.PosiQuantity, ["PosiMtmPnL"] = e.PosiMtmPnL }); } golden["KeyFloatEodPositions"] = keyArray; // 完整浮动腿序列(用于分析精度累积过程) var allArray = new JArray(); foreach (var e in floatEods) { allArray.Add(new JObject { ["ValueDate"] = e.ValueDate.ToString("yyyy-MM-dd"), ["PosiDividendSum"] = e.PosiDividendSum, ["TdPosiDividend"] = e.TdPosiDividend, ["RealizedDividend"] = e.RealizedDividend }); } golden["AllFloatEodDividends"] = allArray; // 录制回放所需的输入数据(用于重新调 UpdateEodPosition) if (preSwapEod != null && swapPosition != null) { var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; golden["ReplayInput"] = new JObject { ["Trade"] = JObject.FromObject(td, JsonSerializer.Create(settings)), ["Position"] = JObject.FromObject(swapPosition, JsonSerializer.Create(settings)), ["PreSwapEod"] = JObject.FromObject(preSwapEod, JsonSerializer.Create(settings)), ["SwapFlowEvents"] = JArray.FromObject(swapFlowEvents, JsonSerializer.Create(settings)) }; } string json = JsonConvert.SerializeObject(golden, Formatting.Indented); string path = Path.Combine(GoldenDir, $"golden_{TradeNumber}.json"); File.WriteAllText(path, json); Console.WriteLine($"录制完成: {path}"); Console.WriteLine($"\n关键数据:"); foreach (var e in keyFloatEods) { Console.WriteLine($" {e.ValueDate:yyyy-MM-dd}: PosiDividendSum={e.PosiDividendSum}, TdPosiDividend={e.TdPosiDividend}, TdCloseDividend={e.TdCloseDividend}, RealizedDividend={e.RealizedDividend}"); } var preSwap = keyFloatEods.FirstOrDefault(x => x.ValueDate == swapDate.AddDays(-1)); var swapDay = keyFloatEods.FirstOrDefault(x => x.ValueDate == swapDate); if (preSwap != null && swapDay != null) { decimal expected = preSwap.PosiDividendSum - swapDay.TdCloseDividend; decimal actual = swapDay.PosiDividendSum; Console.WriteLine($"\n精度分析:"); Console.WriteLine($" 互换前 PosiDividendSum = {preSwap.PosiDividendSum}"); Console.WriteLine($" 互换实现 TdCloseDividend = {swapDay.TdCloseDividend}"); Console.WriteLine($" 期望 PosiDividendSum = {preSwap.PosiDividendSum} - ({swapDay.TdCloseDividend}) = {expected}"); Console.WriteLine($" 实际 PosiDividendSum = {actual}"); Console.WriteLine($" 差异 = {actual - expected}"); } } finally { db?.Dispose(); } } #endregion #region 回放:读 golden 验证精度差异 /// /// 回放 golden:用真实数据重新调 UpdateEodPosition,验证修复后 PosiDividendSum 正确。 /// /// 红灯(修复前):从头重算产生 0.36 差异 /// 绿灯(修复后):递增模式,PosiDividendSum = 前日 + 新计 - 实现 = -90000 /// [TestMethod] public void Replay_VerifyPrecisionDiff() { string sourceDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "GLMS20260105"); if (!Directory.Exists(sourceDir)) { Assert.Inconclusive($"golden 目录不存在: {sourceDir}(请先跑 Record_FromRealDB)"); return; } var files = Directory.GetFiles(sourceDir, "*.json"); Assert.IsTrue(files.Length > 0, "应至少有1个golden文件"); var json = File.ReadAllText(files[0]); var golden = JObject.Parse(json); var replayInput = golden["ReplayInput"]; if (replayInput == null) { Assert.Inconclusive("golden 缺少 ReplayInput(请重新录制)"); return; } var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; var td = replayInput["Trade"]!.ToObject(JsonSerializer.Create(settings)); var position = replayInput["Position"]!.ToObject(JsonSerializer.Create(settings)); var preSwapEod = replayInput["PreSwapEod"]!.ToObject(JsonSerializer.Create(settings)); var swapFlowEvents = replayInput["SwapFlowEvents"]!.ToObject>(JsonSerializer.Create(settings)); var swapDateStr = golden["SwapDate"]!.Value(); var swapDate = DateTime.Parse(swapDateStr); Console.WriteLine($"SwapDate = {swapDateStr}"); Console.WriteLine($"互换前 PosiDividendSum = {preSwapEod.PosiDividendSum}"); Console.WriteLine($"互换 DividendIn = {string.Join(",", swapFlowEvents.Select(x => x.DividendIn))}"); // 用修复后的代码重新调 UpdateEodPosition var service = new ReplayStubService(preSwapEod.UnderlyingCode); var result = service.ExecuteUpdateEodPosition( position, preSwapEod, td, swapDate, swapDate.AddDays(-1), swapFlowEvents); // 期望:PosiDividendSum = 前日 + 当天新计 - 实现 decimal expected = preSwapEod.PosiDividendSum + result.TdPosiDividend - result.TdCloseDividend; Console.WriteLine($"\n修复后结果:"); Console.WriteLine($" TdPosiDividend = {result.TdPosiDividend}"); Console.WriteLine($" TdCloseDividend = {result.TdCloseDividend}"); Console.WriteLine($" PosiDividendSum = {result.PosiDividendSum}"); Console.WriteLine($" 期望 = {preSwapEod.PosiDividendSum} + {result.TdPosiDividend} - ({result.TdCloseDividend}) = {expected}"); Assert.AreEqual(expected, result.PosiDividendSum, $"修复后 PosiDividendSum 应=前日+新计-实现={expected},实际={result.PosiDividendSum}"); Console.WriteLine($"\n✅ 修复验证通过:PosiDividendSum={result.PosiDividendSum} = {expected}"); } #endregion #region 回放用 Stub private sealed class ReplayStubService : SwapEodPositionService { private readonly string _underlyingCode; public ReplayStubService(string underlyingCode) : base(new OptUserInfo(0, "Replay", OptUserFrom.UnitTest)) { _underlyingCode = underlyingCode; } protected override underlying_manager GetUnderlyingData(string underlyingCode) { // 返回最小可用数据(增值税=0) return new underlying_manager { ValueAddedTax = 0m }; } protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) { vobp = 0m; return 1.01m; // 固定价格 } protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) { // 返回0:互换日的 TdPosiDividend=0(无新增分红),聚焦验证 PosiDividendSum 的递增逻辑 return 0m; } protected override void SaveAllChanges() { } protected override double GetCurrencyRate(string q, string s, DateTime d, bool p, CurrencyRateType t) => 1.0; public eod_swap_position ExecuteUpdateEodPosition( swap_position swapPosition, eod_swap_position eod, trade td, DateTime valueDate, DateTime preSettleDate, List unwindEvents) { return UpdateEodPosition(swapPosition, eod, null, td, valueDate, preSettleDate, unwindEvents); } } #endregion } }