互换三个bug的TDD红灯/绿灯测试

单利利息偏大(红灯): Step1_SimpleInterestRedTest 用1813坐实
单利路径(CalcDailySimpleInterest)未加consumedInterest扣除,
默认值偏大8271(历史已结利息未扣)。复利路径c6adb3bb已修。

审核状态卡死(红灯): SwapApproveStatusStuckTest 3个方法
坐实28/28互换事件CloseMethod=0走else分支(无ExerciseDate判断),
当前3笔交易实际卡死(到期+确认成交)。Step2分析副作用:Notional清零
需放cs:1577之后避免被td.Notional=td.TradeAmount覆盖。

重收盘误删(绿灯确认): Step1加修复后逻辑模拟,确认排除manualClientCashIds
后手动互换记录不再被命中(78751f0a已修)。
This commit is contained in:
hjhan
2026-07-01 12:22:57 +08:00
parent 3b610fe47c
commit 3118c44365
3 changed files with 473 additions and 3 deletions
@@ -99,6 +99,192 @@ namespace YLErp.Modules.SwapModule
}
}
/// <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} {"",14} {"eod待实现IPS",14} {"CI",14} {"(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);
// 应计基数 = 待实现 - 已结(这才是正确的"未实现利息")
decimal expected = ips - ci;
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();
Console.WriteLine($" {d.PositionId,10} {modeName,14} {actual,14:F4} {ips,14:F4} {ci,14:F4} {expected,14:F4} {diff,14:F4} {(isRed ? "" : "绿"),6}");
}
Console.WriteLine($"\n[结论]");
if (redCount > 0)
{
Console.WriteLine($" ⚠ 坐实单利路径 bug{redCount} 条单利腿默认值偏大(含历史已结利息)。");
Console.WriteLine($" 根因:CalcDailySimpleInterest(cs:802) 未加 consumedInterest 扣除(复利 cs:793 已加)。");
}
else
{
Console.WriteLine($" 单利路径未检测到偏大(可能已修或样本无历史互换)。");
}
// 红灯断言:单利路径应存在偏大
Assert.IsTrue(redCount > 0,
"红灯:单利路径应存在默认值偏大(含历史已结利息)。修复后此断言应反转。");
}
finally
{
db?.Dispose();
}
}
/// <summary>
/// 单利红灯样本交易ID。从 Step0e 选"标的期初全价+单利+有历史互换"的交易。
/// </summary>
private int SimpleInterestSampleTradeId => 1813;
/// <summary>
/// Step0b:对单笔交易做详细诊断——对比"待实现"vs"已实现"利息,判断默认值是否重复计入。
///
@@ -395,6 +581,13 @@ namespace YLErp.Modules.SwapModule
// 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[] {