test(swap): 扩展DealInterests测试-多次互换守恒+预付金ratio翻转

新增2个测试场景:
- DI_SWAP_MULTI_001: 攒10天→互换结清→再攒5天→再互换结清
  守恒约束: RealizedInterest+InterestIncomeSum=全程利息(17天)
  验证多次互换结算后利息不丢失/不重复
- DI_MARGIN_001: 预付金腿(初始预付金,marginTypes)互换结清后
  RealizedInterest应为负(ratio翻转:收取→-1→支付方向)
  验证cs:789-793的marginTypes ratio翻转逻辑

验证: 89(T0/T1)+7(DealInterests)=96全通过,无回归。
This commit is contained in:
hjhan
2026-07-01 18:25:44 +08:00
parent 775d7a5881
commit be88442c91
@@ -422,6 +422,174 @@ namespace YLErp.Modules.SwapModule
Console.WriteLine($"连续5天归档:InterestIncomeSum 从0递增到{runningIncomeSum:F6} = 5×{DailyInterest:F6} ✅");
}
// ================================================================
// 场景4:互换→收盘→再攒→再互换 守恒验证
// ================================================================
#region 4
/// <summary>
/// [DI_SWAP_MULTI_001] 攒10天→互换结清→再攒5天→再互换结清
/// ---------------------------------------------------------------
/// 验证:第一次互换后 InterestIncomeSum≈当天新计(攒的10天付了),
/// 再攒5天后 InterestIncomeSum≈6天(5天新攒+1天当天新计),
/// 第二次互换后 InterestIncomeSum≈当天新计(攒的6天又付了)。
///
/// 守恒约束:两次互换结算的 TdCloseInterest 之和 = 全程利息(15天+2天新计)。
/// </summary>
[TestMethod]
public void DI_SWAP_MULTI_001_多次互换结算守恒()
{
var td = CreateTrade();
var position = CreateInterestPosition();
// --- Phase 1: 攒10天(4/27~5/6),到5/6 ---
var date10 = StartDate.AddDays(10); // 5/7
decimal sum10days = Math.Round(Principal * FixedRate * 10 / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var preEod10 = CreatePreEod(date10.AddDays(-1), sum10days - DailyInterest); // 前日=9天
// 当天新计让它到10天
var svc1 = new StubEodPositionService();
svc1.ExecuteDealInterests(new List<swap_position> { position },
new List<eod_swap_position> { preEod10 }, date10, td,
new List<swap_flow_event>(), Principal, 0m, 0m, 1m, Principal);
var eod10days = svc1.PersistedPositions[0];
AssertDecimal(sum10days, eod10days.InterestIncomeSum, "10天后待实现应=10天利息");
Console.WriteLine($"Phase1: 攒10天 InterestIncomeSum={eod10days.InterestIncomeSum:F6}");
// --- Phase 2: 5/7 互换结清 ---
var swapDate1 = date10; // 同天互换
var svc2 = new StubEodPositionService();
var swapEvt1 = CreateSwapFlowEvent(swapDate1, sum10days);
var swapResult1 = svc2.ExecuteSaveEodInterestPosition(
eod10days, null, position, td, swapDate1, new List<swap_flow_event> { swapEvt1 });
// 互换后待实现≈当天新计(攒的10天付了,但当天又产生1天新计)
decimal dailyInc = Math.Round(Principal * FixedRate / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
AssertDecimal(dailyInc, swapResult1.InterestIncomeSum, "第一次互换后待实现≈当天新计");
decimal firstRealized = swapResult1.TdCloseInterest;
Console.WriteLine($"Phase2: 第一次互换 TdCloseInterest={firstRealized:F6}, 待实现={swapResult1.InterestIncomeSum:F6}");
// --- Phase 3: 再攒5天 ---
decimal runningSum = swapResult1.InterestIncomeSum;
var runningDate = swapDate1.AddDays(1);
for (int i = 0; i < 5; i++)
{
var svc = new StubEodPositionService();
var preEod = CreatePreEod(runningDate.AddDays(-1), runningSum);
// 需要 preEod.RealizedInterest 累积
preEod.RealizedInterest = swapResult1.RealizedInterest;
svc.ExecuteDealInterests(new List<swap_position> { position },
new List<eod_swap_position> { preEod }, runningDate, td,
new List<swap_flow_event>(), Principal, 0m, 0m, 1m, Principal);
runningSum = svc.PersistedPositions[0].InterestIncomeSum;
runningDate = runningDate.AddDays(1);
}
Console.WriteLine($"Phase3: 再攒5天后 InterestIncomeSum={runningSum:F6}");
// --- Phase 4: 再互换结清 ---
var svc4 = new StubEodPositionService();
var preEodFinal = CreatePreEod(runningDate.AddDays(-1), runningSum);
preEodFinal.RealizedInterest = swapResult1.RealizedInterest;
var swapEvt2 = CreateSwapFlowEvent(runningDate, runningSum);
var swapResult2 = svc4.ExecuteSaveEodInterestPosition(
preEodFinal, null, position, td, runningDate, new List<swap_flow_event> { swapEvt2 });
decimal secondRealized = swapResult2.TdCloseInterest;
Console.WriteLine($"Phase4: 第二次互换 TdCloseInterest={secondRealized:F6}, 待实现={swapResult2.InterestIncomeSum:F6}");
// 守恒:两次互换实现的 + 最终待实现 = 全程天数 × dailyInc
// 全程天数 = 10天(Phase1) + 1天(第一次互换当天新计) + 5天(Phase3) + 1天(第二次互换当天新计) = 17天
// 但第一次互换的当天新计进了 InterestIncomeSum 没进 TdCloseInterest
// 第二次互换同理。所以守恒 = RealizedInterest合计 + 最终InterestIncomeSum = 全程利息
decimal totalDays = 10 + 1 + 5 + 1; // 17天
decimal expectedTotalInterest = Math.Round(Principal * FixedRate * totalDays / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
decimal actualTotal = swapResult2.RealizedInterest + swapResult2.InterestIncomeSum;
Console.WriteLine($"守恒: RealizedInterest({swapResult2.RealizedInterest:F6}) + InterestIncomeSum({swapResult2.InterestIncomeSum:F6}) = {actualTotal:F6}");
Console.WriteLine($"期望: {totalDays}天 × {dailyInc:F6} = {expectedTotalInterest:F6}");
AssertDecimal(expectedTotalInterest, actualTotal,
"已实现+待实现 应=全程利息(守恒)");
}
#endregion
// ================================================================
// 场景5:预付金腿(marginTypes ratio 翻转)符号验证
// ================================================================
#region 5 ratio
/// <summary>
/// [DI_MARGIN_001] 预付金腿互换结清后 RealizedInterest 应为负(支付方向)
/// ---------------------------------------------------------------
/// 预付金腿 InterestDirection=收取(1),但 marginTypes 会把 ratio 翻转为 -1。
/// SwapPositionValue 应为负(负债),RealizedInterest 也应为负(券商支付)。
/// 验证 cs:789-793 的 ratio 翻转逻辑。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_MARGIN_001_预付金腿RealizedInterest为负()
{
var service = new StubEodPositionService();
var td = CreateTrade();
var settleDate = new DateTime(2026, 5, 10);
// 预付金腿(初始预付金 InterestMode=5InterestDirection=收取)
var marginPosition = new swap_position
{
id = 2001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum., InterestMode = (int)InterestModeEnum.,
InterestRateDefault = 0.005m, InterestPrincipalFix = 500m,
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
FloatRateUnderlyingCode = null,
InterestSwapInterval = null
};
// 攒10天的预付金利息
decimal marginDaily = Math.Round(500m * 0.005m / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
decimal margin10days = marginDaily * 10;
var preEod = new eod_swap_position
{
id = 200, SwapTradeId = 1, PositionId = 2001, ValueDate = settleDate.AddDays(-1),
ClientId = 999998, InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestIncomeSum = margin10days, InterestProfitSum = margin10days,
RealizedInterest = 0m, InterestRateDefault = 0.005m,
TdInterestPrincipal = 500m, PosiNotionalValue = 500m,
InterestType = (int)InterestTypeEnum., IsAnnualized = true,
interest_rest_days = 1, FloatRate = 0m
};
// 互换结清
var swapEvent = new swap_flow_event
{
id = 3001, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.,
EventDate = settleDate, UnwindDate = settleDate, PositionId = 2001,
InterestDirection = (int)SwapDirectionEnum.,
InterestAmount = margin10days, InterestClosePnL = -margin10days, // 预付金 ratio 翻转后为负
InterestRate = 0.005m, InterestMode = (int)InterestModeEnum.,
InterestPrincipal = 500m, FloatRate = 0m,
DataState = (int)SwapFlowDateStateEnum.
};
var result = service.ExecuteSaveEodInterestPosition(
preEod, null, marginPosition, td, settleDate, new List<swap_flow_event> { swapEvent });
// 预付金 marginTypes 翻转 ratio=-1
// RealizedInterest = 0 + TdCloseInterest(margin10days) * ratio(-1) = -margin10days
AssertDecimal(-margin10days, result.RealizedInterest,
"预付金腿 RealizedInterest 应为负(ratio翻转后支付方向)");
Console.WriteLine($"预付金腿 RealizedInterest={result.RealizedInterest:F6}(负=支付)✅");
Console.WriteLine($"SwapPositionValue={result.SwapPositionValue:F6}(应≈当天新计×ratio=-正)");
}
#endregion
#endregion
}
}