Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs
hjhan f7d23dc132 revert: 撤销单利consumedInterest扣除(双重扣减导致1天利息变0)
单利起点InterestProfitSum已含历史累计(eod快照),
再减consumedInterest会双重扣减。复利起点是0(从头算),
减consumedInterest正确。两者计算结构不同不能套用。

正确方向:让InterestProfitSum在互换结清后归零(eod层方案B),
而非在计算时减consumedInterest。待重新设计单利修复方案。

测试还原为红灯(Assert redCount>0),注释更新说明正确方向。
2026-07-01 13:38:24 +08:00

654 lines
36 KiB
C#
Raw Permalink 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换部分平仓后利息端/预付金默认盈亏偏大 - 录制/验证测试(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
};
/// <summary>
/// Step0:探查测试库,列出有"多次平仓/互换操作"的互换交易,供挑选样本。
///
/// 复现条件:一笔交易 swap_flow_event 里 EventType IN(平仓,互换,自动互换) 且 DataState=完成
/// 的记录 ≥ 2 条(说明做过多次操作),且有 eod_swap_position(已收盘)。
/// </summary>
[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();
}
}
/// <summary>
/// Step0e:探查"单利 + 有互换历史"的样本,用于验证单利路径是否也需要 consumedInterest 扣除。
///
/// 复利路径(c6adb3bb)已修,单利路径(CalcDailySimpleInterest)未修。
/// 需找:单利利息腿 + 该腿有历史互换/自动互换事件(InterestAmount≠0) + 有eod。
/// </summary>
[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();
}
}
/// <summary>
/// Step1_SimpleInterestRedTest:单利路径红灯测试。
///
/// 复利路径已由 c6adb3bb 修复(consumedInterest 扣除),但单利路径(CalcDailySimpleInterest)
/// 未加该扣除。本测试坐实:单利利息腿在"有历史互换结清后再平仓"时,默认值仍偏大。
///
/// 红灯(当前):默认值包含历史已结利息(consumedInterest),偏大
/// 绿灯(修复后):单利路径也扣除 consumedInterest,默认值正确
/// </summary>
[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();
}
}
/// <summary>
/// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。
/// </summary>
private int SimpleInterestSampleTradeId => 1813;
/// <summary>
/// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。
///
/// 核心逻辑(不改数据,纯查询):
/// - 默认值计算读 InterestProfitSum(待实现),不读 RealizedInterest(已实现)
/// - 若某持仓 InterestProfitSum >> 0 且已有多次操作(RealizedInterest >> 0),
/// 说明下次平仓默认值会基于"全程待实现"重算,重复计入已实现部分
/// - 真正应计基数 = InterestProfitSum - RealizedInterest(剩余未实现)
/// </summary>
[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();
}
}
/// <summary>
/// 样本交易ID。1889 = GLMS-20260616-000429号收益结算+收盘,30号平仓。
/// </summary>
private int SampleTradeId => 1889;
/// <summary>
/// Step0c:精确诊断——调用真实的 GetUnwindInterests 拿默认值,对比 eod 应计,定位偏差。
///
/// 这是最直接的验证:用平仓日的参数调 GetUnwindInterests(与前端拿默认值完全相同的路径),
/// 看返回的 InterestClosePnL 是否包含了"之前已通过互换实现的部分"。
/// </summary>
[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();
}
}
/// <summary>
/// Step0d:针对 1889GLMS-20260616-0004)的全面诊断。
///
/// 场景:29号收益结算(互换)+收盘 → 30号平仓。
/// 测试环境会不断回退复用同一笔交易,需甄别。
///
/// 本方法一次性查清:
/// 1. swap_event 全历史(含回退 EventType=5),甄别哪些是回退后的有效操作
/// 2. swap_flow_event 全历史(含 DataState≠完成的废弃事件)
/// 3. eod_swap_position 按日期序列,看 InterestProfitSum/RealizedInterest 逐日演变
/// 4. 调 GetUnwindInterests 拿30号平仓默认值,对比29号互换已实现的部分
/// </summary>
[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();
}
}
}
}