using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// 互换部分平仓后利息端/预付金默认盈亏偏大 - 录制/验证测试(TDD 红灯) /// ============================================================================ /// 背景: /// 昨天收益结算(互换)→收盘→今天平仓,"预付金平仓盈亏"和"利息端平仓盈亏" /// 默认值偏大。根因:CalcDailySimpleInterest(cs:771) 从 PosiStartDate 全程重算利息, /// 只读 InterestProfitSum(待实现),不读 RealizedInterest(已实现),导致跨天重复计入。 /// 同日去重(cs:435) 只覆盖当天、算尾跳过,跨天不生效。 /// /// TDD 红灯→绿灯: /// 红灯(当前):找一笔多次操作的交易 → 模拟默认值计算 → 断言默认值 > 应计基数(待实现-已实现) /// 绿灯(修复后):默认值 ≤ 应计基数 /// /// 运行方式:全部 [Ignore]+[TestCategory("DBRecording")],不进 CI。 /// ============================================================================ [TestClass] public class SwapPartialUnwindInterestDefaultTest { private static readonly string GoldenDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "SwapPartialUnwindInterest"); private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Include, DateFormatString = "yyyy-MM-ddTHH:mm:ss", ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; /// /// Step0:探查测试库,列出有"多次平仓/互换操作"的互换交易,供挑选样本。 /// /// 复现条件:一笔交易 swap_flow_event 里 EventType IN(平仓,互换,自动互换) 且 DataState=完成 /// 的记录 ≥ 2 条(说明做过多次操作),且有 eod_swap_position(已收盘)。 /// [TestMethod] [TestCategory("DBRecording")] public void Step0_ListMultiOperationTrades() { YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { // 找有多次操作的交易 var multiOpTrades = db.swap_flow_event .Where(x => (x.EventType == (int)SwapFlowEventTypeEnum.平仓 || x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换) && x.DataState == (int)SwapFlowDateStateEnum.完成) .AsEnumerable() .GroupBy(x => x.SwapTradeId) .Where(g => g.Count() >= 2) .Select(g => new { SwapTradeId = g.Key, 操作次数 = g.Count(), 平仓次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓), 互换次数 = g.Count(x => x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换), 最早操作日 = g.Min(x => x.EventDate), 最晚操作日 = g.Max(x => x.EventDate), 利息盈亏合计 = g.Sum(x => x.InterestClosePnL), EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key) }) .Where(t => t.EodCount > 0) .OrderByDescending(t => t.操作次数) .Take(30) .ToList(); Console.WriteLine($"=== 多次操作的互换交易数: {multiOpTrades.Count} ===\n"); Console.WriteLine($"{"TradeId",8} {"操作",6} {"平仓",6} {"互换",6} {"eod",6} {"利息盈亏合计",16} {"操作日期范围",-24}"); foreach (var t in multiOpTrades) { string dateRange = $"{t.最早操作日:yyyy-MM-dd}~{t.最晚操作日:yyyy-MM-dd}"; Console.WriteLine($"{t.SwapTradeId,8} {t.操作次数,6} {t.平仓次数,6} {t.互换次数,6} {t.EodCount,6} {t.利息盈亏合计,16:F2} {dateRange,-24}"); } if (multiOpTrades.Count == 0) { Assert.Inconclusive("无多次操作的样本(需有≥2次平仓/互换且有eod的交易)。"); } Assert.IsTrue(multiOpTrades.Count > 0); } finally { db?.Dispose(); } } /// /// Step0e:探查"单利 + 有互换历史"的样本,用于验证单利路径是否也需要 consumedInterest 扣除。 /// /// 复利路径(c6adb3bb)已修,单利路径(CalcDailySimpleInterest)未修。 /// 需找:单利利息腿 + 该腿有历史互换/自动互换事件(InterestAmount≠0) + 有eod。 /// [TestMethod] [TestCategory("DBRecording")] public void Step0e_ListSimpleInterestSwapTrades() { YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { // 找单利利息腿(InterestType=0=单利)且有历史互换事件的交易 var simplePositions = db.swap_position .Where(x => !x.Invalid && x.InterestDirection > 0 && x.InterestType == (int)InterestTypeEnum.单利) .Select(x => new { x.SwapTradeId, x.id, x.InterestMode, x.InterestPrincipalFix }) .ToList(); Console.WriteLine($"=== 单利利息腿持仓: {simplePositions.Count} 条 ===\n"); // 关联历史互换事件(InterestAmount≠0 说明有实际利息结算) var tradeIds = simplePositions.Select(x => x.SwapTradeId).Distinct().ToList(); var swapEvents = db.swap_flow_event .Where(x => tradeIds.Contains(x.SwapTradeId) && (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换) && x.DataState == (int)SwapFlowDateStateEnum.完成 && x.InterestAmount != 0) .ToList(); var byTrade = simplePositions .Where(p => swapEvents.Any(s => s.SwapTradeId == p.SwapTradeId && s.PositionId == p.id)) .GroupBy(p => p.SwapTradeId) .Select(g => new { SwapTradeId = g.Key, 单利腿数 = g.Count(), 利息模式 = string.Join("|", g.Select(x => ((InterestModeEnum)x.InterestMode).ToString())), 历史互换事件数 = swapEvents.Count(s => s.SwapTradeId == g.Key), 历史利息合计 = swapEvents.Where(s => s.SwapTradeId == g.Key).Sum(s => s.InterestAmount), EodCount = db.eod_swap_position.Count(e => e.SwapTradeId == g.Key) }) .Where(t => t.EodCount > 0) .OrderByDescending(t => Math.Abs(t.历史利息合计)) .Take(20) .ToList(); Console.WriteLine($"{"TradeId",8} {"单利腿",6} {"历史互换",8} {"历史利息合计",16} {"eod",6} {"利息模式",-20}"); foreach (var t in byTrade) { Console.WriteLine($"{t.SwapTradeId,8} {t.单利腿数,6} {t.历史互换事件数,8} {t.历史利息合计,16:F4} {t.EodCount,6} {t.利息模式,-20}"); } if (byTrade.Count == 0) { Assert.Inconclusive("无单利+有互换历史的样本。"); } Assert.IsTrue(byTrade.Count > 0); } finally { db?.Dispose(); } } /// /// Step1_SimpleInterestRedTest:单利路径红灯测试。 /// /// 复利路径已由 c6adb3bb 修复(consumedInterest 扣除),但单利路径(CalcDailySimpleInterest) /// 未加该扣除。本测试坐实:单利利息腿在"有历史互换结清后再平仓"时,默认值仍偏大。 /// /// 红灯(当前):默认值包含历史已结利息(consumedInterest),偏大 /// 绿灯(修复后):单利路径也扣除 consumedInterest,默认值正确 /// [TestMethod] [TestCategory("DBRecording")] public void Step1_SimpleInterestRedTest() { int tradeId = SimpleInterestSampleTradeId; YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { Console.WriteLine($"===== 单利路径红灯测试 SwapTradeId={tradeId} =====\n"); // 1. 确认该交易的单利利息腿 var simplePositions = db.swap_position .Where(x => x.SwapTradeId == tradeId && !x.Invalid && x.InterestDirection > 0 && x.InterestType == (int)InterestTypeEnum.单利) .ToList(); Console.WriteLine($"[1] 单利利息腿: {simplePositions.Count} 条"); foreach (var p in simplePositions) { Console.WriteLine($" PositionId={p.id} Mode={((InterestModeEnum)p.InterestMode)} PrincipalFix={p.InterestPrincipalFix}"); } // 2. 找最近 eod 日期,作为"模拟平仓日" var latestEodDate = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId) .Max(x => (DateTime?)x.ValueDate); if (latestEodDate == null) { Assert.Inconclusive($"交易 {tradeId} 无 eod 数据"); return; } // 用 eod 后一天作为模拟平仓日 var testDate = latestEodDate.Value.AddDays(1); Console.WriteLine($"\n[2] 模拟平仓日: {testDate:yyyy-MM-dd}(eod最近: {latestEodDate:yyyy-MM-dd})"); // 3. 调用真实 GetUnwindInterests(与前端平仓页相同路径) var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); var service = new SwapDealService(userInfo); var defaults = service.GetUnwindInterests( testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓); // 4. 对每个单利腿,对比"默认值"vs"应计基数(待实现-已结利息)" Console.WriteLine($"\n[3] 单利路径诊断:默认值 vs 应计基数"); Console.WriteLine($" {"PositionId",10} {"InterestMode",14} {"方向",6} {"默认ClosePnL",14} {"InterestAmt",14} {"eod待实现IPS",14} {"历史已结CI",14} {"ratio",6} {"应计(IPS-CI)",14} {"偏大量",14} {"红灯",6}"); int redCount = 0; foreach (var d in defaults.Where(x => x.InterestDirection > 0)) { var pos = simplePositions.FirstOrDefault(x => x.id == d.PositionId); if (pos == null) continue; // 跳过非单利腿 // eod 待实现 var preEod = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId && x.PositionId == d.PositionId && x.ValueDate < testDate) .OrderByDescending(x => x.ValueDate).FirstOrDefault(); decimal ips = preEod?.InterestProfitSum ?? 0; // 历史已结利息(复利路径用的 GetConsumedInterest,单利路径没用) decimal ci = service.GetConsumedInterest(tradeId, d.PositionId, testDate); // InterestClosePnL = InterestAmount × interestRatio(方向系数) // interestRatio = InterestDirection==收取(1) ? 1 : -1 decimal interestRatio = d.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; // 应计基数 = (待实现 - 已结) × ratio(与 InterestClosePnL 同口径) decimal expected = (ips - ci) * interestRatio; decimal actual = d.InterestClosePnL; decimal diff = actual - expected; bool isRed = Math.Abs(ci) > 0.01m && Math.Abs(diff) > Math.Abs(ci) * 0.5m; if (isRed) redCount++; string modeName = ((InterestModeEnum)d.InterestMode).ToString(); string dirName = ((SwapDirectionEnum)d.InterestDirection).ToString(); Console.WriteLine($" {d.PositionId,10} {modeName,14} {dirName,6} {actual,14:F4} {d.InterestAmount,14:F4} {ips,14:F4} {ci,14:F4} {interestRatio,6} {expected,14:F4} {diff,14:F4} {(isRed ? "⚠红灯" : "绿灯"),6}"); } Console.WriteLine($"\n[结论]"); if (redCount > 0) { Console.WriteLine($" ⚠ 单利路径仍存在偏大:{redCount} 条单利腿默认值含历史已结利息。"); Console.WriteLine($" 根因:CalcDailySimpleInterest 起点InterestProfitSum在互换后未归零。"); Console.WriteLine($" 注意:不能简单减consumedInterest(会双重扣减,导致应为1天利息变0)。"); Console.WriteLine($" 正确方案:让InterestProfitSum在互换结清后归零(eod层方案B)。"); } else { Console.WriteLine($" 单利路径未检测到偏大。"); } // 红灯断言:单利路径应存在偏大(待正确修复方案) Assert.IsTrue(redCount > 0, "红灯:单利路径应存在默认值偏大。待正确修复(InterestProfitSum归零)后反转。"); } finally { db?.Dispose(); } } /// /// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。 /// private int SimpleInterestSampleTradeId => 1813; /// /// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。 /// /// 核心逻辑(不改数据,纯查询): /// - 默认值计算读 InterestProfitSum(待实现),不读 RealizedInterest(已实现) /// - 若某持仓 InterestProfitSum >> 0 且已有多次操作(RealizedInterest >> 0), /// 说明下次平仓默认值会基于"全程待实现"重算,重复计入已实现部分 /// - 真正应计基数 = InterestProfitSum - RealizedInterest(剩余未实现) /// [TestMethod] [TestCategory("DBRecording")] public void Step0b_DiagnoseSingleTradeInterestDuplication() { int tradeId = SampleTradeId; YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { Console.WriteLine($"===== 诊断 SwapTradeId={tradeId} 利息端默认值重复计入 =====\n"); // 1. 该交易的利息腿(InterestDirection>0)最新 eod 快照 var latestEodDate = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId) .Max(x => (DateTime?)x.ValueDate); if (latestEodDate == null) { Assert.Inconclusive($"交易 {tradeId} 无 eod 数据"); return; } var interestEods = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId && x.ValueDate == latestEodDate && x.InterestDirection > 0) .OrderBy(x => x.PositionId) .ToList(); Console.WriteLine($"[1] 最新eod({latestEodDate:yyyy-MM-dd})利息腿持仓: {interestEods.Count} 条\n"); Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"待实现InterestProfitSum",22} {"已实现RealizedInterest",22} {"应计基数(待-已)",18} {"重复风险",10}"); int riskCount = 0; foreach (var e in interestEods) { decimal base_ = e.InterestProfitSum - e.RealizedInterest; bool risk = e.InterestProfitSum != 0 && e.RealizedInterest != 0 && Math.Abs(e.InterestProfitSum) > Math.Abs(base_); if (risk) riskCount++; string modeName = ((InterestModeEnum)(e.InterestMode)).ToString(); Console.WriteLine($"{e.PositionId,12} {modeName,12} {e.InterestProfitSum,22:F4} {e.RealizedInterest,22:F4} {base_,18:F4} {(risk ? "⚠有" : "无"),10}"); } // 2. 历史操作记录(看每次利息盈亏) var history = db.swap_flow_event .Where(x => x.SwapTradeId == tradeId && x.DataState == (int)SwapFlowDateStateEnum.完成 && (x.EventType == (int)SwapFlowEventTypeEnum.平仓 || x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换)) .OrderBy(x => x.EventDate).ThenBy(x => x.id) .ToList(); Console.WriteLine($"\n[2] 历史操作记录: {history.Count} 条\n"); Console.WriteLine($"{"id",8} {"EventDate",12} {"EventType",10} {"PositionId",12} {"InterestClosePnL",18} {"InterestAmount",16}"); foreach (var h in history) { string etName = ((SwapFlowEventTypeEnum)h.EventType).ToString(); Console.WriteLine($"{h.id,8} {h.EventDate:yyyy-MM-dd} {etName,10} {h.PositionId,12} {h.InterestClosePnL,18:F4} {h.InterestAmount,16:F4}"); } // 3. 诊断结论 Console.WriteLine($"\n[结论]"); if (riskCount > 0) { Console.WriteLine($"⚠ 有 {riskCount} 条利息腿存在重复计入风险:"); Console.WriteLine($" InterestProfitSum(待实现) 被用作下次平仓默认值计算基数(cs:774),"); Console.WriteLine($" 但它没有扣除 RealizedInterest(已实现)。"); Console.WriteLine($" → 部分平仓后再平仓,默认值会偏大(含已实现部分)。"); } else { Console.WriteLine($" 未检测到重复计入风险(可能 InterestProfitSum 或 RealizedInterest 为0)。"); } Assert.IsTrue(interestEods.Count > 0, "应有利息腿持仓"); } finally { db?.Dispose(); } } /// /// 样本交易ID。1889 = GLMS-20260616-0004,29号收益结算+收盘,30号平仓。 /// private int SampleTradeId => 1889; /// /// Step0c:精确诊断——调用真实的 GetUnwindInterests 拿默认值,对比 eod 应计,定位偏差。 /// /// 这是最直接的验证:用平仓日的参数调 GetUnwindInterests(与前端拿默认值完全相同的路径), /// 看返回的 InterestClosePnL 是否包含了"之前已通过互换实现的部分"。 /// [TestMethod] [TestCategory("DBRecording")] public void Step0c_VerifyDefaultViaRealService() { int tradeId = SampleTradeId; YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { // 找最后一次平仓事件,用它的参数模拟"打开平仓页" var lastClose = db.swap_flow_event .Where(x => x.SwapTradeId == tradeId && x.EventType == (int)SwapFlowEventTypeEnum.平仓 && x.DataState == (int)SwapFlowDateStateEnum.完成) .OrderByDescending(x => x.EventDate) .FirstOrDefault(); if (lastClose == null) { Assert.Inconclusive($"交易 {tradeId} 无平仓记录"); return; } Console.WriteLine($"===== 调用 GetUnwindInterests 验证 SwapTradeId={tradeId} ====="); Console.WriteLine($"模拟平仓日: EventDate={lastClose.EventDate:yyyy-MM-dd} UnwindDate={lastClose.UnwindDate:yyyy-MM-dd}\n"); // 该交易平仓前的最近 eod(用于对比) var preEodDate = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId && x.ValueDate < lastClose.UnwindDate) .Max(x => (DateTime?)x.ValueDate); var preEodInterests = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId && x.ValueDate == preEodDate && x.InterestDirection > 0) .ToList(); Console.WriteLine($"[平仓前最近eod: {preEodDate:yyyy-MM-dd}]"); Console.WriteLine($"{"PositionId",12} {"InterestProfitSum(待实现起点)",28} {"RealizedInterest(已实现)",24}"); foreach (var e in preEodInterests) { Console.WriteLine($"{e.PositionId,12} {e.InterestProfitSum,28:F4} {e.RealizedInterest,24:F4}"); } // 调用真实服务(与前端 GetUnwindInterestList 完全相同的路径) var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); var service = new SwapDealService(userInfo); // closePercent 取实际平仓的(从历史 flow_event 推断:InterestPrincipal / PosiNotionalValue) decimal closePercent = 1m; // 先用全平测试 var defaults = service.GetUnwindInterests( lastClose.EventDate, lastClose.UnwindDate.Value, tradeId, closePercent, (int)SwapEventTypeEnum.平仓); Console.WriteLine($"\n[GetUnwindInterests 返回的默认值] closePercent={closePercent}"); Console.WriteLine($"{"PositionId",12} {"InterestMode",12} {"默认InterestClosePnL",22} {"默认InterestAmount",20} {"实际历史InterestClosePnL",24}"); foreach (var d in defaults.Where(x => x.InterestDirection > 0)) { var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId && x.PositionId == d.PositionId && x.id == lastClose.id); string modeName = ((InterestModeEnum)d.InterestMode).ToString(); Console.WriteLine($"{d.PositionId,12} {modeName,12} {d.InterestClosePnL,22:F4} {d.InterestAmount,20:F4} {hist?.InterestClosePnL ?? 0,24:F4}"); } // 诊断:默认值 vs 历史实际值 的差异 Console.WriteLine($"\n[诊断]"); bool hasDiscrepancy = false; foreach (var d in defaults.Where(x => x.InterestDirection > 0)) { var hist = db.swap_flow_event.FirstOrDefault(x => x.SwapTradeId == tradeId && x.PositionId == d.PositionId && x.id == lastClose.id); if (hist != null && Math.Abs(d.InterestClosePnL - hist.InterestClosePnL) > 0.01m) { Console.WriteLine($" PositionId={d.PositionId}: 默认值={d.InterestClosePnL:F4} vs 历史={hist.InterestClosePnL:F4} 差异={d.InterestClosePnL - hist.InterestClosePnL:F4}"); hasDiscrepancy = true; } } if (hasDiscrepancy) { Console.WriteLine($" ⚠ 默认值与历史实际值有差异(可能是重算口径变化或bug)"); } else { Console.WriteLine($" 默认值与历史实际值一致(该样本未复现偏差)"); } Assert.IsTrue(defaults.Count > 0, "应返回利息腿默认值"); } finally { db?.Dispose(); } } /// /// Step0d:针对 1889(GLMS-20260616-0004)的全面诊断。 /// /// 场景:29号收益结算(互换)+收盘 → 30号平仓。 /// 测试环境会不断回退复用同一笔交易,需甄别。 /// /// 本方法一次性查清: /// 1. swap_event 全历史(含回退 EventType=5),甄别哪些是回退后的有效操作 /// 2. swap_flow_event 全历史(含 DataState≠完成的废弃事件) /// 3. eod_swap_position 按日期序列,看 InterestProfitSum/RealizedInterest 逐日演变 /// 4. 调 GetUnwindInterests 拿30号平仓默认值,对比29号互换已实现的部分 /// [TestMethod] [TestCategory("DBRecording")] public void Step0d_DiagnoseTrade1889_FullTimeline() { int tradeId = SampleTradeId; YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; } try { Console.WriteLine($"===== 全面诊断 SwapTradeId={tradeId} =====\n"); // 1. swap_event 全历史(含回退/删除) var allEvents = db.swap_event .Where(x => x.SwapTradeId == tradeId) .OrderBy(x => x.id) .ToList(); Console.WriteLine($"[1] swap_event 全历史: {allEvents.Count} 条(甄别回退)"); Console.WriteLine($" 仅显示 Invalid=False(有效)的事件:"); var validEvents = allEvents.Where(x => !x.Invalid).ToList(); Console.WriteLine($" {"id",8} {"EventType",10} {"ValueDate",12} {"ClientCashId",12} {"EventReason",-20}"); foreach (var e in validEvents) { string etName = ((SwapEventTypeEnum)e.EventType).ToString(); Console.WriteLine($" {e.id,8} {etName,10} {e.ValueDate:yyyy-MM-dd} {e.ClientCashId,12} {(e.EventReason ?? ""),-20}"); } Console.WriteLine($" (另有 {allEvents.Count(x => x.Invalid)} 条 Invalid=True 的回退/历史事件,已隐藏)"); // 2. swap_flow_event 全历史(仅完成状态,过滤废弃) var allFlowEvents = db.swap_flow_event .Where(x => x.SwapTradeId == tradeId) .OrderBy(x => x.id) .ToList(); var validFlowEventsAll = allFlowEvents.Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成).ToList(); Console.WriteLine($"\n[2] swap_flow_event 完成状态: {validFlowEventsAll.Count} 条(共{allFlowEvents.Count}条,已隐藏{allFlowEvents.Count - validFlowEventsAll.Count}条废弃)"); Console.WriteLine($" {"id",8} {"EventDate",12} {"UnwindDate",12} {"EventType",10} {"PositionId",10} {"InterestClosePnL",18} {"InterestAmount",16} {"MarkClosePnl",14}"); foreach (var f in validFlowEventsAll) { string etName = ((SwapFlowEventTypeEnum)f.EventType).ToString(); Console.WriteLine($" {f.id,8} {f.EventDate:yyyy-MM-dd} {f.UnwindDate?.ToString("yyyy-MM-dd") ?? "-",-12} {etName,10} {f.PositionId,10} {f.InterestClosePnL,18:F4} {f.InterestAmount,16:F4} {f.MarkClosePnl,14:F4}"); } // 3. eod_swap_position 按日期序列(利息腿),看 InterestProfitSum/RealizedInterest 演变 var eodTimeline = db.eod_swap_position .Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0) .OrderBy(x => x.ValueDate).ThenBy(x => x.PositionId) .ToList(); Console.WriteLine($"\n[3] eod_swap_position 利息腿按日序列: {eodTimeline.Count} 条"); Console.WriteLine($"{"ValueDate",12} {"PositionId",10} {"InterestProfitSum",18} {"RealizedInterest",18} {"TdCloseInterest",16} {"InterestIncomeSum",18}"); foreach (var e in eodTimeline) { Console.WriteLine($"{e.ValueDate:yyyy-MM-dd} {e.PositionId,10} {e.InterestProfitSum,18:F4} {e.RealizedInterest,18:F4} {e.TdCloseInterest,16:F4} {e.InterestIncomeSum,18:F4}"); } // 4. 甄别:找出有效的 29号互换 和 30号平仓 var validFlowEvents = allFlowEvents .Where(x => x.DataState == (int)SwapFlowDateStateEnum.完成) .OrderBy(x => x.EventDate).ThenBy(x => x.id) .ToList(); var swapOn29 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 29) && (x.EventType == (int)SwapFlowEventTypeEnum.互换 || x.EventType == (int)SwapFlowEventTypeEnum.自动互换)).ToList(); var closeOn30 = validFlowEvents.Where(x => x.EventDate == new DateTime(2026, 6, 30) && x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList(); Console.WriteLine($"\n[4] 关键操作甄别(DataState=完成)"); Console.WriteLine($" 29号互换/自动互换: {swapOn29.Count} 条"); foreach (var s in swapOn29) Console.WriteLine($" id={s.id} PositionId={s.PositionId} InterestClosePnL={s.InterestClosePnL:F4} InterestAmount={s.InterestAmount:F4}"); Console.WriteLine($" 30号平仓: {closeOn30.Count} 条"); foreach (var c in closeOn30) Console.WriteLine($" id={c.id} PositionId={c.PositionId} InterestClosePnL={c.InterestClosePnL:F4} InterestAmount={c.InterestAmount:F4}"); // 5. 模拟"打开平仓页"——分别测 6-29/6-30/7-1 三天,对比默认值变化 Console.WriteLine($"\n[5] 调 GetUnwindInterests 模拟打开平仓页(6-29/6-30/7-1 三天对比)"); // 先查利息腿的计息类型(单利/复利),判断走哪个修复路径 var interestPositions = DbContextFactory.GetYLDbContext().swap_position .Where(x => x.SwapTradeId == tradeId && x.InterestDirection > 0 && !x.Invalid).ToList(); foreach (var p in interestPositions) { Console.WriteLine($" PositionId={p.id} InterestMode={((InterestModeEnum)p.InterestMode)} InterestType={((InterestTypeEnum)p.InterestType)}"); } var userInfo = new OptUserInfo(1, "UnitTest", OptUserFrom.UnitTest); var service = new SwapDealService(userInfo); var testDates = new[] { new DateTime(2026, 6, 29), new DateTime(2026, 6, 30), new DateTime(2026, 7, 1), }; Console.WriteLine($" {"日期",12} {"PositionId",10} {"InterestMode",14} {"默认InterestClosePnL",22} {"eod待实现IPS",14} {"eod已实现RI",14} {"Δ默认-待实现",14}"); foreach (var testDate in testDates) { var defaults = service.GetUnwindInterests( testDate, testDate, tradeId, 1m, (int)SwapEventTypeEnum.平仓); foreach (var d in defaults.Where(x => x.InterestDirection > 0)) { // 找该日期前最近的 eod var preEod = eodTimeline.Where(x => x.PositionId == d.PositionId && x.ValueDate < testDate) .OrderByDescending(x => x.ValueDate).FirstOrDefault(); decimal ips = preEod?.InterestProfitSum ?? 0; decimal ri = preEod?.RealizedInterest ?? 0; decimal delta = d.InterestClosePnL - ips; string modeName = ((InterestModeEnum)d.InterestMode).ToString(); string preEodDate = preEod?.ValueDate.ToString("MM-dd") ?? "无"; Console.WriteLine($" {testDate:yyyy-MM-dd} {d.PositionId,10} {modeName,14} {d.InterestClosePnL,22:F4} {ips,14:F4}({preEodDate}) {ri,14:F4} {delta,14:F4}"); } } // 6. 核心诊断 Console.WriteLine($"\n[6] 核心诊断"); Console.WriteLine($" 关键观察:29号互换已实现 77.26,看 eod 的 InterestProfitSum(待实现) 是否扣减了已实现部分"); var eod29 = eodTimeline.Where(x => x.ValueDate == new DateTime(2026, 6, 29)).ToList(); foreach (var e in eod29) { Console.WriteLine($" PositionId={e.PositionId} 6-29 eod:"); Console.WriteLine($" InterestProfitSum(待实现) = {e.InterestProfitSum:F4}"); Console.WriteLine($" RealizedInterest(已实现) = {e.RealizedInterest:F4}"); Console.WriteLine($" TdCloseInterest(当日实现) = {e.TdCloseInterest:F4}"); if (e.InterestProfitSum != 0 && e.RealizedInterest != 0 && Math.Abs(e.InterestProfitSum - e.RealizedInterest) < 0.1m) { Console.WriteLine($" ⚠ 待实现({e.InterestProfitSum:F4}) ≈ 已实现({e.RealizedInterest:F4}) → 互换结清后待实现没归零!"); Console.WriteLine($" → 导致后续平仓默认值仍基于待实现(77.26)算,偏大"); } } Console.WriteLine($"\n 用户反馈:6-29看平仓默认=0(正确,因为当天还没收盘/互换),6-30和7-1有问题"); Console.WriteLine($" 根因:29号收盘后 InterestProfitSum 没扣减已实现的 77.26(仍=77.26),"); Console.WriteLine($" 所以后续平仓默认值 = 77.26(应已归零的待实现) + 增量 → 偏大"); Assert.IsTrue(allEvents.Count > 0); } finally { db?.Dispose(); } } } }