Files
zszq-trs/UnitTestProject/Modules/EodModule/GLMS20260805FR007UnderlyingIdDiagnoseTest.cs
T
hjhan 66a97e03ef fix(swap): 复利重置日=平仓日时 calcLast 不再跳过 FR007 取价
根因(GLMS-JIATT-20260805):InterestCalcMode='10'(算头不算尾,calcLast=false)时,
CalcDailyCompoundInterest 循环的 'if(!calcLast && accrueDate==endDate) continue'
会跳过平仓日。若平仓日恰好是重置日(i%period==0),取价代码块被一并跳过,
导致 flowEvent.FloatRate 落库为旧周期利率,传染后续 EOD 复利计算。

修复:把重置日的 FR007 取价提前到 calcFirst/calcLast 跳过判断之前——
calcLast 只应跳过'计息',不应跳过'重置日利率取价'。同时循环外用最终
floatRate 兜底赋值 flowEvent.FloatRate,确保落库值反映最后重置日的利率。

验证:
- CI_007 合成测试(平仓日=重置日):修复前 FloatRate=旧值(FAIL),修复后=新值(PASS)
- 连库验证(GLMS-JIATT-20260805 8/4平仓):FloatRate 0.0123→0.0213(8/3新值)
- 利息金额不变(calcLast 不计当天利息,Amount 不受影响,只修正 FloatRate 字段)
- 全套 swap 测试无新增回归(334通过,2失败均为pre-existing单利/EOD路径)

附:CI_007 复现测试 + GLMS20260805 FR007/EOD 诊断工具 + 文档 longRatio 状态更新
2026-08-07 20:43:16 +08:00

582 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using YLErp.BLL;
using YLErp.DBModels;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 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。
/// </summary>
[TestClass]
public class GLMS20260805FR007UnderlyingIdDiagnoseTest
{
/// <summary>
/// 诊断 FR007 价格行的 UnderlyingId 是否错挂(GLMS-20260701 同类复发判定)
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_FR007_UnderlyingIdMismatch()
{
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 事故复发");
}
/// <summary>
/// 诊断 GLMS-JIATT-20260805 复利 EOD 取价日:算出哪些天是重置日、实际查哪天的 FR007
/// 回答"是不是只需要 8/3 一天的价格即可"
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_Trade_FR007_ResetDays()
{
const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB";
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<DateTime>(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 复利");
}
/// <summary>
/// 逐日对比:EOD 累计 InterestProfitSum(增量)vs 平仓从头重放(全段)—— 定位哪天开始偏差
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_EOD_vs_Unwind_Compound_DailyCompare()
{
const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB";
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 用的浮动利率是否在重置日正确切换、非重置日是否正确沿用");
}
/// <summary>
/// 深挖复利腿(38122)在 8/4 互换前后发生了什么:flow_event + EOD 全字段
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_Trade_CompoundLeg_AroundSwap()
{
const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB";
const long CompoundPositionId = 38122;
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");
}
/// <summary>
/// 精确诊断:8/4 重置日为何取到 FR007=0.0123 而非 0.0213
/// 复刻 CalcDailyCompoundInterest 循环 + GetFloatRate 逻辑,逐 i 打印 floatRate 演变
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_Trade_804_ResetDay_FloatRate_Trace()
{
const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB";
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=falseCalcDailyCompoundInterest 循环里 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=true8/4 不会被跳过,应能取到新 FR007(0.0213)。");
Console.WriteLine($" 若平仓返回的 FloatRate 仍是 0.0123 → 根因在别处(需进一步查 GetFloatRate/循环覆盖)");
}
}
/// <summary>
/// 查这笔交易所有腿 + EOD 快照的 PositionId 映射,搞清哪条腿真正算利息
/// </summary>
[TestMethod]
[TestCategory("DbDiagnose")]
public void Diagnose_Trade_AllLegs_And_EodMapping()
{
const string TradeNumber = "GLMS-JIATT-20260805-FICC-01-2180120IB";
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}");
}
}
}
}