From cd03fc53fbdd400ed4183a75f5337012081a036e Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 1 Jul 2026 09:56:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=92=E6=8D=A2=E4=B8=A4=E4=B8=AA=E7=8B=AC?= =?UTF-8?q?=E7=AB=8Bbug=E5=88=86=E6=9E=90=E6=96=87=E6=A1=A3+=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug①利息默认值偏大: 互换结清后eod的InterestProfitSum(待实现)没归零 (=RealizedInterest=77.26), 导致后续平仓默认值=77.26(本该0)+增量→偏大。 6-29收盘后三天对比坐实: 6-30默认83.70, 7-1默认90.14, 均含已实现的77.26。 根因 eod公式cs:829 对互换场景扣减失效(TdInterestIncome≈TdCloseInterest抵消)。 Bug②收益结算审核后状态卡死: ApproveSwapTrade(cs:1525)漏ExerciseDate判断, 到期互换审核后状态退回'确认成交'(CloseMethod默认0≠全部平仓1), 导致EodCheckMaturityTrade一直阻止收盘。引入点083848fb(2025-05-28)。 - 新增利息偏大分析文档(根因/修复方案A扣RealizedInterest/B eod归零) - 新增审核状态卡死分析文档(根因/修复补ExerciseDate判断) - 新增SwapPartialUnwindInterestDefaultTest(Step0探查/Step0b诊断/Step0d三天对比) --- .../SwapPartialUnwindInterestDefaultTest.cs | 454 ++++++++++++++++++ ...收益结算审核后状态卡死阻止收盘问题分析.md | 129 +++++ ...平仓后利息端预付金默认盈亏偏大问题分析.md | 295 ++++++++++++ 3 files changed, 878 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs create mode 100644 项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md create mode 100644 项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md diff --git a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs new file mode 100644 index 00000000..a0da7e58 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs @@ -0,0 +1,454 @@ +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(); + } + } + + /// + /// 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 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(); + } + } + } +} diff --git a/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md b/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md new file mode 100644 index 00000000..37c0ddf9 --- /dev/null +++ b/项目文档/互换收益结算审核后状态卡死阻止收盘问题分析.md @@ -0,0 +1,129 @@ +# 互换收益结算审核后状态卡死、阻止收盘问题分析 + +> 本文分析到期互换"收益结算(互换)审核通过后,交易状态不流转,导致收盘到期检查一直阻止"的问题。 +> 成文于 2026-07-01。 + +--- + +## 一、问题概述 + +**复现场景**:一笔互换交易到期日(如 2026-06-29),用户在界面做"收益结算(互换)"并审核通过,但: +- 交易状态没变成"已到期"或"已平仓",仍是"确认成交" +- 不管做多少次收益结算+审核,状态依旧不变 +- 收盘报错:`[检查当日到期交易]当日到期的场外交易未全部操作完成,请操作完成后再执行收盘操作:收益互换 GLMS-20260427-0001` + +--- + +## 二、根因:审核路径 ApproveSwapTrade 漏了到期日判断 + +### 代码对照(铁证) + +**免审核路径 `SwapIncome`(正确,基线就有)** — `SwapDealService.cs:1481-1485`: +```csharp +if (td.ExerciseDate <= unwindData.ValueDate) // 判断到期 +{ + td.Notional = 0; + td.StockEqvNotional = 0; + td.TradeStatus = "已到期"; // ✅ 到期 → 已到期 +} +``` + +**审核路径 `ApproveSwapTrade`(漏了)** — `SwapDealService.cs:1525-1538`: +```csharp +if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓) + td.TradeStatus = "已平仓"; +else +{ + td.TradeStatus = ConsTrade.确认成交; // ❌ 到期互换落到这里!没有 ExerciseDate 判断 + td.HasPartialUnWind = 1; +} +``` + +`ApproveSwapTrade` **完全没有 `ExerciseDate` 到期判断**,而互换操作的 `CloseMethod` 又是默认值 0(`InitIncome` cs:216 从不设它)→ 0 ≠ 全部平仓(1) → 走 else → 状态退回"确认成交"。 + +### 为什么互换必然走 else 分支 + +- `CloseMethodEnum`:Unknown=0 / 全部平仓=1 / 部分平仓=2 +- 互换初始化 `InitIncome`(cs:216-292)**从不给 `unwindData.CloseMethod` 赋值** → 默认 0 +- 只有平仓路径(`InitUnwind`/`InitLongShortUnwind`)才设 `CloseMethod=全部平仓` +- 0 ≠ 1 → 必走 else → 退回"确认成交" + +### 收盘检查为什么一直阻止 + +`EodCheckMaturityTrade.cs:27-33`(到期检查): +```csharp +var predicate = PredicateBuilder.Create(t => + t.ExerciseDate >= startDate && t.ExerciseDate <= settleDate // 今日到期 + && t.TradeStatus == ConsTrade.确认成交 // ★ 状态还是"确认成交" + && t.SettlementFlag != 1); +``` + +判断口径只看 `TradeStatus`。`TradeCompleteStatus = { 已到期, 已执行, 已平仓 }`(ConsTrade.cs:33)。审核后状态停在"确认成交"(不在完成列表里)→ 永远命中 → 永远阻止。 + +--- + +## 三、关于"提交数量但审核时数量0" + +用户还反馈"提交了数量但审核时候数量0"。经查: + +1. `InitIncome`(cs:216-292)**从没给 `unwindData.CloseQty` 赋值**(对比 `InitUnwind` cs:90 有赋值)→ `CloseQty` 默认 0 +2. `SaveSwapDeal`(cs:1597)的 `item.Quantity = unwindData.CloseQty` 被 `eventType == 平仓` 条件挡住 → 互换(3)不执行 + +但这不是核心问题——**核心是状态没流转**。互换的 Quantity 语义与平仓不同(互换是收益结清,不是按数量平仓),数量为 0 可能是设计如此。状态卡死才是阻止收盘的直接原因。 + +--- + +## 四、引入时间(git blame) + +| 路径 | 提交 | 时间 | 说明 | +|------|------|------|------| +| `SwapIncome` 到期判断(正确)| `f9d8a256` | 2024-05-09 | 山证基线,一直有 | +| `ApproveSwapTrade` 状态分支(漏判断)| `083848fb` | 2025-05-28 | 审核流程重构,漏同步 ExerciseDate 判断 | + +`083848fb`(2025-05-28)重构审核流程时把状态分支改成 `CloseMethod==全部平仓` 判断,但**忘了同步 `SwapIncome` 里的 `ExerciseDate` 到期判断**。 + +--- + +## 五、涉及文件 + +| 文件:行号 | 问题 | +|----------|------| +| `SwapDealService.cs:1525-1538`(ApproveSwapTrade)| **漏 ExerciseDate 判断**,到期互换退回"确认成交" | +| `SwapDealService.cs:1481-1485`(SwapIncome)| 正确对照(有到期判断)| +| `SwapDealService.cs:216`(InitIncome)| 不设 CloseMethod → 默认 0,必然走 else | +| `EodCheckMaturityTrade.cs:27-33` | 阻止收盘的检查(只认 TradeStatus)| +| `ConsTrade.cs:33` | TradeCompleteStatus 定义(确认成交不在内)| + +--- + +## 六、修复方向(最小改动) + +在 `ApproveSwapTrade`(cs:1534-1538)的 else 分支补到期判断,与 `SwapIncome` 对齐: + +```csharp +else +{ + // 补:到期互换应设为"已到期"(与 SwapIncome cs:1481 对齐) + if (td.ExerciseDate <= swapEvent.unwindData.ValueDate) + { + td.Notional = 0; + td.StockEqvNotional = 0; + td.TradeStatus = "已到期"; + } + else + { + td.TradeStatus = ConsTrade.确认成交; + td.HasPartialUnWind = 1; + } +} +``` + +改后到期互换审核通过 → 状态正确变成"已到期" → 收盘检查通过。 + +--- + +## 七、团队决策问题 + +1. **互换的 CloseMethod 是否应该设值**:当前 `InitIncome` 不设 CloseMethod(默认 0),是否应在互换初始化时根据是否全额结清设为"全部平仓"?这会影响 `ApproveSwapTrade` 走哪个分支。 +2. **互换 Quantity=0 是否符合预期**:`InitIncome` 不设 CloseQty,导致互换 flow_event 的 Quantity=0。需业务确认这是设计意图还是遗漏。 +3. **历史卡住的交易怎么处理**:GLMS-20260427-0001 等已卡住的交易,是否需要手动改 TradeStatus 为"已到期"才能继续收盘? diff --git a/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md b/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md new file mode 100644 index 00000000..030c8084 --- /dev/null +++ b/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md @@ -0,0 +1,295 @@ +# 互换部分平仓后:利息端/预付金"平仓盈亏默认值"偏大问题分析 + +> 本文分析收益互换"昨天收益结算(互换)→ 收盘 → 今天平仓"场景下,**预付金平仓盈亏和利息端平仓盈亏的默认值计算偏大**的问题:根因、涉及的文件、如何确认、修复方案与自测方法。 +> 成文于 2026-07-01。 + +--- + +## 一、问题概述 + +**复现场景**: +1. 第一天:对互换交易做**收益结算(互换)** → 收盘 +2. 第二天:再次进入**平仓**页 + +**现象**:平仓页的"预付金平仓盈亏"和"利息端平仓盈亏"**默认值偏大**——包含了之前已经通过互换/收益结算平掉的那部分利息,没有扣除。 + +**用户怀疑**:"这部分是不是没有减去之前已经平掉的部分?"——**怀疑成立**,下文逐层证明。 + +--- + +## 二、数据流:这两个默认值从哪来 + +### 关键结论:默认值是后端算的,前端只展示 + +前端 `unwindSwapTrade.js` **不计算**这两个默认值,只是把后端返回的 `InterestClosePnL` 原样填到输入框: + +| 界面字段 | 绑定字段 | 数据来源 | +|---------|---------|---------| +| 预付金平仓盈亏 | `marginList[].InterestClosePnL` | 后端接口返回 | +| 利息端平仓盈亏 | `interestList[].InterestClosePnL` | 后端接口返回 | +| 浮动端平仓盈亏 | `floatPosition.FloatPnlSum`(前端现算)| 前端 `calcFloatClosePnl` | + +前端调接口(`unwindSwapTrade.js:264-265`): +```js +{ valueDate, unwindDate, tradeId, closePercent, eventType: 2 } +``` + +### 后端接口链路 + +``` +POST /swaptrade2/GetUnwindInterestList (SwapTrade2Controller.cs:287) + └─ SwapDealService.GetUnwindInterests (SwapDealService.cs:302) + ├─ 取 closeList = 当天已完成的平仓/互换事件 (cs:336) + └─ GetInterests (cs:358) + ├─ CalcNotionalByMode (cs:453) ← 按利息模式算计息基数 + ├─ CalcUnwindInterest → InitSwapDealInterest (cs:595/626) + │ └─ CalcDailySimpleInterest / CalcDailyCompoundInterest (cs:771/705) + │ ↑ 这里全量重算利息,不扣历史已平部分 + └─ 同日去重:closeList 扣减 (cs:418-446) ← 只扣当天,跨天漏 +``` + +--- + +## 三、根因:互换结清后 eod 的 InterestProfitSum(待实现)没扣减已实现部分 + +> ✅ 6-29 收盘后用交易 1889(GLMS-20260616-0004)三天对比实测坐实。用户的判断"从开头算了"正确。 + +### 3.1 实测坐实(6-29 收盘后,6-29/6-30/7-1 三天对比) + +利息腿 PositionId=34695(标的期初全价),29号收益结算(互换) + 29号收盘后: + +``` +日期 平仓默认值 eod待实现(IPS) eod已实现(RI) 说明 +6-29(互换前) 0.0003 77.26(6-28eod) 0.00 当天看=0,正确 +6-30 83.6986 77.26(6-29eod) 77.26 偏大! +7-1 90.1370 77.26(6-29eod) 77.26 更偏大! +``` + +**核心铁证**:6-29 收盘后,`InterestProfitSum`(待实现)= **77.26**,`RealizedInterest`(已实现)= **77.26**。**两个相等——互换已经把全部利息实现了(77.26),但"待实现"没归零**,还是 77.26。 + +所以后续平仓默认值 = 待实现(77.26) + 每日增量(6.44/天)。6-30 = 77.26+6.44=83.70,7-1 = 77.26+12.88=90.14。**偏大的量正是那个本该归零却没归零的 77.26**。 + +### 3.2 根因机制:eod 扣减公式对"互换"场景失效 + +`SwapEodPositionService.cs:829`(收盘更新待实现利息): +```csharp +InterestIncomeSum = pre.InterestIncomeSum + TdInterestIncome - TdCloseInterest; +// = 77.26(6-28) + 77.26(当日新计) - 77.26(当日互换实现) +// = 77.26 ← 抵消了,没扣减! +``` + +**为什么抵消**:互换操作当日会重新计息(`TdInterestIncome` = 77.26,相当于把到期前的利息算了一遍),同时互换实现(`TdCloseInterest` = 77.26)。两个相等,相减为 0,所以 `InterestIncomeSum` 维持在 77.26 不变。 + +**正常平仓不会这样**:平仓时 `TdInterestIncome` 是"昨日到今日的增量"(小),`TdCloseInterest` 是"平仓实现的全部"(大),两者不等 → 能扣减。但互换(收益结算)的计息逻辑把全程利息都算了一遍(`TdInterestIncome ≈ TdCloseInterest`),导致扣减失效。 + +### 3.3 CalcDailySimpleInterest 读这个"没扣减的待实现"作为起点 + +`SwapDealService.cs:771`(CalcDailySimpleInterest): +```csharp +decimal interestProfitSum = preEodPosition.InterestProfitSum; // :774 读 77.26(本该归零) +decimal interest = interestProfitSum * closePercent; // :776 起点 = 77.26 +for (int i = 0; i <= calcDays; i++) // :784 补算昨日之后增量 +{ + if (accrueDate > preEodPosition.ValueDate) // :789 + interest += interest1; // :823 每天+6.44 +} +// 结果:6-30 = 77.26 + 6.44 = 83.70(偏大,含已实现的77.26) +``` + +`GetInterests` 的同日去重(cs:435-443)只覆盖"当天"的已完成事件,29号互换 EventDate=6-29 ≠ 平仓日 6-30 → **跨天去重不生效**。 + +### 3.4 根因总结 + +``` +互换结清后正确的状态应该是:InterestProfitSum(待实现) = 0(全部已实现) +但实际:InterestProfitSum = 77.26(= RealizedInterest,没归零) + ↓ 原因 +eod 公式 cs:829:IPS = pre.IPS + TdInterestIncome - TdCloseInterest + 互换时 TdInterestIncome ≈ TdCloseInterest → 抵消 → IPS 不变 + ↓ 后果 +后续平仓默认值 = IPS(77.26,本该0) + 增量 → 偏大 77.26 +``` + +--- + +## 四、涉及哪些文件 + +| 层 | 文件:行号 | 作用/问题 | +|----|----------|----------| +| 后端·默认值入口 | `SwapTrade2Controller.cs:287` | GetUnwindInterestList 接口 | +| 后端·默认值计算 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| 主计算链路 | +| 后端·单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| 起点=InterestProfitSum×closePercent,补算昨日之后增量 | +| 后端·复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| 从头算,注释"只能用要平仓的名义本金从头开始算" | +| 后端·方向系数 | `SwapDealService.cs:673`(interestRatio)| 收取=1/支付=-1,**实测符号相反疑似此处** | +| 后端·同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435`(算尾跳过)| 跨天不扣 | +| 后端·固定值本金(可疑B)| `SwapDealService.cs:461-463` | 固定值强制100% | +| 后端·eod扣减待实现 | `SwapEodPositionService.cs:829` | InterestIncomeSum 扣 TdCloseInterest(**有扣减**)| +| 前端·展示(非源)| `unwindSwapTrade.js:262-275` | 仅展示后端值 | + +> **前端不是 bug 源**:前端只把后端 `InterestClosePnL` 填进输入框,无额外计算。 + +--- + +## 五、引入时间(git blame) + +| 代码点 | 提交 | 时间 | 说明 | +|--------|------|------|------| +| `interestProfitSum = preEodPosition.InterestProfitSum`(cs:774)| `f9d8a256` | 2024-05-09 | 山证基线 | +| `interest = interestProfitSum * closePercent`(cs:776)| `6708878d` | 2026-04-13 hjhan | 改起点为×closePercent | +| 同日去重(cs:435-443)| `535497c68` | 2026-06-04 吴方海 | 加同日去重补救,仅覆盖当天 | +| eod 扣减 TdCloseInterest(cs:829)| 基线 | 2024-05 | eod 层有扣减已实现 | + +**结论**:利息计算链路是基线设计,经历过多次局部修补(`6708878d` 改起点、`535497c68` 加同日去重)。**具体哪个改动引入了用户反馈的偏差,需用干净样本定位**。 + +--- + +## 六、如何确认 bug(已坐实) + +### 6.1 实测坐实(交易 1889,6-29 收盘后三天对比,Step0d) + +用真实的 `GetUnwindInterests` 分别测 6-29/6-30/7-1 三天,结果(PositionId=34695 标的期初全价): + +``` +日期 平仓默认值 eod待实现(IPS) eod已实现(RI) 说明 +6-29(互换前) 0.0003 77.26(6-28eod) 0.00 当天看=0,正确 +6-30 83.6986 77.26(6-29eod) 77.26 偏大!含已实现的77.26 +7-1 90.1370 77.26(6-29eod) 77.26 更偏大! +``` + +**核心铁证**:6-29 收盘后 `InterestProfitSum`(待实现)= 77.26 = `RealizedInterest`(已实现)= 77.26。互换已全部实现,但待实现没归零 → 后续默认值 = 77.26(本该0) + 增量 → 偏大。 + +### 6.2 录制测试 + +`SwapPartialUnwindInterestDefaultTest.Step0d_DiagnoseTrade1889_FullTimeline`: +- 展示 swap_event 全历史(甄别回退) +- 展示 eod 按日序列(InterestProfitSum/RealizedInterest 演变) +- 调用真实 `GetUnwindInterests` 拿默认值 +- 对比"默认值 vs eod待实现"判定是否重复计入 + +--- + +## 七、修复方案 + +> 根因已精确定位:互换结清后 `InterestProfitSum` 没归零(= RealizedInterest),导致后续平仓默认值偏大。 + +### 方案 A(推荐·最小改动):平仓默认值起点扣除 RealizedInterest + +`CalcDailySimpleInterest`(cs:774/776)读 `InterestProfitSum` 作为起点。既然 `InterestProfitSum` 没扣已实现,就在这里扣: + +```csharp +// SwapDealService.cs:774-776 改前 +decimal interestProfitSum = preEodPosition.InterestProfitSum; +var TdInterestPrincipal = preEodPosition.TdInterestPrincipal; +decimal interest = interestProfitSum * closePercent; + +// 改后:扣除已通过互换/平仓实现的部分 +decimal interestProfitSum = preEodPosition.InterestProfitSum - preEodPosition.RealizedInterest; +var TdInterestPrincipal = preEodPosition.TdInterestPrincipal; +decimal interest = interestProfitSum * closePercent; +``` + +**原理**:`InterestProfitSum`(待实现累计)- `RealizedInterest`(已实现累计)= **真正未实现的利息**。互换结清后 IPS=77.26、RI=77.26,相减=0,起点正确归零。后续只加增量。 + +**验证**(用 1889 数据): +- 6-30 默认值 = (77.26 - 77.26) + 6.44(一天增量) = **6.44** ✅(而非偏大的 83.70) +- 7-1 默认值 = (77.26 - 77.26) + 12.88 = **12.88** ✅ + +**优点**:改动 1 行,利用现有字段,覆盖单利路径。 +**风险**:需确认所有场景下 `RealizedInterest` 都正确累加了已实现利息。对复利路径(`CalcDailyCompoundInterest` cs:705)需同步检查。 + +### 方案 B(治本):修正 eod 扣减公式,让互换结清后 InterestProfitSum 正确归零 + +根因在 `SwapEodPositionService.cs:829`——互换时 `TdInterestIncome ≈ TdCloseInterest` 导致扣减抵消。需在互换(EventType=互换/自动互换)的收盘路径里特殊处理: + +```csharp +// SwapEodPositionService.cs:829 附近 +// 互换结清时,待实现利息应直接归零(已全部实现),不能靠 TdInterestIncome - TdCloseInterest 抵消 +if (isSwap && closePercent == 1) // 全量互换结清 +{ + newEodPayPosition.InterestIncomeSum = 0; // 待实现归零 +} +else +{ + newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + + newEodPayPosition.TdInterestIncome + - newEodPayPosition.TdCloseInterest; +} +``` + +**优点**:从源头修正,所有读 `InterestProfitSum` 的地方都受益。 +**缺点**:改动 eod 核心逻辑,风险较大,需全面回归。 + +### 方案 C(补充):closeList 扩展到历史 + +`GetUnwindInterests` 的 closeList(cs:336)只查当天,跨天互换事件漏掉。可扩展到历史: +```csharp +// cs:336 改 UnwindDate == unwindDate 为 UnwindDate <= unwindDate +``` +但 cs:435 的 `!calcLast` 限制仍会导致算尾配置下不扣。作为方案 A/B 的补充。 + +### 方案对比 + +| 方案 | 改动量 | 治本程度 | 风险 | +|------|--------|---------|------| +| A(扣 RealizedInterest)| 极小(1行) | 修默认值 | 低,需验证字段口径 | +| B(eod 公式归零)| 中 | 修源头 | 较高,改 eod 核心 | +| C(closeList 扩历史)| 小 | 修去重 | 中,算尾仍漏 | + +**建议**:优先方案 A 止血(最小改动,直接验证),方案 B 作为后续治本。 + +--- + +## 八、录制 golden source + +### 已坐实(Step0d,交易 1889,6-29 收盘后三天对比) + +`Step0d_DiagnoseTrade1889_FullTimeline` 已调用真实服务坐实 bug: +- 6-29 收盘后 InterestProfitSum(待实现)=77.26 = RealizedInterest(已实现)=77.26,**没归零** +- 6-30 默认值 83.70 = 77.26(本该归零的待实现) + 6.44(一天增量) → 偏大 77.26 +- 7-1 默认值 90.14 = 77.26 + 12.88(两天增量) → 偏大 77.26 + +### 录制 golden JSON + +当前 Step0d 是诊断输出(Console),尚未序列化 golden 文件。建议后续: +1. 把 1889 的快照(swap_event + eod + 默认值结果)序列化为 `SwapPartialUnwindInterest/partial_unwind_trade_1889.json` +2. 红灯断言:`Assert(默认值 > eod待实现 × 合理倍数)`,修复后反转 + +### 注意:1889 有大量回退 + +1889 有 52 条 swap_event(48 条 Invalid=True 的回退历史),测试环境反复回退复用。录制 golden 时需注意甄别有效事件(Invalid=False)。Step0d 已处理:只展示 Invalid=False 的 4 条有效事件。 + +### 与前两个 golden 的关系 + +三份 golden 构成"互换损益计算"回归基线: +- `SwapDividend/`(分红重复计算)✅ +- `SwapReEodDeleteCash/`(重收盘误删资金)✅ +- `SwapPartialUnwindInterest/`(本次)⏳ Step0d 已坐实,golden 待序列化 + +--- + +## 九、团队决策问题 + +1. **业务口径确认**:部分平仓/互换后再次平仓,利息端盈亏默认值应该是"剩余持仓的应计利息(扣已实现)"?当前行为是"从头算(含已实现)",业务上预期哪个? +2. **方案选择**:方案 A(closeList 扩历史,推荐)/ 方案 B(扣 RealizedInterest)? +3. **回退复用的影响**:1889 被反复回退,eod 只到 6-22(29号收盘数据疑似被回退清掉)。需确认:回退是否应该清理 eod?还是回退后 eod 应保留? +4. **是否补单测**:建议把 Step0d 的坐实逻辑固化为 golden + 红灯断言,防止复发。 + +--- + +## 附录:关键代码位置索引 + +| 项 | 文件:行号 | +|----|----------| +| 默认值接口入口 | `SwapTrade2Controller.cs:287`(GetUnwindInterestList)| +| 默认值计算主方法 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| +| 单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| +| 复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| +| InterestProfitSum 读取(起点)| `SwapDealService.cs:774` / `:645`(InitSwapDealInterest)| +| 方向系数(实测符号相反疑似)| `SwapDealService.cs:673`(interestRatio)| +| 同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435-443`(扣减)| +| 固定值本金(可疑B)| `SwapDealService.cs:461-463`(CalcNotionalByMode)| +| eod 扣减待实现(有扣减)| `SwapEodPositionService.cs:829`(InterestIncomeSum 扣 TdCloseInterest)| +| 平仓后回写持仓 | `SwapDealService.cs:1664`(InterestAmount 累加)/ `:1668`(预付金扣减)| +| 前端展示(非源)| `unwindSwapTrade.js:262-275` / `SwapUnwind.cshtml:113,146`| +| 数据模型·待实现 | `EodSwapPosition.cs:282`(InterestProfitSum)/ `:270`(InterestIncomeSum)| +| 数据模型·已实现 | `EodSwapPosition.cs:355`(RealizedInterest)/ `:368`(RealizedPnl)|