- 修复了5/11 EOD跨日复利本金应保留剩余70%动态本金的计算 - 调整了calcLast=true时已平部分动态本金的处理逻辑 - 修正了部分平仓后剩余仓位跨日携带的本金缩放算法 - 添加了针对不同计算路径的本金比例调整机制
1747 lines
92 KiB
C#
1747 lines
92 KiB
C#
using Newtonsoft.Json;
|
||
using YLErp.DBModels;
|
||
using YLErp.DBModels.Enums;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// DealInterests 利息腿归档 - 合成单元测试(内存,不连库)
|
||
/// ============================================================================
|
||
/// 目标:验证收盘时利息腿 eod 的字段计算,覆盖三个分支:
|
||
/// ① 手动互换分支 SaveEodInterestPosition(我们修复 InterestIncomeSum 归零的核心)
|
||
/// ② 普通日分支 SaveEodInterestPositionCopy(InterestIncomeSum 每日递增)
|
||
/// ③ 多日守恒(半平后多日再全平,利息一致性)
|
||
///
|
||
/// 模仿 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 : TestableSwapEodPositionService
|
||
{
|
||
/// <summary>
|
||
/// 自动互换场景可注入固定流水,避免为了验证结算边界而依赖真实计息公式。
|
||
/// 未赋值时仍走生产使用的真实 GetInterests 计算。
|
||
/// </summary>
|
||
public List<swap_flow_event> AutoInterests { get; set; }
|
||
|
||
public SwapDealService DealService { get; set; }
|
||
|
||
public eod_swap_position LastInterestCalculationEodPosition { get; private set; }
|
||
|
||
public StubEodPositionService() : base(nameof(DealInterestsScenarioTest))
|
||
{
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
LastInterestCalculationEodPosition = eodPositions.SingleOrDefault();
|
||
if (AutoInterests != null)
|
||
{
|
||
return AutoInterests;
|
||
}
|
||
|
||
return (DealService ?? 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 包装:验证自动互换时的“高精度应结 -> 两位实际结算 -> 待实现尾差”链路。
|
||
public eod_swap_position ExecuteSaveAutoEodInterestPosition(
|
||
eod_swap_position eodPayPosition, swap_position position, trade td,
|
||
DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap = null,
|
||
decimal posiLongNotional = DealInterestsScenarioTest.Principal,
|
||
decimal orginPv = DealInterestsScenarioTest.Principal)
|
||
{
|
||
SaveAutoEodInterestPosition(eodPayPosition, null, position, td, valueDate, interval,
|
||
lastEodSwap, posiLongNotional, 0m, 1m, orginPv);
|
||
return PersistedPositions.LastOrDefault();
|
||
}
|
||
|
||
public eod_swap_position ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
eod_swap_position eodPayPosition, swap_position position, trade td,
|
||
DateTime valueDate, IntervalModel interval, decimal posiLongNotional,
|
||
decimal posiShortNotional, List<swap_flow_event> flowEvents,
|
||
decimal closeNotional, bool autoSwap)
|
||
{
|
||
SaveAutoEodWithCloseInterestPosition(eodPayPosition, null, position, td, valueDate, interval,
|
||
posiLongNotional, posiShortNotional, flowEvents, closeNotional, autoSwap, 1m,
|
||
DealInterestsScenarioTest.Principal);
|
||
return PersistedPositions.LastOrDefault();
|
||
}
|
||
|
||
// public 包装:直接调用 protected virtual DealInterests(已改为 virtual,无需反射)
|
||
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)
|
||
{
|
||
DealInterests(interestList, eodPositions, new List<eod_swap_position>(),
|
||
settleDate, td, flowEvents, new List<swap_flow_event>(), null,
|
||
posiLongNational, posiShortNational, closeNational, grossPrice, orginPv);
|
||
}
|
||
}
|
||
|
||
private sealed class StubCompoundSwapDealService : SwapDealService
|
||
{
|
||
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
|
||
|
||
public StubCompoundSwapDealService(IReadOnlyDictionary<DateTime, double> floatRates = null)
|
||
: base(new OptUserInfo(0, nameof(StubCompoundSwapDealService), OptUserFrom.UnitTest))
|
||
{
|
||
_floatRates = floatRates ?? new Dictionary<DateTime, double>();
|
||
}
|
||
|
||
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
|
||
{
|
||
return _floatRates.TryGetValue(valueDate.Date, out rate);
|
||
}
|
||
|
||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
|
||
{
|
||
return 0m;
|
||
}
|
||
}
|
||
|
||
#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.完成
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建自动互换利息流水。InterestAmount 是高精度应结,生产入口负责将实际结算收敛到两位。
|
||
/// </summary>
|
||
private static swap_flow_event CreateAutoSwapFlowEvent(DateTime eventDate, decimal interestAmount)
|
||
{
|
||
return new swap_flow_event
|
||
{
|
||
id = 2002, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.自动互换,
|
||
EventDate = eventDate, UnwindDate = eventDate, PositionId = 1001,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestAmount = interestAmount,
|
||
TdInterestAmount = 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/10(13天),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
|
||
|
||
// ================================================================
|
||
// 场景2:DealInterests 分支选择逻辑验证
|
||
// ================================================================
|
||
|
||
#region 场景2:分支选择
|
||
|
||
/// <summary>
|
||
/// [DI_BRANCH_001] 普通日(无互换无平仓无观察日)→ 走 copy 分支
|
||
/// ---------------------------------------------------------------
|
||
/// flowEvents 为空,insterval=null,hasSwap=false,hasClose=false
|
||
/// → 应走 SaveEodInterestPositionCopy(cs: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
|
||
/// → 应走 SaveEodInterestPosition(cs: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=5,InterestDirection=收取)
|
||
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
|
||
|
||
// ================================================================
|
||
// 场景6:自动互换两位实际结算与到期清零
|
||
// ================================================================
|
||
|
||
#region 场景6:自动互换尾差与最终结算
|
||
|
||
/// <summary>
|
||
/// [DI_AUTO_SETTLEMENT_001] 非最终自动互换:实际结算按两位,尾差继续保留在待实现。
|
||
/// 0.0082 四舍五入后实际结算 0.01,待实现应为 0.0082 - 0.01 = -0.0018。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_001_非最终自动互换保留舍入尾差()
|
||
{
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event> { CreateAutoSwapFlowEvent(new DateTime(2026, 5, 10), 0.0082m) }
|
||
};
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
var result = service.ExecuteSaveAutoEodInterestPosition(
|
||
CreatePreEod(new DateTime(2026, 5, 9), 0m), position, td, new DateTime(2026, 5, 10),
|
||
new IntervalModel { Date = new DateTime(2026, 5, 10), Rate = FixedRate, Settlement = 1 });
|
||
|
||
AssertDecimal(0.01m, result.TdCloseInterest, "日终当日已实现必须使用两位实际结算金额");
|
||
AssertDecimal(-0.0018m, result.InterestIncomeSum, "非最终结算的尾差必须继续留在待实现");
|
||
AssertDecimal(0.01m, service.AutoInterests.Single().InterestAmount, "自动互换流水金额必须为两位");
|
||
AssertDecimal(0.01m, service.AutoInterests.Single().InterestClosePnL, "资金汇总使用的流水损益必须为两位");
|
||
}
|
||
|
||
/// <summary>
|
||
/// [DI_AUTO_SETTLEMENT_002] 到期自动互换:仍按两位实际结算,但不存在后续计息时待实现必须清零。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_002_到期自动互换清零待实现()
|
||
{
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event> { CreateAutoSwapFlowEvent(ExerciseDate, 0.0082m) }
|
||
};
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
var result = service.ExecuteSaveAutoEodInterestPosition(
|
||
CreatePreEod(ExerciseDate.AddDays(-1), 0m), position, td, ExerciseDate,
|
||
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 1 });
|
||
|
||
AssertDecimal(0.01m, result.TdCloseInterest, "到期自动结算仍按金额两位落库");
|
||
AssertDecimal(0m, result.InterestIncomeSum, "到期最终自动结算后不得遗留待实现尾差");
|
||
}
|
||
|
||
/// <summary>
|
||
/// [DI_AUTO_SETTLEMENT_003] 自动互换后的后续部分平仓必须续接尾差和累计已实现。
|
||
/// 7/7 自动互换将 0.008191780822 按 0.01 实际结算,留下 -0.001808219178;
|
||
/// 7/8 平仓一半后,待实现继续参与计算,7/9 全平时才清零。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_003_自动互换后部分平仓续接尾差和已实现()
|
||
{
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event>
|
||
{
|
||
CreateAutoSwapFlowEvent(new DateTime(2026, 5, 10), 0.008191780822m)
|
||
}
|
||
};
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
var autoResult = service.ExecuteSaveAutoEodInterestPosition(
|
||
CreatePreEod(new DateTime(2026, 5, 9), 0m), position, td, new DateTime(2026, 5, 10),
|
||
new IntervalModel { Date = new DateTime(2026, 5, 10), Rate = FixedRate, Settlement = 1 });
|
||
|
||
var firstCloseDate = new DateTime(2026, 5, 11);
|
||
var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, 0.01m);
|
||
firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
firstCloseFlow.InterestPrincipal = 50m;
|
||
// 模拟 CalcUnwindInterest:上日尾差加当日新增,尚未扣除本次 0.01 平仓结算。
|
||
service.AutoInterests = new List<swap_flow_event>
|
||
{
|
||
CreateAutoSwapFlowEvent(firstCloseDate, 0.016383561644m)
|
||
};
|
||
service.AutoInterests[0].InterestPrincipal = 50m;
|
||
var firstCloseResult = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
autoResult, position, td, firstCloseDate, null, 50m, 0m,
|
||
new List<swap_flow_event> { firstCloseFlow }, 50m, false);
|
||
|
||
AssertDecimal(-0.001808219178m, service.LastInterestCalculationEodPosition.InterestProfitSum,
|
||
"部分平仓计算必须带入自动互换遗留的待实现尾差");
|
||
Assert.AreEqual(position.id, service.LastInterestCalculationEodPosition.PositionId,
|
||
"部分平仓计息必须按腿标识匹配上一日日终");
|
||
AssertDecimal(-0.010438356164m, firstCloseResult.InterestIncomeSum,
|
||
"部分平仓后待实现应延续历史尾差");
|
||
AssertDecimal(0.02m, firstCloseResult.RealizedInterest,
|
||
"部分平仓后累计已实现应包含此前自动互换和本次平仓");
|
||
|
||
var finalCloseDate = firstCloseDate.AddDays(1);
|
||
var finalCloseFlow = CreateSwapFlowEvent(finalCloseDate, 0.01m);
|
||
finalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
finalCloseFlow.InterestPrincipal = 50m;
|
||
service.AutoInterests = new List<swap_flow_event>
|
||
{
|
||
CreateAutoSwapFlowEvent(finalCloseDate, 0.010479452055m)
|
||
};
|
||
var finalCloseResult = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
firstCloseResult, position, td, finalCloseDate, null, 0m, 0m,
|
||
new List<swap_flow_event> { finalCloseFlow }, 50m, false);
|
||
|
||
AssertDecimal(0m, finalCloseResult.InterestIncomeSum, "全平后待实现应清零");
|
||
AssertDecimal(0.03m, finalCloseResult.RealizedInterest,
|
||
"全平后累计已实现应包含自动互换和两次平仓");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_004_PartialCloseAccruesOnlyAfterPreviousEod()
|
||
{
|
||
const decimal originalNotional = 10012.35m;
|
||
const decimal remainingNotional = 5006.17m;
|
||
const decimal closeNotional = 5006.172835m;
|
||
const decimal rate = 0.0299m;
|
||
const decimal pendingInterest = 0.820379534246m;
|
||
const decimal settledInterest = 0.82m;
|
||
const decimal expectedPendingInterest = 0.410474008219m;
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "01",
|
||
SettlementRules = 0
|
||
});
|
||
var position = CreateInterestPosition();
|
||
position.InterestRateDefault = rate;
|
||
position.InterestSwapInterval = null;
|
||
var previousEodDate = StartDate.AddDays(2);
|
||
var closeDate = previousEodDate.AddDays(1);
|
||
var previousEod = CreatePreEod(previousEodDate, pendingInterest, settledInterest);
|
||
previousEod.TdInterestPrincipal = originalNotional;
|
||
var closeFlow = CreateSwapFlowEvent(closeDate, settledInterest);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
closeFlow.InterestPrincipal = 5006.18m;
|
||
closeFlow.InterestRate = rate;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, closeDate, null,
|
||
remainingNotional, 0m, new List<swap_flow_event> { closeFlow },
|
||
closeNotional, false);
|
||
|
||
AssertDecimal(expectedPendingInterest, result.InterestIncomeSum,
|
||
"Partial close must accrue only the day after the previous EOD snapshot");
|
||
AssertDecimal(remainingNotional, result.TdInterestPrincipal,
|
||
"The close-day snapshot must carry the remaining principal into the next EOD");
|
||
AssertDecimal(1.64m, result.RealizedInterest,
|
||
"Realized interest must include the previous and current settlements");
|
||
Assert.AreEqual(previousEodDate, service.LastInterestCalculationEodPosition.ValueDate,
|
||
"The previous EOD ValueDate must be preserved for accrual boundaries");
|
||
Assert.AreEqual(previousEod.id, service.LastInterestCalculationEodPosition.id,
|
||
"The previous EOD identity must not be reset to a new position");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_COMPOUND_RESET_PARTIAL_CLOSE_UsesCalculatedPrincipal()
|
||
{
|
||
const decimal remainingNotional = 500m;
|
||
const decimal compoundPrincipalAfterSevenDays = 500.958904m;
|
||
var closeDate = StartDate.AddDays(7);
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event>
|
||
{
|
||
new swap_flow_event
|
||
{
|
||
InterestPrincipal = compoundPrincipalAfterSevenDays,
|
||
InterestRate = FixedRate,
|
||
InterestAmount = 0m,
|
||
TdInterestAmount = 0m
|
||
}
|
||
}
|
||
};
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
position.InterestType = (int)InterestTypeEnum.复利;
|
||
position.interest_rest_days = 7;
|
||
var closeFlow = CreateSwapFlowEvent(closeDate, 0m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
CreatePreEod(closeDate.AddDays(-1), 0m), position, td, closeDate, null,
|
||
remainingNotional, 0m, new List<swap_flow_event> { closeFlow }, remainingNotional, false);
|
||
|
||
AssertDecimal(compoundPrincipalAfterSevenDays, result.TdInterestPrincipal,
|
||
"复利重置日部分平仓后,EOD 本金必须保留 CalcSwapInterests 计算的 7 天复利本金");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_MANUAL_PREPAY_PARTIAL_CLOSE_UsesHistoryPlusRemainingDailyInterest()
|
||
{
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
position.InterestMode = (int)InterestModeEnum.初始预付金;
|
||
position.InterestPrincipalFix = Principal;
|
||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||
previousEod.InterestMode = position.InterestMode;
|
||
previousEod.TdInterestPrincipal = Principal;
|
||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
closeFlow.InterestPrincipal = 500m;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, StartDate.AddDays(3), null,
|
||
500m, 0m, new List<swap_flow_event> { closeFlow }, 500m, false);
|
||
|
||
var expected = previousEod.InterestIncomeSum
|
||
+ result.TdInterestIncome - result.TdCloseInterest;
|
||
AssertDecimal(expected, result.InterestIncomeSum,
|
||
"预付金部分平仓待实现收益应为历史待实现+平仓后当日新增-平仓实现");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_MANUAL_PARTIAL_CLOSE_CalcLastIncludesClosedPrincipalDailyInterest()
|
||
{
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "11",
|
||
SettlementRules = 0
|
||
});
|
||
var position = CreateInterestPosition();
|
||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||
previousEod.TdInterestPrincipal = Principal;
|
||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, StartDate.AddDays(3), null,
|
||
500m, 0m, new List<swap_flow_event> { closeFlow }, 500m, false);
|
||
|
||
AssertDecimal(Principal * FixedRate / AnnualDays, result.TdInterestIncome,
|
||
"算尾部分平仓的当日新计利息应包含已平仓部分(按全额本金计提)");
|
||
|
||
// 补充:算尾部分平仓的待实现利息总额应满足递推
|
||
// InterestIncomeSum = 前日待实现 + 当日新计(全额本金,含被平仓部分) - 当日实现
|
||
// 钉死"算尾 → 被平仓部分多1天利息 → 进入待实现总额"的完整链条
|
||
decimal expectedIncomeSum = previousEod.InterestIncomeSum + result.TdInterestIncome - result.TdCloseInterest;
|
||
AssertDecimal(expectedIncomeSum, result.InterestIncomeSum,
|
||
"算尾部分平仓:InterestIncomeSum 应=前日待实现+当日新计(含被平仓部分)-当日实现");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_MANUAL_PARTIAL_CLOSE_NoCalcLastUsesRemainingPrincipalDailyInterest()
|
||
{
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||
previousEod.TdInterestPrincipal = Principal;
|
||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, StartDate.AddDays(3), null,
|
||
500m, 0m, new List<swap_flow_event> { closeFlow }, 500m, false);
|
||
|
||
AssertDecimal(500m * FixedRate / AnnualDays, result.TdInterestIncome,
|
||
"不算尾部分平仓的当日新计利息只应包含剩余持仓部分(按剩余本金计提)");
|
||
|
||
// 补充对照:不算尾时待实现利息总额同样满足递推,但当日新计只含剩余持仓
|
||
// 与 CalcLast 测试对照:不算尾的 TdInterestIncome 更小(差额=被平仓部分1天利息),
|
||
// 因此 InterestIncomeSum 也相应更小——证明算尾/不算尾的差异确实传导到待实现总额
|
||
decimal expectedIncomeSum = previousEod.InterestIncomeSum + result.TdInterestIncome - result.TdCloseInterest;
|
||
AssertDecimal(expectedIncomeSum, result.InterestIncomeSum,
|
||
"不算尾部分平仓:InterestIncomeSum 应=前日待实现+当日新计(仅剩余持仓)-当日实现");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_005_AutoSettlementKeepsRemainingPrincipal()
|
||
{
|
||
const decimal originalNotional = 10012.35m;
|
||
const decimal remainingNotional = 5006.17m;
|
||
const decimal rate = 0.0299m;
|
||
var settlementDate = new DateTime(2026, 7, 14);
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "01",
|
||
SettlementRules = 0
|
||
});
|
||
var position = CreateInterestPosition();
|
||
position.InterestRateDefault = rate;
|
||
position.InterestSwapInterval = null;
|
||
var previousEod = CreatePreEod(settlementDate.AddDays(-1), 2.460947197259m, 1.64m);
|
||
previousEod.TdInterestPrincipal = remainingNotional;
|
||
var staleAggregate = new eod_swap { NotionalValue = originalNotional };
|
||
|
||
var result = service.ExecuteSaveAutoEodInterestPosition(
|
||
previousEod, position, td, settlementDate,
|
||
new IntervalModel { Date = settlementDate, Rate = rate, Settlement = 1 },
|
||
staleAggregate, remainingNotional, originalNotional);
|
||
|
||
AssertDecimal(2.87m, result.TdCloseInterest,
|
||
"Automatic settlement must round the half-position interest to 2.87");
|
||
AssertDecimal(remainingNotional, result.TdInterestPrincipal,
|
||
"Automatic settlement must not restore the original principal from eod_swap");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_006_CloseAndAutoSettlementOnlySettlesRemainder()
|
||
{
|
||
var settleDate = new DateTime(2026, 7, 16);
|
||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||
};
|
||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
CreatePreEod(settleDate.AddDays(-1), 0m), CreateInterestPosition(), CreateTrade(),
|
||
settleDate, new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||
|
||
AssertDecimal(0.73m, autoFlow.InterestAmount,
|
||
"Automatic settlement must deduct the 0.50 already settled by the close");
|
||
AssertDecimal(0.73m, autoFlow.InterestClosePnL,
|
||
"The automatic flow PnL must use the actual 2-decimal remainder");
|
||
AssertDecimal(1.23m, result.TdCloseInterest,
|
||
"EOD realized interest must include both manual and automatic settlements");
|
||
AssertDecimal(0.0045m, result.InterestIncomeSum,
|
||
"The high-precision total less actual settlements must remain unrealized");
|
||
AssertDecimal(1.23m, result.RealizedInterest,
|
||
"Cumulative realized interest must add the combined actual settlement once");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_007_PayLegKeepsUnsignedSettlementAndAppliesDirectionOnce()
|
||
{
|
||
var settleDate = new DateTime(2026, 7, 16);
|
||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||
autoFlow.InterestDirection = (int)SwapDirectionEnum.支付;
|
||
autoFlow.InterestClosePnL = -1.2345m;
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||
};
|
||
var position = CreateInterestPosition();
|
||
position.InterestDirection = (int)SwapDirectionEnum.支付;
|
||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
closeFlow.InterestDirection = (int)SwapDirectionEnum.支付;
|
||
closeFlow.InterestClosePnL = -0.50m;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate,
|
||
new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||
|
||
AssertDecimal(0.73m, autoFlow.InterestAmount);
|
||
AssertDecimal(-0.73m, autoFlow.InterestClosePnL);
|
||
AssertDecimal(1.23m, result.TdCloseInterest,
|
||
"TdCloseInterest follows the unsigned settlement convention used by other interest branches");
|
||
AssertDecimal(0.0045m, result.InterestIncomeSum);
|
||
AssertDecimal(-1.23m, result.RealizedInterest,
|
||
"The pay direction must be applied exactly once when cumulative realized interest is stored");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_AUTO_SETTLEMENT_008_MarginLegAppliesReversedDirectionOnce()
|
||
{
|
||
var settleDate = new DateTime(2026, 7, 16);
|
||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||
autoFlow.InterestMode = (int)InterestModeEnum.初始预付金;
|
||
var service = new StubEodPositionService
|
||
{
|
||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||
};
|
||
var position = CreateInterestPosition();
|
||
position.InterestMode = (int)InterestModeEnum.初始预付金;
|
||
position.InterestPrincipalFix = 50m;
|
||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
closeFlow.InterestMode = (int)InterestModeEnum.初始预付金;
|
||
|
||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate,
|
||
new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||
|
||
AssertDecimal(0.73m, autoFlow.InterestAmount);
|
||
AssertDecimal(0.73m, autoFlow.InterestClosePnL);
|
||
AssertDecimal(1.23m, result.TdCloseInterest);
|
||
AssertDecimal(0.0045m, result.InterestIncomeSum);
|
||
AssertDecimal(-1.23m, result.RealizedInterest,
|
||
"A received margin principal produces payable interest, so the margin ratio reverses once");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_MANUAL_CLOSE_006_FinalCloseIncludesCloseDateInterest()
|
||
{
|
||
const decimal originalNotional = 10012.35m;
|
||
const decimal remainingNotional = 5006.17m;
|
||
const decimal rate = 0.0299m;
|
||
const decimal pendingInterest = 0.411136145205m;
|
||
const decimal expectedInterest = 0.821230619178m;
|
||
var closeDate = new DateTime(2026, 7, 16);
|
||
var service = new StubEodPositionService();
|
||
var td = CreateTrade();
|
||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "01",
|
||
SettlementRules = 0
|
||
});
|
||
var position = CreateInterestPosition();
|
||
position.InterestRateDefault = rate;
|
||
position.InterestSwapInterval = null;
|
||
var previousEod = CreatePreEod(closeDate.AddDays(-1), pendingInterest, 4.51m);
|
||
previousEod.TdInterestPrincipal = remainingNotional;
|
||
var previousFloatingPosition = new eod_swap_position
|
||
{
|
||
PosiDirection = (int)SwapDirectionEnum.支付,
|
||
PosiNotionalValue = remainingNotional
|
||
};
|
||
var previousAggregate = new eod_swap
|
||
{
|
||
NotionalValue = originalNotional,
|
||
NotionalValueLong = remainingNotional
|
||
};
|
||
var orginPv = SwapDealService.ResolveUnwindPreviousNotional(
|
||
previousAggregate, new List<eod_swap_position> { previousEod, previousFloatingPosition },
|
||
remainingNotional);
|
||
|
||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||
previousAggregate, Array.Empty<eod_swap_position>(), originalNotional),
|
||
"Missing details must fall back to the aggregate directional notionals");
|
||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||
null, null, remainingNotional),
|
||
"Missing EOD data must fall back to the current notional");
|
||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||
new eod_swap { NotionalValue = originalNotional }, Array.Empty<eod_swap_position>(),
|
||
remainingNotional),
|
||
"Zero directional notionals must not override a non-zero current remaining notional");
|
||
|
||
var result = new SwapDealService(service).GetInterests(
|
||
td, td.trade_extend, closeDate, closeDate,
|
||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 1m, orginPv,
|
||
false, settment: false, newCalcLast: false, closeList: null).Single();
|
||
|
||
AssertDecimal(remainingNotional, result.InterestPrincipal,
|
||
"Final close must accrue on the remaining principal");
|
||
AssertDecimal(expectedInterest, result.InterestAmount,
|
||
"InterestCalcMode 01 must include the final close date");
|
||
AssertDecimal(0.82m, Math.Round(result.InterestAmount, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero), "Final close cash interest must be 0.82");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_MANUAL_CLOSE_007_CompoundFinalCloseSettlesFirstPartialCloseRoundingTail()
|
||
{
|
||
const decimal originalNotional = 10012.35m;
|
||
const decimal remainingNotional = originalNotional / 2m;
|
||
const decimal rate = 0.0299m;
|
||
var firstCloseDate = StartDate.AddDays(6);
|
||
var finalCloseDate = firstCloseDate.AddDays(6);
|
||
var td = CreateTrade();
|
||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "11",
|
||
SettlementRules = 0
|
||
});
|
||
var position = CreateInterestPosition();
|
||
position.InterestType = (int)InterestTypeEnum.复利;
|
||
position.InterestRateDefault = rate;
|
||
position.InterestPrincipalFix = originalNotional;
|
||
position.interest_rest_days = 1;
|
||
position.InterestSwapInterval = null;
|
||
var dealService = new StubCompoundSwapDealService();
|
||
var eodService = new StubEodPositionService { DealService = dealService };
|
||
|
||
var firstCloseInterest = dealService.GetInterests(
|
||
td, td.trade_extend, firstCloseDate, firstCloseDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
originalNotional, originalNotional, 0m, remainingNotional, 0.5m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||
settment: false).Single();
|
||
var firstCloseCash = Math.Round(firstCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero);
|
||
var firstCloseRoundingTail = firstCloseInterest.InterestAmount - firstCloseCash;
|
||
Assert.AreNotEqual(0m, firstCloseRoundingTail,
|
||
$"首次部分平仓高精度利息 {firstCloseInterest.InterestAmount:F12} 按两位实际结算 {firstCloseCash:F2},尾差 {firstCloseRoundingTail:F12}");
|
||
var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, firstCloseCash);
|
||
firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
firstCloseFlow.InterestPrincipal = remainingNotional;
|
||
var firstCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
null, position, td, firstCloseDate, null, remainingNotional, 0m,
|
||
new List<swap_flow_event> { firstCloseFlow }, remainingNotional, false);
|
||
|
||
var replayAtPreviousEod = dealService.GetInterests(
|
||
td, td.trade_extend, firstCloseDate, firstCloseDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||
settment: false).Single();
|
||
var replayAtFinalClose = dealService.GetInterests(
|
||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||
settment: false).Single();
|
||
var expectedFinalInterest = firstCloseEod.InterestIncomeSum
|
||
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount;
|
||
var expectedFinalCash = Math.Round(expectedFinalInterest, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero);
|
||
var expectedTotalCash = Math.Round(firstCloseCash + firstCloseEod.InterestIncomeSum
|
||
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount,
|
||
ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||
|
||
var finalCloseInterest = dealService.GetInterests(
|
||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||
new List<eod_swap_position> { firstCloseEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 1m, originalNotional,
|
||
settment: false).Single();
|
||
var finalCloseCash = Math.Round(finalCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero);
|
||
var finalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash);
|
||
finalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
finalCloseFlow.InterestPrincipal = remainingNotional;
|
||
var finalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
|
||
new List<swap_flow_event> { finalCloseFlow }, remainingNotional, false);
|
||
|
||
AssertDecimal(expectedFinalCash, finalCloseCash,
|
||
"最终全平现金必须带走上一日日终的待实现利息尾差");
|
||
AssertDecimal(expectedTotalCash,
|
||
firstCloseCash + finalCloseCash,
|
||
"两次实际结算现金必须守恒");
|
||
AssertDecimal(0m, finalCloseEod.InterestIncomeSum, "全平且实际金额覆盖应结利息后待实现应清零");
|
||
AssertDecimal(firstCloseCash + finalCloseCash, finalCloseEod.RealizedInterest,
|
||
"累计已实现利息必须等于历次实际结算金额之和");
|
||
|
||
var incompleteFinalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash - 0.01m);
|
||
incompleteFinalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||
incompleteFinalCloseFlow.InterestPrincipal = remainingNotional;
|
||
var incompleteFinalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
|
||
new List<swap_flow_event> { incompleteFinalCloseFlow }, remainingNotional, false);
|
||
AssertDecimal(expectedFinalInterest - incompleteFinalCloseFlow.InterestAmount,
|
||
incompleteFinalCloseEod.InterestIncomeSum,
|
||
"最终全平流水少结 0.01 时,日终必须保留未结利息而非清零");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_GLMS_20260421_0007_ContractNotionalCompoundPartialCloseScalesAndFinalCloseUsesRemainingPrincipal()
|
||
{
|
||
const decimal notional = 303139117.8m;
|
||
const decimal partialPercent = 0.3m;
|
||
const decimal partialNotional = notional * partialPercent;
|
||
const decimal remainingNotional = notional - partialNotional;
|
||
const decimal spread = 0.0025m;
|
||
var startDate = new DateTime(2026, 4, 21);
|
||
var maturityDate = new DateTime(2026, 5, 19);
|
||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||
var td = new trade
|
||
{
|
||
id = 7007,
|
||
TradeNumber = "GLMS-20260421-0007",
|
||
ClientId = 999998,
|
||
TradeType = "收益互换",
|
||
TradeDate = startDate,
|
||
StartDate = startDate,
|
||
ExerciseDate = maturityDate,
|
||
TradeStatus = "确认成交",
|
||
ValidState = "Valid",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 7007,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "11",
|
||
SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
var position = new swap_position
|
||
{
|
||
id = 70071,
|
||
SwapTradeId = td.id,
|
||
PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = (int)InterestModeEnum.合约名义本金规模,
|
||
InterestRateDefault = spread,
|
||
InterestPrincipalFix = notional,
|
||
PosiStartDate = startDate,
|
||
PosiMatuirityDate = maturityDate,
|
||
IsInitial = true,
|
||
Invalid = false,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = maturityDate, Rate = spread, Settlement = 0 }
|
||
})
|
||
};
|
||
var service = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||
{
|
||
[new DateTime(2026, 4, 20)] = 0.0132,
|
||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||
[new DateTime(2026, 4, 23)] = 0.0132,
|
||
[new DateTime(2026, 4, 24)] = 0.0131,
|
||
[new DateTime(2026, 4, 27)] = 0.013502,
|
||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||
[new DateTime(2026, 5, 7)] = 0.0136,
|
||
[new DateTime(2026, 5, 8)] = 0.0135,
|
||
[new DateTime(2026, 5, 9)] = 0.0131,
|
||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||
[new DateTime(2026, 5, 12)] = 0.013,
|
||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||
[new DateTime(2026, 5, 14)] = 0.013,
|
||
[new DateTime(2026, 5, 15)] = 0.013,
|
||
[new DateTime(2026, 5, 18)] = 0.0132,
|
||
[new DateTime(2026, 5, 19)] = 0.0131
|
||
});
|
||
var previousEod = new eod_swap_position
|
||
{
|
||
id = 70072,
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
ValueDate = new DateTime(2026, 5, 10),
|
||
TdInterestPrincipal = 303324019.3183441374m,
|
||
InterestIncomeSum = 266674.349853521170m,
|
||
InterestProfitSum = 266674.349853521170m,
|
||
FloatRate = 0.0139m
|
||
};
|
||
|
||
var partial = service.GetInterests(
|
||
td, td.trade_extend, partialCloseDate, partialCloseDate,
|
||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||
notional, notional, 0m, partialNotional, partialPercent,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, notional,
|
||
settment: false).Single();
|
||
AssertDecimal(84090.95m, Math.Round(partial.InterestAmount, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero),
|
||
"GLMS-20260421-0007 30% 复利合约名义本金平仓必须按比例结算");
|
||
|
||
var final = service.GetInterests(
|
||
td, td.trade_extend, maturityDate, maturityDate,
|
||
new List<eod_swap_position>(), new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, remainingNotional,
|
||
settment: false, newCalcLast: true).Single();
|
||
AssertDecimal(268428.73m, Math.Round(final.InterestAmount, ConsGlobal.MoneyRound,
|
||
MidpointRounding.AwayFromZero),
|
||
"后续全平必须只结算剩余70%本金的复利,不重复结算原始全额");
|
||
Assert.AreNotEqual(280303.16m,
|
||
Math.Round(final.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
|
||
"后续全平不得再次使用原始全额本金");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_GLMS_20260421_0007_EodPartialCloseCarriesRemainingCompoundAccrual()
|
||
{
|
||
const decimal originalNotional = 303139117.8m;
|
||
const decimal partialNotional = 90941735.34m;
|
||
const decimal remainingNotional = 212197382.46m;
|
||
const decimal spread = 0.0025m;
|
||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||
var intermediateDate = new DateTime(2026, 5, 18);
|
||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||
var td = new trade
|
||
{
|
||
id = 7007,
|
||
TradeNumber = "GLMS-20260421-0007-EOD",
|
||
ClientId = 999998,
|
||
TradeType = "收益互换",
|
||
TradeDate = new DateTime(2026, 4, 21),
|
||
StartDate = new DateTime(2026, 4, 21),
|
||
ExerciseDate = finalCloseDate,
|
||
TradeStatus = "确认成交",
|
||
ValidState = "Valid",
|
||
StructureType = "单标的",
|
||
QuoteCurrency = "CNY",
|
||
SettlementCurrency = "CNY",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 7007,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "11",
|
||
SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
var position = new swap_position
|
||
{
|
||
id = 70071,
|
||
SwapTradeId = td.id,
|
||
PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = (int)InterestModeEnum.合约名义本金规模,
|
||
InterestRateDefault = spread,
|
||
InterestPrincipalFix = originalNotional,
|
||
PosiStartDate = td.StartDate.Value,
|
||
PosiMatuirityDate = finalCloseDate,
|
||
IsInitial = true,
|
||
Invalid = false,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = finalCloseDate, Rate = spread, Settlement = 0 }
|
||
})
|
||
};
|
||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||
{
|
||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||
[new DateTime(2026, 5, 12)] = 0.013,
|
||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||
[new DateTime(2026, 5, 19)] = 0.0131
|
||
});
|
||
var eodService = new StubEodPositionService { DealService = dealService };
|
||
var previousEod = new eod_swap_position
|
||
{
|
||
id = 70072,
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
ValueDate = new DateTime(2026, 5, 10),
|
||
InterestDirection = position.InterestDirection,
|
||
InterestMode = position.InterestMode,
|
||
InterestType = position.InterestType,
|
||
InterestRateDefault = spread,
|
||
InterestIncomeSum = 266674.349853521170m,
|
||
InterestProfitSum = 266674.349853521170m,
|
||
TdInterestPrincipal = 303324019.3183441374m,
|
||
PosiNotionalValue = originalNotional,
|
||
FloatRate = 0.0139m,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0
|
||
};
|
||
var partialCloseFlow = new swap_flow_event
|
||
{
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
EventType = (int)SwapFlowEventTypeEnum.平仓,
|
||
EventDate = partialCloseDate,
|
||
UnwindDate = partialCloseDate,
|
||
InterestDirection = position.InterestDirection,
|
||
InterestRate = spread,
|
||
InterestPrincipal = partialNotional,
|
||
InterestAmount = 84090.95m,
|
||
InterestClosePnL = 84090.95m,
|
||
DataState = (int)SwapFlowDateStateEnum.完成
|
||
};
|
||
|
||
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
|
||
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
|
||
AssertDecimal(13628.805251563956m, partialEod.TdInterestIncome,
|
||
"5/11 EOD 当日新增复利必须按平仓前全额本金计提");
|
||
AssertDecimal(196212.205105085126m, partialEod.InterestIncomeSum,
|
||
"5/11 EOD 应保留部分平仓后的待实现复利");
|
||
AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional),
|
||
partialEod.TdInterestPrincipal,
|
||
"5/11 EOD 跨日复利本金应保留剩余70%动态本金");
|
||
|
||
var intermediateInterest = dealService.GetInterests(
|
||
td, td.trade_extend, intermediateDate, intermediateDate,
|
||
new List<eod_swap_position> { partialEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||
settment: false, newCalcLast: true).Single();
|
||
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
||
$"5/18 复利平仓应承接 5/11 日终剩余本金的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
||
|
||
}
|
||
|
||
[DataTestMethod]
|
||
[DataRow((int)InterestModeEnum.合约名义本金规模)]
|
||
[DataRow((int)InterestModeEnum.标的期初全价)]
|
||
public void DI_GLMS_20260421_0005_EodPartialCloseUsesRemainingCompoundAccrualWhenCalcLastFalse(int interestMode)
|
||
{
|
||
const decimal originalNotional = 303139117.8m;
|
||
const decimal partialNotional = 90941735.34m;
|
||
const decimal remainingNotional = 212197382.46m;
|
||
const decimal spread = 0.0025m;
|
||
var partialCloseDate = new DateTime(2026, 5, 11);
|
||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||
var td = new trade
|
||
{
|
||
id = 1828,
|
||
TradeNumber = "GLMS-20260421-0005",
|
||
ClientId = 999998,
|
||
TradeType = "收益互换",
|
||
TradeDate = new DateTime(2026, 4, 21),
|
||
StartDate = new DateTime(2026, 4, 21),
|
||
ExerciseDate = finalCloseDate,
|
||
TradeStatus = "确认成交",
|
||
ValidState = "Valid",
|
||
StructureType = "单标的",
|
||
QuoteCurrency = "CNY",
|
||
SettlementCurrency = "CNY",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 1828,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "10",
|
||
SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
var position = new swap_position
|
||
{
|
||
id = 18281,
|
||
SwapTradeId = td.id,
|
||
PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = interestMode,
|
||
InterestRateDefault = spread,
|
||
InterestPrincipalFix = originalNotional,
|
||
PosiStartDate = td.StartDate.Value,
|
||
PosiMatuirityDate = finalCloseDate,
|
||
IsInitial = true,
|
||
Invalid = false,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||
{
|
||
new IntervalModel { Date = finalCloseDate, Rate = spread, Settlement = 0 }
|
||
})
|
||
};
|
||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||
{
|
||
[new DateTime(2026, 4, 21)] = 0.0132,
|
||
[new DateTime(2026, 4, 28)] = 0.0136,
|
||
[new DateTime(2026, 4, 30)] = 0.0139,
|
||
[new DateTime(2026, 5, 4)] = 0.0139,
|
||
[new DateTime(2026, 5, 5)] = 0.0139,
|
||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||
[new DateTime(2026, 5, 11)] = 0.0134,
|
||
[new DateTime(2026, 5, 12)] = 0.013,
|
||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||
[new DateTime(2026, 5, 19)] = 0.0131
|
||
});
|
||
var eodService = new StubEodPositionService { DealService = dealService };
|
||
var previousEod = new eod_swap_position
|
||
{
|
||
id = 18282,
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
ValueDate = new DateTime(2026, 5, 10),
|
||
InterestDirection = position.InterestDirection,
|
||
InterestMode = position.InterestMode,
|
||
InterestType = position.InterestType,
|
||
InterestRateDefault = spread,
|
||
InterestIncomeSum = 266674.349853521170m,
|
||
InterestProfitSum = 266674.349853521170m,
|
||
TdInterestPrincipal = 303324019.318344137434m,
|
||
PosiNotionalValue = originalNotional,
|
||
FloatRate = 0.0139m,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0
|
||
};
|
||
var partialCloseFlow = new swap_flow_event
|
||
{
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
EventType = (int)SwapFlowEventTypeEnum.平仓,
|
||
EventDate = partialCloseDate,
|
||
UnwindDate = partialCloseDate,
|
||
InterestDirection = position.InterestDirection,
|
||
InterestRate = spread,
|
||
InterestPrincipal = partialNotional,
|
||
InterestAmount = 80002.30m,
|
||
InterestClosePnL = 80002.30m,
|
||
DataState = (int)SwapFlowDateStateEnum.完成
|
||
};
|
||
|
||
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
|
||
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
|
||
|
||
AssertDecimal(9540.163676094769m, partialEod.TdInterestIncome,
|
||
"0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提");
|
||
AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum,
|
||
"0005 部分平仓后日终待实现复利必须扣除实际80002.30结算");
|
||
AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional),
|
||
partialEod.TdInterestPrincipal,
|
||
"0005 部分平仓后,下一日复利本金必须只继承剩余70%本金");
|
||
|
||
var intermediateDate = new DateTime(2026, 5, 18);
|
||
var intermediateInterest = dealService.GetInterests(
|
||
td, td.trade_extend, intermediateDate, intermediateDate,
|
||
new List<eod_swap_position> { partialEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||
settment: false, newCalcLast: true).Single();
|
||
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
||
$"0005 5/18 复利应承接部分平仓后的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
||
|
||
var intermediateEod = partialEod.Clone();
|
||
intermediateEod.id = 18283;
|
||
intermediateEod.ValueDate = intermediateDate;
|
||
intermediateEod.InterestIncomeSum = intermediateInterest.InterestAmount;
|
||
intermediateEod.InterestProfitSum = intermediateInterest.InterestAmount;
|
||
intermediateEod.TdInterestPrincipal = remainingNotional;
|
||
intermediateEod.PosiNotionalValue = remainingNotional;
|
||
|
||
var expectedEndFlow = new swap_flow_event { InterestRate = spread };
|
||
decimal expectedAmountAtEnd = 0m;
|
||
decimal expectedTdAmountAtEnd = 0m;
|
||
dealService.CalcDailyCompoundInterest(
|
||
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
|
||
intermediateEod.FloatRate, 1m, originalNotional, true, false,
|
||
ref expectedAmountAtEnd, ref expectedTdAmountAtEnd);
|
||
var expectedPreviousFlow = new swap_flow_event { InterestRate = spread };
|
||
decimal expectedAmountAtPreviousEod = 0m;
|
||
decimal expectedTdAmountAtPreviousEod = 0m;
|
||
dealService.CalcDailyCompoundInterest(
|
||
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false,
|
||
intermediateEod.FloatRate, 1m, originalNotional, true, true,
|
||
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
|
||
var expectedFinalInterest = intermediateEod.InterestIncomeSum
|
||
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
|
||
var finalInterest = dealService.GetInterests(
|
||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||
new List<eod_swap_position> { intermediateEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||
settment: false, newCalcLast: false).Single();
|
||
AssertDecimal(expectedFinalInterest, finalInterest.InterestAmount,
|
||
"0005 最终全平重放时,历史5/18终点必须包含当日利息后再做差额");
|
||
|
||
}
|
||
|
||
[TestMethod]
|
||
public void DI_GLMS_20260421_0006_FinalCloseCarriesPreviousEodInterest()
|
||
{
|
||
const decimal remainingNotional = 212197382.46m;
|
||
const decimal expectedInterest = -119386.71m;
|
||
var finalCloseDate = new DateTime(2026, 5, 19);
|
||
var td = new trade
|
||
{
|
||
id = 6006,
|
||
TradeNumber = "GLMS-20260421-0006",
|
||
ClientId = 999998,
|
||
TradeType = "收益互换",
|
||
TradeDate = new DateTime(2026, 4, 21),
|
||
StartDate = new DateTime(2026, 4, 22),
|
||
ExerciseDate = finalCloseDate,
|
||
TradeStatus = "确认成交",
|
||
ValidState = "Valid",
|
||
StructureType = "单标的",
|
||
QuoteCurrency = "CNY",
|
||
SettlementCurrency = "CNY",
|
||
trade_extend = new trade_extend
|
||
{
|
||
TradeId = 6006,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = "10",
|
||
SettlementRules = 0
|
||
})
|
||
}
|
||
};
|
||
var position = new swap_position
|
||
{
|
||
id = 60061,
|
||
SwapTradeId = td.id,
|
||
PositionType = (int)PositionTypeFlag.Unknown,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||
InterestRateDefault = -0.021m,
|
||
InterestPrincipalFix = remainingNotional,
|
||
PosiStartDate = td.StartDate.Value,
|
||
PosiMatuirityDate = td.ExerciseDate.Value,
|
||
IsInitial = true,
|
||
Invalid = false,
|
||
InterestType = (int)InterestTypeEnum.复利,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0,
|
||
FloatRateUnderlyingCode = "FR007",
|
||
InterestSwapInterval = null
|
||
};
|
||
var previousEod = new eod_swap_position
|
||
{
|
||
id = 60062,
|
||
SwapTradeId = td.id,
|
||
PositionId = position.id,
|
||
ValueDate = new DateTime(2026, 5, 18),
|
||
InterestDirection = position.InterestDirection,
|
||
InterestMode = position.InterestMode,
|
||
InterestType = position.InterestType,
|
||
InterestRateDefault = position.InterestRateDefault,
|
||
FloatRate = 0.0132m,
|
||
InterestIncomeSum = expectedInterest,
|
||
InterestProfitSum = expectedInterest,
|
||
TdInterestPrincipal = remainingNotional,
|
||
PosiNotionalValue = remainingNotional,
|
||
IsAnnualized = true,
|
||
interest_rest_days = 7,
|
||
interest_rule = 0
|
||
};
|
||
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
|
||
{
|
||
[new DateTime(2026, 4, 22)] = 0.0132,
|
||
[new DateTime(2026, 4, 29)] = 0.0138,
|
||
[new DateTime(2026, 5, 6)] = 0.0136,
|
||
[new DateTime(2026, 5, 13)] = 0.0129,
|
||
[new DateTime(2026, 5, 19)] = 0.0131
|
||
});
|
||
|
||
var result = dealService.GetInterests(
|
||
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓, false, false, 0m, remainingNotional,
|
||
settment: false).Single();
|
||
|
||
AssertDecimal(expectedInterest, result.InterestAmount,
|
||
"GLMS-20260421-0006 最终全平应承接 5/18 日终待实现利息");
|
||
Assert.AreNotEqual(-123072.67m, result.InterestAmount,
|
||
"不得回归旧库错误的 -123072.67 最终利息");
|
||
}
|
||
|
||
/// <summary>
|
||
/// [DI_MATURITY_SETTLEMENT_001] 到期日存在手动互换但未带齐待实现时不能清零;
|
||
/// 当前事件按两位覆盖全部可结金额后,才可视为最终结算并清零。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void DI_MATURITY_SETTLEMENT_001_到期手动互换仅在结清后清零()
|
||
{
|
||
var settleDate = ExerciseDate;
|
||
var td = CreateTrade();
|
||
var position = CreateInterestPosition();
|
||
position.InterestPrincipalFix = 0m;
|
||
|
||
// 事件未结算此前的 0.0082:到期日也必须保留待实现。
|
||
var incompleteService = new StubEodPositionService();
|
||
var incompleteEvent = CreateSwapFlowEvent(settleDate, 0m);
|
||
incompleteEvent.InterestPrincipal = 0m;
|
||
incompleteEvent.InterestRate = 0m;
|
||
var incompleteResult = incompleteService.ExecuteSaveEodInterestPosition(
|
||
CreatePreEod(settleDate.AddDays(-1), 0.0082m), null, position, td, settleDate,
|
||
new List<swap_flow_event> { incompleteEvent });
|
||
AssertDecimal(0.0082m, incompleteResult.InterestIncomeSum, "到期但未结清时不得丢弃历史待实现");
|
||
|
||
// 前端按两位提交 0.01,可覆盖 0.0082 的最终金额,允许清零。
|
||
var finalService = new StubEodPositionService();
|
||
var finalEvent = CreateSwapFlowEvent(settleDate, 0.01m);
|
||
finalEvent.InterestPrincipal = 0m;
|
||
finalEvent.InterestRate = 0m;
|
||
var finalResult = finalService.ExecuteSaveEodInterestPosition(
|
||
CreatePreEod(settleDate.AddDays(-1), 0.0082m), null, position, td, settleDate,
|
||
new List<swap_flow_event> { finalEvent });
|
||
AssertDecimal(0m, finalResult.InterestIncomeSum, "两位最终结算覆盖待实现后应清零");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
}
|
||
}
|