Files
zszq-trs/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
T
hjhan be88442c91 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全通过,无回归。
2026-07-01 18:25:44 +08:00

596 lines
32 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 Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// DealInterests 利息腿归档 - 合成单元测试(内存,不连库)
/// ============================================================================
/// 目标:验证收盘时利息腿 eod 的字段计算,覆盖三个分支:
/// ① 手动互换分支 SaveEodInterestPosition(我们修复 InterestIncomeSum 归零的核心)
/// ② 普通日分支 SaveEodInterestPositionCopyInterestIncomeSum 每日递增)
/// ③ 多日守恒(半平后多日再全平,利息一致性)
///
/// 模仿 GetInterestsUnitTest_T0 的风格:
/// - 继承生产类,override 虚方法替换 DB 调用
/// - 内存构造 trade/position/eod/flowEvent 数据
/// - 断言业务期望值(独立计算,非循环论证)
/// ============================================================================
[TestClass]
public class DealInterestsScenarioTest
{
#region 测试常量
private const decimal Principal = 1000m;
private const decimal FixedRate = 0.01m;
private const int AnnualDays = 365;
private static readonly DateTime StartDate = new(2026, 4, 27);
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
/// <summary>每天利息(固定利率,算头不算尾,年化365天)</summary>
private static decimal DailyInterest => Math.Round(Principal * FixedRate / AnnualDays, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
#endregion
#region Stub:内存 SwapEodPositionService
/// <summary>
/// 测试用子类:override 虚方法,把 DB 调用替换为内存操作。
/// - PersistEodSwapPosition:收集到列表而非写库
/// - GetCurrencyRate:返回 1.0(本币)
/// </summary>
private sealed class StubEodPositionService : SwapEodPositionService
{
public List<eod_swap_position> PersistedPositions { get; } = new();
public StubEodPositionService() : base(new OptUserInfo(0, nameof(DealInterestsScenarioTest), OptUserFrom.UnitTest))
{
}
protected override void PersistEodSwapPosition(eod_swap_position position)
{
// 收集到列表,不写库。如果 id=0 模拟新增。
if (position.id == 0) position.id = PersistedPositions.Count + 1;
PersistedPositions.Add(position);
}
protected override void SaveAllChanges()
{
// 不做任何事(内存模式)
}
protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType)
{
return 1.0; // 本币,汇率=1
}
// override CalcSwapInterests:用真实 SwapDealService 算(固定利率不需 mock 浮动利率)
// 生产代码默认实现也是 new SwapDealService(this).GetInterests(...),这里保持一致
// 但 SwapDealService 内部 TryGetFloatRate 会连库——固定利率(FloatRateUnderlyingCode=null)不会触发
protected override List<swap_flow_event> CalcSwapInterests(
trade td, trade_extend tradeExtend,
DateTime valueDate, DateTime unwindDate,
List<eod_swap_position> eodPositions, List<swap_position> positions,
decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue,
decimal closePosiNotionalValue, decimal closePrecent,
int eventType, bool tdClose, bool needPrice,
decimal grossPrice, decimal orginPv,
bool add = false, bool settment = true, bool newCalcLast = false,
List<swap_flow_event> closeList = null)
{
return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
grossPrice, orginPv, add, settment, newCalcLast, closeList);
}
// public 包装:让测试能调用 protected 方法
public eod_swap_position ExecuteSaveEodInterestPosition(
eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition,
swap_position position, trade td, DateTime valueDate, List<swap_flow_event> flowEvents)
{
SaveEodInterestPosition(eodPayPosition, newEodPayPosition, position, td, valueDate, flowEvents);
return PersistedPositions.LastOrDefault();
}
// public 包装:调用 DealInterests(通过反射,因为参数太多不好包)
public void ExecuteDealInterests(
List<swap_position> interestList, List<eod_swap_position> eodPositions,
DateTime settleDate, trade td, List<swap_flow_event> flowEvents,
decimal posiLongNational, decimal posiShortNational,
decimal closeNational, decimal grossPrice, decimal orginPv)
{
var method = typeof(SwapEodPositionService).GetMethod("DealInterests",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
method.Invoke(this, new object[]
{
interestList, eodPositions, new List<eod_swap_position>(),
settleDate, td, flowEvents, new List<swap_flow_event>(), null,
posiLongNational, posiShortNational, closeNational, grossPrice, orginPv
});
}
}
#endregion
#region 数据构建器
private static trade CreateTrade()
{
return new trade
{
id = 1, TradeNumber = "UT-DEAL-INT-001", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾
SettlementRules = 0
})
}
};
}
private static swap_position CreateInterestPosition()
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
FloatRateUnderlyingCode = null, // 固定利率,不需要浮动
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
{
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
})
};
}
/// <summary>创建前一日 eod(模拟"昨天收盘后的状态"</summary>
private static eod_swap_position CreatePreEod(DateTime valueDate, decimal interestProfitSum, decimal realizedInterest = 0m)
{
return new eod_swap_position
{
id = 100, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate,
ClientId = 999998, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价,
InterestProfitSum = interestProfitSum,
InterestIncomeSum = interestProfitSum,
RealizedInterest = realizedInterest,
InterestRateDefault = FixedRate,
TdInterestPrincipal = Principal,
PosiNotionalValue = Principal,
InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1,
FloatRate = 0m
};
}
/// <summary>创建互换 flow_event(模拟"当天做了收益结算"</summary>
private static swap_flow_event CreateSwapFlowEvent(DateTime eventDate, decimal interestAmount)
{
return new swap_flow_event
{
id = 2001, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.互换,
EventDate = eventDate, UnwindDate = eventDate, PositionId = 1001,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestAmount = interestAmount,
InterestClosePnL = interestAmount, // 收取方向,两者相等
InterestRate = FixedRate,
InterestMode = (int)InterestModeEnum.标的期初全价,
InterestPrincipal = Principal,
FloatRate = 0m,
DataState = (int)SwapFlowDateStateEnum.完成
};
}
private static void AssertDecimal(decimal expected, decimal actual, string message = "")
{
var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2);
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
}
#endregion
// ================================================================
// 场景1:互换结清后 InterestIncomeSum 应归零(cs:837 修复验证)
// ================================================================
#region 场景1:互换结清后 InterestIncomeSum 归零
/// <summary>
/// [DI_SWAP_ZERO_001] 互换结清-攒了N天利息后全额互换结算,待实现应归零
/// ---------------------------------------------------------------
/// 起息日4/27,攒到5/1013天),InterestProfitSum≈13天利息。
/// 5/10做互换结算,flow_event.InterestAmount=13天利息。
/// 收盘后 InterestIncomeSum 应≈0(全部已实现)。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_SWAP_ZERO_001_互换结清后待实现归零()
{
var service = new StubEodPositionService();
var td = CreateTrade();
var position = CreateInterestPosition();
var settleDate = new DateTime(2026, 5, 10);
// 攒了13天利息(4/27~5/9,算头不算尾)
int days = (settleDate - StartDate).Days;
decimal accumulatedInterest = Math.Round(Principal * FixedRate * days / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var preEod = CreatePreEod(settleDate.AddDays(-1), accumulatedInterest);
// 当天做了互换结算,利息=攒的全部
var swapEvent = CreateSwapFlowEvent(settleDate, accumulatedInterest);
// 执行互换分支
var result = service.ExecuteSaveEodInterestPosition(preEod, null, position, td, settleDate, new List<swap_flow_event> { swapEvent });
// 核心断言:InterestIncomeSum = pre + 当天新计(TdInterestIncome) - 实现(TdCloseInterest)
// 互换把攒的13天全付了(TdCloseInterest=accumulatedInterest),但当天又产生1天新计(TdInterestIncome)
// 所以 InterestIncomeSum 应 ≈ 1天新计利息(而非严格0)
// 公式(cs:869): pre.InterestIncomeSum + TdInterestIncome - TdCloseInterest
decimal expectedTdInterestIncome = Math.Round(Principal * FixedRate / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
AssertDecimal(expectedTdInterestIncome, result.InterestIncomeSum,
$"互换结清后 InterestIncomeSum 应=当天新计利息({expectedTdInterestIncome:F6})" +
$"而非攒的全程({accumulatedInterest:F6})");
// TdCloseInterest 应=互换实现的利息
AssertDecimal(accumulatedInterest, result.TdCloseInterest, "TdCloseInterest 应=互换实现的利息");
// RealizedInterest 应累加(preEod.RealizedInterest + TdCloseInterest * ratio
// 收取方向 ratio=1
AssertDecimal(accumulatedInterest, result.RealizedInterest, "RealizedInterest 应累加已实现利息");
Console.WriteLine($"攒了{days}天利息={accumulatedInterest:F6}");
Console.WriteLine($"互换结清后 InterestIncomeSum={result.InterestIncomeSum:F6}(应≈0)✅");
Console.WriteLine($"TdCloseInterest={result.TdCloseInterest:F6} RealizedInterest={result.RealizedInterest:F6}");
}
/// <summary>
/// [DI_SWAP_ZERO_002] 互换结清后 InterestIncomeSum 不为负(防多扣)
/// ---------------------------------------------------------------
/// 验证:待实现=0(已结清)时,TdCloseInterest=当天新计,InterestIncomeSum 应=0。
/// 公式: 0 + 当天新计 - 当天新计 = 0。如果公式有误会变成负数。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_SWAP_ZERO_002_互换结清后待实现不为负()
{
var service = new StubEodPositionService();
var td = CreateTrade();
var position = CreateInterestPosition();
var swapDate = new DateTime(2026, 5, 10);
// 已结清状态:待实现=0
var postSwapEod = CreatePreEod(swapDate.AddDays(-1), 0m, 0m);
// 互换只结算当天新计(InterestAmount=当天新计利息)
decimal dailyInc = Math.Round(Principal * FixedRate / AnnualDays,
ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
var swapEvent = CreateSwapFlowEvent(swapDate, dailyInc);
var result = service.ExecuteSaveEodInterestPosition(postSwapEod, null, position, td, swapDate, new List<swap_flow_event> { swapEvent });
// 公式: 0(待实现) + dailyInc(新计) - dailyInc(实现) = 0
AssertDecimal(0m, result.InterestIncomeSum, "待实现=0+当天新计-当天新计应=0,不应为负");
Console.WriteLine($"已结清后再互换(只结算当天新计)InterestIncomeSum={result.InterestIncomeSum:F6} = 0 ✅");
}
#endregion
// ================================================================
// 场景2DealInterests 分支选择逻辑验证
// ================================================================
#region 场景2:分支选择
/// <summary>
/// [DI_BRANCH_001] 普通日(无互换无平仓无观察日)→ 走 copy 分支
/// ---------------------------------------------------------------
/// flowEvents 为空,insterval=nullhasSwap=falsehasClose=false
/// → 应走 SaveEodInterestPositionCopycs:338
/// ---------------------------------------------------------------
/// <summary>
/// [DI_BRANCH_001] 普通日收盘归档:InterestIncomeSum 每天递增1天利息
/// ---------------------------------------------------------------
/// 前日待实现=1天利息,今日收盘(无互换无平仓),应变成2天利息。
/// 验证 copy 分支(SaveEodInterestPositionCopy)的 InterestIncomeSum 公式。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_BRANCH_001_普通日归档待实现递增()
{
var service = new StubEodPositionService();
var td = CreateTrade();
var position = CreateInterestPosition();
var settleDate = new DateTime(2026, 4, 28); // 第2天
var preEod = CreatePreEod(settleDate.AddDays(-1), DailyInterest); // 前日=1天利息
// 普通日:无互换无平仓,InterestSwapInterval=null(当天非观察日)
position.InterestSwapInterval = null;
service.ExecuteDealInterests(
new List<swap_position> { position },
new List<eod_swap_position> { preEod },
settleDate, td, new List<swap_flow_event>(),
Principal, 0m, 0m, 1m, Principal);
Assert.IsTrue(service.PersistedPositions.Count > 0, "应生成eod");
var result = service.PersistedPositions[0];
// 普通日:InterestIncomeSum 应 = 前日 + 当天新计 = 1天 + 1天 = 2天
AssertDecimal(DailyInterest * 2, result.InterestIncomeSum,
$"普通日后 InterestIncomeSum 应=2天利息({DailyInterest * 2:F6})");
Console.WriteLine($"普通日归档:InterestIncomeSum={result.InterestIncomeSum:F6} = 2×{DailyInterest:F6} ✅");
}
/// <summary>
/// [DI_BRANCH_002] 互换日(hasSwap=true)→ 走 SaveEodInterestPosition 分支
/// ---------------------------------------------------------------
/// flowEvents 含 EventType=互换,hasSwap=true
/// → 应走 SaveEodInterestPositioncs:330
/// → 验证 PersistEodSwapPosition 被调用(生成了 eod
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_BRANCH_002_互换日走SaveEodInterestPosition分支()
{
var service = new StubEodPositionService();
var td = CreateTrade();
var position = CreateInterestPosition();
var settleDate = new DateTime(2026, 5, 10);
var preEod = CreatePreEod(settleDate.AddDays(-1), DailyInterest * 13);
// 互换事件
var swapEvent = CreateSwapFlowEvent(settleDate, DailyInterest * 13);
position.InterestSwapInterval = null;
var interestList = new List<swap_position> { position };
var eodPositions = new List<eod_swap_position> { preEod };
service.ExecuteDealInterests(interestList, eodPositions, settleDate, td,
new List<swap_flow_event> { swapEvent },
Principal, 0m, 0m, 1m, Principal);
// 互换分支应生成1条 eod
Assert.AreEqual(1, service.PersistedPositions.Count, "互换分支应生成1条eod");
var result = service.PersistedPositions[0];
// InterestIncomeSum = pre + 当天新计 - 实现 ≈ 当天新计(攒的全付了)
AssertDecimal(DailyInterest, result.InterestIncomeSum, "互换结清后待实现≈当天新计利息");
Console.WriteLine($"互换日分支执行,InterestIncomeSum={result.InterestIncomeSum:F6} ≈ 当天新计({DailyInterest:F6}) ✅");
}
#endregion
// ================================================================
// 场景3:多日守恒——连续收盘归档,InterestIncomeSum 应线性递增
// ================================================================
#region 场景3:多日连续归档
/// <summary>
/// [DI_MULTI_001] 连续5天普通日收盘归档,InterestIncomeSum 每天递增1天利息
/// ---------------------------------------------------------------
/// 从4/27(首日)开始,连续收盘到5/1,验证 InterestIncomeSum 线性递增。
/// 每天收盘后 InterestIncomeSum 应 = 天数 × DailyInterest。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void DI_MULTI_001_连续5天归档待实现线性递增()
{
var td = CreateTrade();
var position = CreateInterestPosition();
position.InterestSwapInterval = null; // 无观察日
decimal runningIncomeSum = 0m;
var runningDate = StartDate;
for (int day = 0; day < 5; day++)
{
var service = new StubEodPositionService();
var preEod = CreatePreEod(runningDate.AddDays(-1), runningIncomeSum);
service.ExecuteDealInterests(
new List<swap_position> { position },
new List<eod_swap_position> { preEod },
runningDate, td, new List<swap_flow_event>(),
Principal, 0m, 0m, 1m, Principal);
Assert.IsTrue(service.PersistedPositions.Count > 0, $"第{day + 1}天应生成eod");
var result = service.PersistedPositions[0];
// 首日 InterestIncomeSum = 1天利息,后续每天+1天利息
decimal expected = DailyInterest * (day + 1);
AssertDecimal(expected, result.InterestIncomeSum,
$"第{day + 1}天 InterestIncomeSum 应={(day + 1)}天利息");
runningIncomeSum = result.InterestIncomeSum;
runningDate = runningDate.AddDays(1);
}
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
}
}