- 新建 TestableSwapEodPositionService 收敛 8/8 Stub 重复的高频 override (PersistEodSwapPosition/SaveAllChanges/GetCurrencyRate/AddClientCash) + 统一 OptUserInfo 构造 + PersistedPositions/SaveChangesCount 输出捕获 - SwapEodPositionService.DealInterests 改 protected virtual(行为零变化) - 8 个 ScenarioTest 改为继承基类,删除重复 override - 消灭 DealInterestsScenarioTest/DealInterestsGoldenReplayTest 的反射调用 (typeof().GetMethod().Invoke → 直接调用 DealInterests) 验证:dotnet build 0 错误;dotnet test SwapModule 284通过/6跳过/0失败
572 lines
31 KiB
C#
572 lines
31 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
|
||
{
|
||
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)
|
||
{
|
||
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 包装:直接调用 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);
|
||
}
|
||
}
|
||
|
||
#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/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
|
||
|
||
#endregion
|
||
}
|
||
}
|