using System.Diagnostics; using YLErp.BLL; using YLErp.DBModels; namespace YLErp.Modules.EodModule { /// /// FR007 UnderlyingId 错挂诊断(连 96 测试库)—— GLMS-20260701 同类事故复发排查 /// ============================================================================ /// 背景:EodPriceUnderlyingIdGuardTest 记录的事故—— /// FR007 价格行 UnderlyingCode='FR007' 但 UnderlyingId 被错写成 /// 511160.SH(2173889)/159111.SZ(2173890),正确应为 FR007 的 2170838。 /// 网页端按 UnderlyingId(int) JOIN underlying_manager 把 FR007 行误挂到别的标的(显示正常); /// EOD 结算按 UnderlyingCode(string 'FR007') JOIN 查不到 → "结算价格缺失 / 没用上"。 /// /// 本测试连真实库,回答用户问题:"是不是又关联到错误标的了?" /// 1) 查 underlying_manager 里 FR007 的正确 id /// 2) 查 eod_commodity_future_price 里所有 UnderlyingCode='FR007' 的行,看 UnderlyingId 是否=正确 id /// 3) 对比"网页端查询(UnderlyingId JOIN)" vs "EOD 查询(UnderlyingCode JOIN)" 是否一致 /// 4) 核对最近 N 天的 FR007 行是否错挂(复发判定) /// /// 用法:本地连 96 库跑 Diagnose_FR007_UnderlyingIdMismatch;连不上库自动 Inconclusive。 /// [TestClass] public class GLMS20260805FR007UnderlyingIdDiagnoseTest { /// /// 诊断 FR007 价格行的 UnderlyingId 是否错挂(GLMS-20260701 同类复发判定) /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_FR007_UnderlyingIdMismatch() { DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } // ---- 1. 查 underlying_manager 里 FR007 的正确 id(权威定义)---- var fr007Underlying = db.underlying_manager .Where(a => a.UnderlyingCode == "FR007") .Select(a => new { a.id, a.UnderlyingCode, a.UnderlyingName }) .ToList(); Console.WriteLine("===== 1. underlying_manager 里 FR007 的定义 ====="); if (fr007Underlying.Count == 0) { Console.WriteLine(" ⚠⚠ underlying_manager 无 UnderlyingCode='FR007' 的记录!"); Console.WriteLine(" → 这是致命问题:EOD 按 UnderlyingCode 查 FR007 必然查不到(结算价格缺失)"); } foreach (var u in fr007Underlying) { Console.WriteLine($" id={u.id} Code={u.UnderlyingCode} Name={u.UnderlyingName} ← 这是 FR007 的正确 UnderlyingId"); } int? fr007CorrectId = fr007Underlying.FirstOrDefault()?.id; Console.WriteLine(); // ---- 2. 查 eod_commodity_future_price 里所有 FR007 行,看 UnderlyingId 是否错挂 ---- var fr007PriceRows = db.eod_commodity_future_price .Where(a => a.UnderlyingCode == "FR007") .OrderByDescending(a => a.ValueDate) .Take(30) .Select(a => new { a.ValueDate, a.UnderlyingCode, a.UnderlyingId, a.ReferencePrice, a.DataSource }) .ToList(); Console.WriteLine($"===== 2. eod_commodity_future_price 里 FR007 行(最近{fr007PriceRows.Count}条,新→旧)====="); Console.WriteLine($" {"ValueDate",-12}{"UnderlyingCode",-16}{"UnderlyingId",-14}{"是否错挂?",-12}{"ReferencePrice",-16}{"DataSource"}"); int mismatchCount = 0; foreach (var r in fr007PriceRows) { bool mismatched = fr007CorrectId.HasValue && r.UnderlyingId.HasValue && r.UnderlyingId.Value != fr007CorrectId.Value; if (mismatched) mismatchCount++; string flag = mismatched ? "⚠错挂!" : (r.UnderlyingId == null ? "空" : "✓正确"); Console.WriteLine($" {r.ValueDate:yyyy-MM-dd} {r.UnderlyingCode,-16}{r.UnderlyingId?.ToString() ?? "NULL",-14}{flag,-12}{r.ReferencePrice,-16}{r.DataSource}"); } Console.WriteLine($"\n 小结:{mismatchCount}/{fr007PriceRows.Count} 条 FR007 行 UnderlyingId 错挂"); if (mismatchCount > 0) { Console.WriteLine(" ⚠⚠ 确认复发:FR007 行的 UnderlyingId 被错写成别的标的 id!"); Console.WriteLine(" 网页端按 UnderlyingId JOIN 能查到(误挂到 511160.SH/159111.SZ 等),但 EOD 按 UnderlyingCode='FR007' 查反而正常"); Console.WriteLine(" → 若用户看到'网页有、EOD 没用上',需进一步看 EOD 查询路径(见下方第4步)"); } Console.WriteLine(); // ---- 3. 反向查:UnderlyingId=FR007正确id 的行里,有没有 UnderlyingCode 不是 FR007 的(错误传染方向2)---- if (fr007CorrectId.HasValue) { var crossContaminated = db.eod_commodity_future_price .Where(a => a.UnderlyingId == fr007CorrectId.Value && a.UnderlyingCode != "FR007") .OrderByDescending(a => a.ValueDate) .Take(10) .Select(a => new { a.ValueDate, a.UnderlyingCode, a.UnderlyingId, a.ReferencePrice }) .ToList(); Console.WriteLine($"===== 3. 反向查:UnderlyingId=FR007({fr007CorrectId}) 但 Code≠FR007 的行(错误传染方向2)====="); if (crossContaminated.Count == 0) { Console.WriteLine(" (无)FR007 的 id 没有被别的标的发生的行误用"); } else { Console.WriteLine($" ⚠ 发现 {crossContaminated.Count} 条:这些行占了 FR007 的 id 但 Code 是别的标的"); foreach (var c in crossContaminated) { Console.WriteLine($" {c.ValueDate:yyyy-MM-dd} Code={c.UnderlyingCode} UnderlyingId={c.UnderlyingId} Price={c.ReferencePrice}"); } } } Console.WriteLine(); // ---- 4. EOD 查询路径验证:EodPriceQueryService.TryGetPrice 的查询能否命中 ---- Console.WriteLine("===== 4. EOD 查询路径验证(EodPriceQueryService.TryGetPrice 的实际命中情况)====="); Console.WriteLine(" EOD 按 UnderlyingCode(string) 精确匹配 + ValueDate 精确匹配,不依赖 UnderlyingId。"); Console.WriteLine(" 即:即使 UnderlyingId 错挂,只要 UnderlyingCode='FR007' 且 ValueDate 对得上,EOD 仍能查到。"); Console.WriteLine(" → UnderlyingId 错挂主要影响【网页端展示/JOIN】,不一定影响【EOD 取价】。"); Console.WriteLine(" → 若 EOD 仍取不到价,根因更可能是:日期错位/非重置日/未上传当日值,而非 UnderlyingId 错挂。"); Console.WriteLine(); // ---- 5. 近 7 天 FR007 上传覆盖情况(判断 EOD 取不到是不是因为没上传)---- var recentDates = db.eod_commodity_future_price .Where(a => a.UnderlyingCode == "FR007" && a.ValueDate >= DateTime.Today.AddDays(-10)) .OrderBy(a => a.ValueDate) .Select(a => new { a.ValueDate, a.ReferencePrice }) .ToList(); Console.WriteLine($"===== 5. 近 10 天 FR007 上传覆盖(判断是否漏传导致 EOD 取不到)====="); if (recentDates.Count == 0) { Console.WriteLine(" ⚠⚠ 近 10 天无任何 FR007 上传记录!EOD 复利取价必然失败(或用历史快照)"); } foreach (var d in recentDates) { var weekday = d.ValueDate.DayOfWeek; string wd = weekday == DayOfWeek.Saturday || weekday == DayOfWeek.Sunday ? "周末" : "工作日"; Console.WriteLine($" {d.ValueDate:yyyy-MM-dd}({wd}) FR007={d.ReferencePrice}"); } // ---- 结论判定 ---- Console.WriteLine("\n===== 诊断结论 ====="); if (mismatchCount > 0) { Console.WriteLine(" [确认] FR007 行存在 UnderlyingId 错挂(GLMS-20260701 同类复发)"); Console.WriteLine(" 影响:网页端按 UnderlyingId JOIN 会把 FR007 误挂到别的标的显示"); Console.WriteLine(" 但 EOD 取价走 UnderlyingCode,错挂不直接导致 EOD 取不到价"); } else { Console.WriteLine(" [排除] FR007 行 UnderlyingId 均正确,未复发 GLMS-20260701 事故"); Console.WriteLine(" → 'EOD 没用上 FR007' 更可能是:非重置日(设计)/日期错位/未上传当日值"); } // 断言:错挂数应为 0(若 >0 说明复发) Assert.IsTrue(mismatchCount == 0, $"FR007 有 {mismatchCount} 条价格行 UnderlyingId 错挂(应为 {fr007CorrectId}),GLMS-20260701 事故复发"); } /// /// 诊断 GLMS-JIATT-20260805 复利 EOD 取价日:算出哪些天是重置日、实际查哪天的 FR007 /// 回答"是不是只需要 8/3 一天的价格即可" /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_Trade_FR007_ResetDays() { const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB"; DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); if (td == null) { Assert.Inconclusive($"测试库无 {TradeNumber}"); return; } Console.WriteLine($"===== 交易 {TradeNumber} (id={td.id}) ====="); Console.WriteLine($" StartDate(起息日/tradeDate) = {td.StartDate:yyyy-MM-dd}"); Console.WriteLine(); // 复利腿配置(InterestType=1 复利) var compoundLegs = db.swap_position .Where(p => p.SwapTradeId == td.id && !p.Invalid && p.InterestType == 1) .ToList(); if (compoundLegs.Count == 0) { Console.WriteLine(" ⚠ 该交易无复利腿(InterestType=1),FR007 取价逻辑不适用"); Assert.Inconclusive("无复利腿"); return; } foreach (var leg in compoundLegs) { Console.WriteLine($" 复利腿 PositionId={leg.id} Mode={leg.InterestMode}"); Console.WriteLine($" PosiStartDate={leg.PosiStartDate:yyyy-MM-dd}"); Console.WriteLine($" interest_rest_days(重置周期)={leg.interest_rest_days}"); Console.WriteLine($" interest_rule(日期偏移)={leg.interest_rule}"); Console.WriteLine($" FloatRateUnderlyingCode={leg.FloatRateUnderlyingCode}"); Console.WriteLine($" InterestRateDefault(加点固定利率)={leg.InterestRateDefault}"); Console.WriteLine(); } // 用第一条复利腿的配置算重置日(EOD 用 td.StartDate 算 days,见 SwapDealService.cs:1085,1385) var leg0 = compoundLegs[0]; int interestPeriod = leg0.interest_rest_days ?? 1; int interestRule = leg0.interest_rule ?? 0; DateTime tradeDate = td.StartDate.Value; string floatCode = leg0.FloatRateUnderlyingCode; Console.WriteLine($"===== EOD 复利取价日推算(days=(收盘日-StartDate)%{interestPeriod}==0 才取价)====="); Console.WriteLine($" 公式:fr007RateDate = GetNonHolidayDefore(收盘日 + interest_rule({interestRule}))"); Console.WriteLine($" 法定节假日会回退到前一工作日(GetNonHolidayDefore)"); Console.WriteLine(); // 推算 7/27 ~ 8/7 每天是不是重置日,以及重置日实际查哪天的 FR007 Console.WriteLine($" {"收盘日",-12}{"days",-8}{"重置日?",-10}{"查询日(raw)",-14}{"查询日(节假日回退)",-20}{"FR007有值?"}"); var fr007Dates = db.eod_commodity_future_price .Where(a => a.UnderlyingCode == "FR007" && a.ReferencePrice != null && a.ReferencePrice != 0) .Select(a => a.ValueDate) .ToList(); var fr007Set = new HashSet(fr007Dates); // 简单节假日表(周末;法定节假日用 GetNonHolidayDefore 实际逻辑,这里近似用周末判断) DateTime CalcNonHoliday(DateTime d) { while (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday) d = d.AddDays(-1); return d; } int resetDayCount = 0; for (var d = new DateTime(2026, 7, 27); d <= new DateTime(2026, 8, 7); d = d.AddDays(1)) { int days = (d - tradeDate).Days; bool isReset = days % interestPeriod == 0; if (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday) continue; // EOD 不跑周末 string resetFlag = isReset ? "✓重置日" : "非重置"; if (isReset) resetDayCount++; DateTime rawQueryDate = d.AddDays(interestRule); DateTime actualQueryDate = CalcNonHoliday(rawQueryDate); bool hasFr007 = fr007Set.Contains(actualQueryDate); Console.WriteLine($" {d:yyyy-MM-dd} {days,-8}{resetFlag,-10}{rawQueryDate:yyyy-MM-dd} {actualQueryDate:yyyy-MM-dd} {(hasFr007 ? "✓有值" : "✗缺失")}"); } Console.WriteLine($"\n 小结:7/27~8/7 期间共 {resetDayCount} 个重置日(EOD 只有这些天才查 FR007)"); Console.WriteLine(" → 非重置日根本不查 FR007,沿用上一重置周期的利率,无需每天都有值"); Console.WriteLine(" → 只要【重置日实际查到的那天】有 FR007 值即可,其它天空值不影响 EOD 复利"); } /// /// 逐日对比:EOD 累计 InterestProfitSum(增量)vs 平仓从头重放(全段)—— 定位哪天开始偏差 /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_EOD_vs_Unwind_Compound_DailyCompare() { const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB"; DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); if (td == null) { Assert.Inconclusive($"测试库无 {TradeNumber}"); return; } Console.WriteLine($"===== 交易 {TradeNumber} (id={td.id}) EOD累计 vs 平仓重放 逐日对比 ====="); Console.WriteLine($" StartDate={td.StartDate:yyyy-MM-dd}"); Console.WriteLine(); // 复利腿(取 IsInitial=true 的,其 id 才是 EOD/平仓 PositionId 匹配的 key) var compoundLeg = db.swap_position.FirstOrDefault(p => p.SwapTradeId == td.id && !p.Invalid && p.InterestType == 1 && p.IsInitial); if (compoundLeg == null) { Assert.Inconclusive("无 IsInitial 复利腿"); return; } Console.WriteLine($" 复利腿(初始) id={compoundLeg.id} Mode={compoundLeg.InterestMode} PosiStartDate={compoundLeg.PosiStartDate:yyyy-MM-dd}"); Console.WriteLine($" interest_rest_days={compoundLeg.interest_rest_days} interest_rule={compoundLeg.interest_rule} Rate={compoundLeg.InterestRateDefault}"); Console.WriteLine(); // 读取 EOD 逐日快照(复利腿,按初始腿 id 匹配 PositionId) var eodSeq = db.eod_swap_position .Where(e => e.SwapTradeId == td.id && !e.Invalid && e.PositionId == compoundLeg.id) .OrderBy(e => e.ValueDate) .Select(e => new { e.ValueDate, e.InterestProfitSum, e.TdInterestPrincipal, e.FloatRate, e.InterestIncomeSum, e.TdInterestIncome }) .ToList(); Console.WriteLine($" EOD 快照共 {eodSeq.Count} 天"); Console.WriteLine(); // 平仓从头重放:逐日调 GetUnwindInterests(closePercent=1, 全平) // 注意:平仓返回值 = 从头重放全段利息 - consumedInterest*1(扣历史已结) // EOD InterestProfitSum 是逐日增量累计(不扣 consumedInterest) // 所以两者差 = consumedInterest(历史已结)。重点看"差"是否稳定。 var user = new OptUserInfo(0, nameof(GLMS20260805FR007UnderlyingIdDiagnoseTest), OptUserFrom.UnitTest); var svc = new YLErp.Modules.SwapModule.SwapDealService(user); int interestPeriod = compoundLeg.interest_rest_days ?? 1; int interestRule = compoundLeg.interest_rule ?? 0; DateTime tradeDate0 = td.StartDate.Value; Console.WriteLine($" {"日期",-12}{"days",-6}{"重置?",-8}{"EOD.FloatRate",-14}{"EOD.ProfitSum",-18}{"EOD.TdIntPrin",-16}{"平仓重放",-18}{"差",-14}{"说明"}"); Console.WriteLine($" {new string('-', 118)}"); decimal prevEodSum = 0; decimal prevUnwind = 0; decimal prevEodFloat = 0; for (var d = td.StartDate.Value; d <= new DateTime(2026, 8, 7); d = d.AddDays(1)) { if (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday) continue; int days = (d - tradeDate0).Days; bool isReset = days % interestPeriod == 0; var eod = eodSeq.FirstOrDefault(e => e.ValueDate == d); decimal eodProfitSum = eod?.InterestProfitSum ?? 0; decimal eodTdPrin = eod?.TdInterestPrincipal ?? 0; decimal eodFloat = eod?.FloatRate ?? 0; // 重置日 FR007 切换检查 string floatNote = ""; if (isReset && days != 0 && prevEodFloat != 0 && eodFloat == prevEodFloat) { floatNote = "⚠重置日FR007未切换!"; } // 平仓从头重放(全平 closePercent=1) decimal unwindInterest = 0; string note = ""; try { var interests = svc.GetUnwindInterests(d, d, td.id, 1m, (int)SwapEventTypeEnum.平仓); var compoundResult = interests.FirstOrDefault(x => x.PositionId == compoundLeg.id); unwindInterest = compoundResult?.InterestAmount ?? 0; // 平仓路径取的 FR007(看是否切换) if (isReset && compoundResult != null) { note = $"平仓FloatRate={compoundResult.FloatRate}"; } } catch (Exception ex) { note = $"⚠平仓失败:{ex.Message}"; } decimal diff = unwindInterest - eodProfitSum; string diffNote = Math.Abs(diff) < 0.01m ? "一致" : (Math.Abs(diff) < 1m ? "微小差" : "偏差"); string resetFlag = isReset ? "✓重置" : ""; Console.WriteLine($" {d:yyyy-MM-dd} {days,-6}{resetFlag,-8}{eodFloat,12:F6} {eodProfitSum,16:F6} {eodTdPrin,14:F4} {unwindInterest,16:F6} {diff,12:F6} {diffNote} {floatNote} {note}"); prevEodSum = eodProfitSum; prevUnwind = unwindInterest; prevEodFloat = eodFloat; } Console.WriteLine(); Console.WriteLine($" ===== 解读 ====="); Console.WriteLine($" · 平仓重放 = 从 PosiStartDate 到当日全段复利利息 - consumedInterest(历史互换已结)"); Console.WriteLine($" · EOD ProfitSum = 逐日增量累计(preEod.ProfitSum + 当天新计)"); Console.WriteLine($" · 两者差应≈consumedInterest(若有历史互换)。若差值不稳定/突变 → 某天 EOD 增量算错"); Console.WriteLine($" · 重点看 FloatRate 列:EOD 用的浮动利率是否在重置日正确切换、非重置日是否正确沿用"); } /// /// 深挖复利腿(38122)在 8/4 互换前后发生了什么:flow_event + EOD 全字段 /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_Trade_CompoundLeg_AroundSwap() { const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB"; const long CompoundPositionId = 38122; DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); if (td == null) { Assert.Inconclusive($"测试库无 {TradeNumber}"); return; } // 1. 复利腿所有 flow_event(看 8/4 互换对它做了什么) Console.WriteLine($"===== 复利腿 PositionId={CompoundPositionId} 所有 flow_event ====="); var flows = db.swap_flow_event .Where(f => f.SwapTradeId == td.id && f.PositionId == CompoundPositionId) .OrderBy(f => f.EventDate).ThenBy(f => f.id) .Select(f => new { f.EventDate, f.UnwindDate, f.EventType, f.InterestMode, f.InterestAmount, f.InterestPrincipal, f.FloatRate, f.InterestRate, f.DataState, f.Quantity, f.PositionQty }) .Take(30) .ToList(); Console.WriteLine($" 共 {flows.Count} 条"); Console.WriteLine($" {"EventDate",-12}{"UnwindDate",-12}{"Type",-6}{"DataState",-10}{"I.Amount",-16}{"I.Principal",-18}{"FloatRate",-12}{"Rate"}"); foreach (var f in flows) { string typeStr = f.EventType == 2 ? "平仓" : f.EventType == 3 ? "互换" : f.EventType == 4 ? "自动" : f.EventType.ToString(); Console.WriteLine($" {f.EventDate:yyyy-MM-dd} {f.UnwindDate:yyyy-MM-dd} {typeStr,-6}{f.DataState,-10}{f.InterestAmount,14:F6} {f.InterestPrincipal,16:F4} {f.FloatRate,10:F6} {f.InterestRate}"); } Console.WriteLine(); // 2. 8/3~8/6 EOD 全字段(看 8/4 互换后状态怎么变的) Console.WriteLine($"===== PositionId={CompoundPositionId} 8/3~8/6 EOD 全字段 ====="); var eods = db.eod_swap_position .Where(e => e.SwapTradeId == td.id && e.PositionId == CompoundPositionId && e.ValueDate >= new DateTime(2026, 8, 3) && e.ValueDate <= new DateTime(2026, 8, 6)) .OrderBy(e => e.ValueDate) .Select(e => new { e.ValueDate, e.InterestProfitSum, e.TdInterestPrincipal, e.FloatRate, e.InterestIncomeSum, e.TdInterestIncome, e.TdCloseInterest, e.RealizedInterest, e.InterestRateDefault, e.PosiStatus }) .ToList(); Console.WriteLine($" {"日期",-12}{"ProfitSum",-16}{"TdIntPrin",-16}{"FloatRate",-12}{"IncomeSum",-16}{"TdIncome",-14}{"TdCloseInt",-14}{"RealizedInt",-14}{"PosiStatus"}"); foreach (var e in eods) { Console.WriteLine($" {e.ValueDate:yyyy-MM-dd} {e.InterestProfitSum,14:F6} {e.TdInterestPrincipal,14:F4} {e.FloatRate,10:F6} {e.InterestIncomeSum,14:F6} {e.TdInterestIncome,12:F6} {e.TdCloseInterest,12:F6} {e.RealizedInterest,12:F6} {e.PosiStatus}"); } Console.WriteLine(); // 3. 看 8/4 是否有 swap_event(互换事件记录) Console.WriteLine($"===== 8/3~8/5 的 swap_event(看有无互换操作)====="); var events = db.swap_event .Where(s => s.SwapTradeId == td.id && !s.Invalid && s.ValueDate >= new DateTime(2026, 8, 3) && s.ValueDate <= new DateTime(2026, 8, 5)) .OrderBy(s => s.ValueDate) .Select(s => new { s.id, s.ValueDate, s.EventType, s.EventReason, s.Invalid }) .ToList(); foreach (var s in events) { string typeStr = s.EventType == 2 ? "平仓" : s.EventType == 3 ? "互换" : s.EventType.ToString(); Console.WriteLine($" id={s.id} {s.ValueDate:yyyy-MM-dd} Type={typeStr} Reason={s.EventReason} Invalid={s.Invalid}"); } if (events.Count == 0) Console.WriteLine(" (8/3~8/5 无 swap_event)"); } /// /// 精确诊断:8/4 重置日为何取到 FR007=0.0123 而非 0.0213 /// 复刻 CalcDailyCompoundInterest 循环 + GetFloatRate 逻辑,逐 i 打印 floatRate 演变 /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_Trade_804_ResetDay_FloatRate_Trace() { const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB"; DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); if (td == null) { Assert.Inconclusive($"测试库无 {TradeNumber}"); return; } var te = db.trade_extend.FirstOrDefault(x => x.TradeId == td.id); // InterestCalcMode string calcMode = "?"; int annualDays = 365; if (te != null && !string.IsNullOrEmpty(te.ExtendJson)) { try { dynamic ext = Newtonsoft.Json.JsonConvert.DeserializeObject(te.ExtendJson); calcMode = (string)ext.InterestCalcMode ?? "?"; annualDays = (int?)ext.AnnualDays ?? 365; } catch { } } bool calcFirst = calcMode.StartsWith("1"); bool calcLast = calcMode.EndsWith("1"); Console.WriteLine($"===== InterestCalcMode 诊断 ====="); Console.WriteLine($" InterestCalcMode = '{calcMode}' → calcFirst={calcFirst} calcLast={calcLast}"); Console.WriteLine(); var compoundLeg = db.swap_position.FirstOrDefault(p => p.SwapTradeId == td.id && !p.Invalid && p.InterestType == 1 && p.IsInitial); if (compoundLeg == null) { Assert.Inconclusive("无复利腿"); return; } int period = compoundLeg.interest_rest_days ?? 1; int rule = compoundLeg.interest_rule ?? 0; DateTime startDate = compoundLeg.PosiStartDate; // 直接调生产代码 GetUnwindInterests(8/4 全平),看返回的 FloatRate DateTime endDate = new DateTime(2026, 8, 4); Console.WriteLine($"===== 1. 调 GetUnwindInterests(8/4) 看复利腿返回的 FloatRate ====="); Console.WriteLine($" PosiStartDate={startDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} period={period} rule={rule}"); Console.WriteLine($" (endDate - PosiStartDate).Days = {(endDate - startDate).Days}, %period = {(endDate - startDate).Days % period}(==0 即重置日)"); Console.WriteLine($" (endDate - td.StartDate).Days = {(endDate - td.StartDate.Value).Days}(GetFloatRate 用这个判重置日)"); Console.WriteLine(); var user = new OptUserInfo(0, nameof(GLMS20260805FR007UnderlyingIdDiagnoseTest), OptUserFrom.UnitTest); var svc = new YLErp.Modules.SwapModule.SwapDealService(user); try { var interests = svc.GetUnwindInterests(endDate, endDate, td.id, 1m, (int)SwapEventTypeEnum.平仓); var compoundResult = interests.FirstOrDefault(x => x.PositionId == compoundLeg.id); if (compoundResult != null) { Console.WriteLine($" ✓ 平仓返回:InterestAmount={compoundResult.InterestAmount:F6} FloatRate={compoundResult.FloatRate:F6} InterestRate={compoundResult.InterestRate}"); Console.WriteLine($" 若 FloatRate≈0.0123 → 取到的是 7/27 旧值(重置日未生效)"); Console.WriteLine($" 若 FloatRate≈0.0213 → 取到的是 8/3 新值(重置日生效,正常)"); } } catch (Exception ex) { Console.WriteLine($" ⚠ 平仓调用失败:{ex.Message}"); } Console.WriteLine(); // 2. 直接验证 FR007 在关键日期的值(EodPriceQueryService.TryGetPrice) Console.WriteLine($"===== 2. FR007 在关键日期的实际值(EodPriceQueryService.TryGetPrice)====="); var checkDates = new[] { ("7/27(首重置日查询日)", new DateTime(2026,7,27)), ("8/3(8/4重置日应查的日期, interest_rule=-1)", new DateTime(2026,8,3)), ("8/4(直接查)", new DateTime(2026,8,4)), }; foreach (var (label, dt2) in checkDates) { bool ok = YLErp.Modules.DataProviderModule.EodPriceQueryService.TryGetPrice(dt2, "FR007", out double v); Console.WriteLine($" {label} {dt2:yyyy-MM-dd}: {(ok ? $"{v:F6}" : "✗查不到")}"); } Console.WriteLine(); // 3. 结论判定 Console.WriteLine($"===== 3. 结论判定 ====="); Console.WriteLine($" calcLast={calcLast}(InterestCalcMode='{calcMode}' EndsWith('1'))"); Console.WriteLine($" 8/4 平仓:endDate=8/4 是重置日((8/4-7/28).Days=7, 7%7=0)"); if (!calcLast) { Console.WriteLine($" ⚠ calcLast=false:CalcDailyCompoundInterest 循环里 i=7(accrueDate=8/4=endDate) 命中"); Console.WriteLine($" 'if(!calcLast && accrueDate==endDate) continue' → 被跳过,不进重置日取价分支"); Console.WriteLine($" → 8/4 重置日不取新 FR007,沿用循环里 i=0(7/28)取到的旧值 0.0123"); Console.WriteLine($" → 这就是根因:算尾规则(calcLast)导致重置日=平仓日时跳过取价"); } else { Console.WriteLine($" calcLast=true:8/4 不会被跳过,应能取到新 FR007(0.0213)。"); Console.WriteLine($" 若平仓返回的 FloatRate 仍是 0.0123 → 根因在别处(需进一步查 GetFloatRate/循环覆盖)"); } } /// /// 查这笔交易所有腿 + EOD 快照的 PositionId 映射,搞清哪条腿真正算利息 /// [TestMethod] [TestCategory("DbDiagnose")] public void Diagnose_Trade_AllLegs_And_EodMapping() { const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB"; DbDiagnoseGuard.RequireTestDb(); YLContext db; try { db = DbContextFactory.GetYLDbContext(); } catch (Exception ex) { Assert.Inconclusive($"无法连接测试库:{ex.Message}"); return; } var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber); if (td == null) { Assert.Inconclusive($"测试库无 {TradeNumber}"); return; } Console.WriteLine($"===== 交易 {TradeNumber} (id={td.id}) 全部持仓腿 ====="); var allLegs = db.swap_position .Where(p => p.SwapTradeId == td.id && !p.Invalid) .OrderBy(p => p.PosiDirection).ThenBy(p => p.IsInitial).ThenBy(p => p.id) .Select(p => new { p.id, p.PositionId, p.IsInitial, p.InterestMode, p.InterestType, p.PosiDirection, p.InterestPrincipalFix, p.PosiNotionalValue, p.PosiStartDate, p.FloatRateUnderlyingCode }) .ToList(); Console.WriteLine($" {"id",-8}{"PositionId",-12}{"IsInit",-8}{"Mode",-6}{"IntType",-8}{"PosiDir",-8}{"Fix",-16}{"PosiNotional",-16}{"PosiStart",-12}{"FloatCode"}"); foreach (var p in allLegs) { Console.WriteLine($" {p.id,-8}{p.PositionId,-12}{p.IsInitial,-8}{p.InterestMode,-6}{p.InterestType,-8}{p.PosiDirection,-8}{p.InterestPrincipalFix,-16}{p.PosiNotionalValue,-16}{p.PosiStartDate:yyyy-MM-dd} {p.FloatRateUnderlyingCode}"); } Console.WriteLine(); // 各腿对应的 EOD 快照数量 Console.WriteLine($"===== 各腿 EOD 快照数量(eod_swap_position)====="); var eodCounts = db.eod_swap_position .Where(e => e.SwapTradeId == td.id && !e.Invalid) .GroupBy(e => e.PositionId) .Select(g => new { PositionId = g.Key, Cnt = g.Count(), MinDate = g.Min(x => x.ValueDate), MaxDate = g.Max(x => x.ValueDate) }) .ToList(); foreach (var c in eodCounts) { Console.WriteLine($" PositionId={c.PositionId} 快照数={c.Cnt} 日期范围={c.MinDate:yyyy-MM-dd}~{c.MaxDate:yyyy-MM-dd}"); } Console.WriteLine(); // 复利腿(Intertype=1)逐日 EOD 明细(所有 PositionId) Console.WriteLine($"===== 复利腿(InterestType=1) EOD 逐日明细(所有 PositionId)====="); var compoundEods = db.eod_swap_position .Where(e => e.SwapTradeId == td.id && !e.Invalid && e.InterestType == 1) .OrderBy(e => e.ValueDate).ThenBy(e => e.PositionId) .Select(e => new { e.ValueDate, e.PositionId, e.InterestProfitSum, e.TdInterestPrincipal, e.FloatRate, e.InterestRateDefault, e.InterestMode }) .Take(40) .ToList(); Console.WriteLine($" 共 {compoundEods.Count} 条"); Console.WriteLine($" {"日期",-12}{"PositionId",-12}{"Mode",-6}{"ProfitSum",-18}{"TdIntPrin",-16}{"FloatRate",-12}{"RateDefault"}"); foreach (var e in compoundEods) { Console.WriteLine($" {e.ValueDate:yyyy-MM-dd} {e.PositionId,-12}{e.InterestMode,-6}{e.InterestProfitSum,16:F6} {e.TdInterestPrincipal,14:F4} {e.FloatRate,10:F6} {e.InterestRateDefault}"); } } } }