互换三个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:
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user