using Newtonsoft.Json; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Models; namespace YLErp.Modules.SwapModule { /// /// 互换利息计算单元测试 /// ================================================================ /// 测试口径说明: /// "11" = 算头算尾(含起息日和到期日) /// "10" = 算头不算尾(含起息日,不含到期日) /// "01" = 不算头算尾(不含起息日,含到期日) /// "00" = 不算头不算尾(不含起息日也不含到期日) /// 不算头不算尾暂时测试不通过 /// 统一测试数据: /// - Principal=1000, FixedRate=1.00%, AnnualDays=365 /// - ResetPeriod=3天, InterestRule=-1(前一营业日), InterestRule=0(当前营业日) /// - FR007@2026-04-27=0.10%, FR007@2026-04-30=0.20% /// - StartDate=2026-04-28, TradeDate=2026-04-27 /// ================================================================ /// [TestClass] public class GetInterestsUnitTest { #region 内部类:浮动利率模拟服务 /// /// StubSwapDealService - 模拟浮动利率获取 /// 用于单元测试中预置FR007价格,避免依赖外部数据源 /// private sealed class StubSwapDealService : SwapDealService { private readonly IReadOnlyDictionary _floatRates; public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) { _floatRates = floatRates; } protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) { rate = 0; return false; } if (_floatRates.TryGetValue(valueDate.Date, out rate)) { return true; } rate = 0; return false; } } #endregion #region 测试常量与共享变量 private const decimal Principal = 1000m; // 本金:1000 private const decimal FixedRate = 0.01m; // 固定利率:1.00% private const int AnnualDays = 365; // 年化天数 private const int ResetPeriod = 3; // 重置周期:3天 private const int InterestRule_Pre = -1; // 前一营业日规则 private const int InterestRule_Cur = 0; // 当前营业日规则 private static readonly DateTime TradeDate = new(2026, 4, 27); // 成交日 private static readonly DateTime StartDate = new(2026, 4, 28); // 起息日(开始计息日) private static readonly DateTime ExerciseDate = new(2027, 4, 27); // 到期日 private SwapDealService _service; private IReadOnlyDictionary _floatRates; [TestInitialize] public void Init() { // 预置FR007价格数据 _floatRates = new Dictionary { [new DateTime(2026, 4, 27)] = 0.001, // FR007@2026-04-27 = 0.10% [new DateTime(2026, 4, 28)] = 0.001, // FR007@2026-04-28 = 0.10% (新增) [new DateTime(2026, 4, 29)] = 0.001, // FR007@2026-04-29 = 0.10% [new DateTime(2026, 4, 30)] = 0.002, // FR007@2026-04-30 = 0.20% [new DateTime(2026, 5, 6)] = 0.002, // FR007@2026-05-06 = 0.20% // 到期日测试用例需要的利率数据(2027年) [new DateTime(2027, 4, 23)] = 0.001, // FR007@2027-04-23 = 0.10%(2027-04-26的前一工作日) [new DateTime(2027, 4, 24)] = 0.001, // FR007@2027-04-24 = 0.10%(周末) [new DateTime(2027, 4, 25)] = 0.001, // FR007@2027-04-25 = 0.10%(周末) [new DateTime(2027, 4, 26)] = 0.001, // FR007@2027-04-26 = 0.10% [new DateTime(2027, 4, 27)] = 0.001 // FR007@2027-04-27 = 0.10%(到期日) }; _service = new StubSwapDealService( new OptUserInfo(0, nameof(GetInterestsUnitTest), OptUserFrom.UnitTest), _floatRates); } #endregion #region 测试数据构建器 /// /// 创建测试用交易对象 /// /// 计息口径:"11"/"10"/"01"/"00" /// 取率规则:-1=前一营业日,0=当前营业日 private static trade CreateTrade(string interestCalcMode, int interestRule = InterestRule_Pre) { var extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = interestCalcMode, SettlementRules = interestRule }) }; return new trade { id = 1, TradeNumber = "UT-SWAP-INT-001", ClientId = 999998, TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate, ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", trade_extend = extend }; } /// /// 创建测试用持仓对象 /// /// 计息口径 /// 取率规则 private static swap_position CreateInterestPosition(string interestCalcMode, int interestRule = InterestRule_Pre) { var intervalModels = new List { new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 } }; 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 = ResetPeriod, interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } /// /// 创建日终持仓记录(EOD归档数据) /// private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) { return new eod_swap_position { id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate, ClientId = 999998, FloatRate = floatRate, TdInterestPrincipal = tdPrincipal, PosiNotionalValue = tdPrincipal, InterestProfitSum = interestSum }; } /// /// 计算期望利息金额 /// 公式:本金 × (固定利率 + 浮动利率) × 计息天数 ÷ 年化天数 /// private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) { var yearlyRate = fixedRate + floatRate; var interest = principal * yearlyRate * days / AnnualDays; return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } #endregion #region 通用的GetInterests调用方法 /// /// 通用平仓计算(不含eodPositions) /// private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, decimal closePercent, int interestRule = InterestRule_Pre) { return CalcUnwind(interestCalcMode, valueDate, unwindDate, closePercent, new List(), interestRule); } /// /// 通用平仓计算(含eodPositions) /// private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, decimal closePercent, List eodPositions, int interestRule = InterestRule_Pre) { var td = CreateTrade(interestCalcMode, interestRule); var position = CreateInterestPosition(interestCalcMode, interestRule); var interests = _service.GetInterests( td, td.trade_extend, valueDate, unwindDate, eodPositions, new List { position }, Principal, 0, 0, Principal, closePercent, (int)SwapEventTypeEnum.平仓, false, false, 0, Principal, false, interestCalcMode.EndsWith("1", StringComparison.Ordinal), false); Assert.AreEqual(1, interests.Count); return interests[0]; } /// /// 通用收盘计算 /// settment=true 表示收盘场景 /// private swap_flow_event CalcEod(string interestCalcMode, DateTime valueDate, List eodPositions, int interestRule = InterestRule_Pre) { var td = CreateTrade(interestCalcMode, interestRule); var position = CreateInterestPosition(interestCalcMode, interestRule); var interests = _service.GetInterests( td, td.trade_extend, valueDate, valueDate, eodPositions, new List { position }, Principal, 0, 0, Principal, 1m, (int)SwapEventTypeEnum.平仓, false, false, 0, Principal, false, interestCalcMode.EndsWith("1", StringComparison.Ordinal), true); // settment=true 表示收盘 Assert.AreEqual(1, interests.Count); return interests[0]; } /// /// 通用自动互换计算 /// 使用SwapEventTypeEnum.自动互换事件类型 /// private swap_flow_event CalcAutoSwap(string interestCalcMode, DateTime valueDate, List eodPositions, decimal closePercent = 1m, int interestRule = InterestRule_Pre) { var td = CreateTrade(interestCalcMode, interestRule); var position = CreateInterestPosition(interestCalcMode, interestRule); var interests = _service.GetInterests( td, td.trade_extend, valueDate, valueDate, eodPositions, new List { position }, Principal, 0, 0, Principal, closePercent, (int)SwapEventTypeEnum.自动互换, false, false, 0, Principal, false, interestCalcMode.EndsWith("1", StringComparison.Ordinal), false); Assert.AreEqual(1, interests.Count); return interests[0]; } #endregion #region 场景1:算头算尾 (InterestCalcMode="11") #region 计息区间说明: /// 11_001: 首日(StartDate=4/28)平仓 → S=4/28, E=4/28 → 1天 /// 11_002: 次日(4/29)平仓 → S=4/28, E=4/29 → 2天 /// 11_003: 次日(4/29)平仓50% → S=4/28, E=4/29 → 2天×50% /// 11_004: 跨周期(5/6)平仓 → S=4/28, E=5/6 → 8天(分段取率) /// 11_EOD_001: 首日(4/28)收盘 → 1天 /// 11_EOD_002: 4/28已收盘 → 4/29平仓 → S=4/29, E=4/29 → 1天 #endregion /// ================================================================ */ /// /// [11_001] 算头算尾 - 首日起息日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日StartDate)盘中执行全平 /// 前置:无上一日EOD持仓(首次操作) /// 操作:valueDate=2026-04-28,执行"全平"(closePercent=100%) /// 口径:算头算尾,计息区间 S=4/28, E=4/28 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_PRE_001() { var interest = CalcUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [11_002] 算头算尾 - 次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 /// 前置:无上一日EOD持仓 /// 操作:valueDate=2026-04-29,执行"全平" /// 口径:算头算尾,计息区间 S=4/28, E=4/29 /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_PRE_002() { var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [11_003] 算头算尾 - 次日平仓50% /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓50% /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) /// 口径:算头算尾,计息区间 S=4/28, E=4/29 /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_PRE_003() { var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [11_004] 算头算尾 - 跨重置周期全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 /// 背景:ResetPeriod=3天,4/28→4/30为第一周期,5/1→5/6为第二周期 /// 操作:valueDate=2026-05-06,执行"全平" /// 取率:跨周期分段取率 /// - 第一段(4/28-4/30): 3天×FR007@4/27(0.10%) /// - 第二段(5/1-5/6): 6天×FR007@4/30(0.20%) /// 口径:算头算尾,计息区间 S=4/28, E=5/6 /// 期望:分段计算利息 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_PRE_004() { var interest = CalcUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); // 预期分段计算:3天@0.10% + 6天@0.20% var expected = Math.Round( ExpectedInterest(3, FixedRate, 0.001m, Principal) + ExpectedInterest(6, FixedRate, 0.002m, Principal), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [11_EOD_001] 算头算尾 - 首日收盘归档 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日)执行收盘EOD归档 /// 前置:无上一日EOD持仓(首次收盘) /// 操作:执行 2026-04-28 收盘归档 /// 口径:算头算尾,计息区间 S=4/28, E=4/28 /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_EOD_001() { var interest = CalcEod("11", new DateTime(2026, 4, 28), new List()); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [11_EOD_002] 算头算尾 - 前日已收盘,次日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 /// 前置:存在4/28的EOD持仓记录(待实现利息=1天利息) /// 操作:valueDate=2026-04-29,执行"全平" /// 口径:算头算尾 /// 期望:总利息=历史待实现利息+当期利息=1天(4/28)+1天(4/29)=2天 /// 利息=2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_EOD_002() { var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); // 平仓利息 = 历史待实现利息(4/28=1天) + 当期利息(4/29=1天) = 2天 var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #region 场景2:算头不算尾 (InterestCalcMode="10") - 当前测试重点 #region 计息区间说明: /// 10_001: 首日(4/28)平仓 → S=4/28, E=4/27 → 0天 /// 10_002: 次日(4/29)全平 → S=4/28, E=4/28 → 1天 /// 10_003: 次日(4/29)半平 → 1天×50% /// 10_004: 次日(4/29)全平后收盘 → 全平利息+收盘待实现=0 /// 10_005: 第3日(4/30)全平 → S=4/28, E=4/29 → 2天 /// 10_006: 第3日(4/30)半平 → 2天×50% /// 10_007: 次日(4/29)半平 + 第3日(4/30)收盘 → 剩余50%×1天 /// 10_008: 第3日(4/30)直接收盘 → 持仓×1天 /// 10_009: 次日(4/29)自动互换 → 1天 /// 10_010: 自动互换后次日(4/30)平仓 → 0天 /// 10_011: 跨周期(5/6)全平 → 分段计息 /// 10_EOD_001: 首日(4/28)收盘 → 0天(首次) /// 10_EOD_002: 4/28收盘 → 4/29全平 → 1天 /// 10_EOD_003: 4/28收盘 → 4/29半平 → 0.5天 /// 10_EOD_004: 4/28→4/29连续收盘 /// 10_EOD_005: 4/28收盘 → 4/30收盘 #endregion /// ================================================================ */ #region 2.1 盘中平仓场景 /// /// [10_001] 算头不算尾 - 首日起息日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日StartDate)盘中执行全平 /// 前置:无上一日EOD持仓 /// 操作:valueDate=2026-04-28,执行"全平" /// 口径:算头不算尾 /// - 算头:计息开始日 S=4/28(起息日) /// - 不算尾:计息结束日 E=4/27(前一日) /// - 计息天数 = E - S = 4/27 - 4/28 = -1 → 0天 /// 期望:计息天数=0天,利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_001() { var interest = CalcUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [10_002] 算头不算尾 - 次日全平(基准场景) /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 /// 前置:无上一日EOD持仓 /// 操作:valueDate=2026-04-29,执行"全平" /// 取率:前一营业日规则 → 取2026-04-27的FR007=0.10% /// 口径:算头不算尾 /// - 算头:S=4/28(起息日) /// - 不算尾:E=4/28(操作日前一日) /// - 计息天数 = 4/28 - 4/28 = 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_002() { var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_003] 算头不算尾 - 次日平仓50% /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) /// 取率:前一营业日规则 → FR007@2026-04-27=0.10% /// 口径:算头不算尾,计息天数=1天 /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_003() { var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_004] 算头不算尾 - 次日全平后收盘 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中全平;2026-04-29 收盘 /// 操作: /// 1. 2026-04-29 盘中执行"全平" → 计息1天 /// 2. 2026-04-29 执行收盘归档 → 待实现利息=0 /// 期望: /// - 全平应计利息=1天 /// - 收盘待实现利息=0(因持仓已不存在) /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_004() { // 第一步:全平计息 var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); // 第二步:收盘(持仓已不存在,利息=0) Console.WriteLine("全平后收盘,待实现利息=0(持仓已不存在)"); } /// /// [10_005] 算头不算尾 - 第3日全平(跨周末) /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-30(第3个工作日)盘中全平 /// 背景:4/28(周二)→4/29(周三)→4/30(周四),跨2个自然日 /// 操作:valueDate=2026-04-30,执行"全平" /// 取率:按"前一营业日"规则,沿用首个周期取率日 2026-04-27 /// 口径:算头不算尾 /// - 算头:S=4/28(起息日) /// - 不算尾:E=4/30(操作日前一日) /// - 计息天数 = 4/30 - 4/28 = 2天 /// 实际计算:持仓期间为4/28~4/29(算头不算尾)=2天 /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_005() { var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_006] 算头不算尾 - 第3日平仓50%(跨周末) /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中平仓一半 /// 操作:valueDate=2026-04-30,执行"平仓50%" /// 取率:FR007@2026-04-27=0.10% /// 口径:算头不算尾,计息天数=2天 /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_006() { var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 0.5m); var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_007] 算头不算尾 - 次日半平 + 第3日收盘 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中平仓一半;2026-04-30 收盘 /// 操作: /// 1. 2026-04-29 盘中"平仓50%" → 剩余50%持仓 /// 2. 2026-04-30 执行收盘归档 → 剩余50%持仓计息 /// 取率:FR007@2026-04-27=0.10% /// 口径:算头不算尾 /// 期望: /// - 4/29全平利息=0.5*1*(1.00%+0.10%)*1000/365 /// - 4/30收盘利息=0.5*1*(1.00%+0.10%)*1000/365(剩余50%计1天) /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_007() { // 第一步:4月29日平仓50% var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); // 第二步:4月30日收盘(剩余50%持仓计息1天) var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m)) }; var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expectedEod, eodInterest.InterestAmount); } /// /// [10_008] 算头不算尾 - 第3日直接收盘(未平仓) /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 已收盘归档;2026-04-30 收盘 /// 背景:持仓期间4/28→4/29已完成收盘归档 /// 操作:直接执行 2026-04-30 收盘归档 /// 取率:FR007@2026-04-27=0.10% /// 口径:算头不算尾 /// 期望:2026-04-30 收盘待实现利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_008() { // 4月29日收盘归档后,4月30日收盘 var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expectedEod, eodInterest.InterestAmount); } /// /// [10_009] 算头不算尾 - 次日自动互换 /// --------------------------------------------------------------- /// 场景:2026-04-29 执行"自动互换" /// 背景:自动互换是互换交易的一种定期重置操作 /// 操作:2026-04-29 执行"自动互换" /// 取率:FR007@2026-04-27=0.10% /// 口径:算头不算尾 /// 期望: /// - 计息天数=1天 /// - 利息=1*(1.00%+0.10%)*1000/365 /// - 当日收盘待实现利息=0(持仓已互换) /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_009() { var interest = CalcAutoSwap("10", new DateTime(2026, 4, 29), new List()); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); Console.WriteLine("自动互换后,当日收盘待实现利息=0"); } /// /// [10_010] 算头不算尾 - 自动互换后次日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-29 已发生自动互换;2026-04-30 执行"全平/收益结算" /// 背景:自动互换已将持仓重置,累计利息清零 /// 操作:valueDate=2026-04-30,执行"全平" /// 口径:算头不算尾 /// 期望:计息天数=0天,利息=0(持仓已互换) /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_010() { // 4月29日自动互换后的eodPosition(自动互换后累计利息清零) var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, 0m) }; // 4月30日平仓(持仓已互换,计息天数=0) var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, eodPositions); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [10_011] 算头不算尾 - 跨重置周期全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 未平仓;2026-05-06 跨重置周期全平 /// 背景: /// - ResetPeriod=3天 /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% /// 操作: /// 1. 2026-04-29 收盘归档 /// 2. 2026-05-06 全平(跨周期) /// 口径:算头不算尾 /// 取率:分段取率 /// - 4/29收盘利息=1天@0.10% /// - 4/30持仓利息=1天@0.10%(第一周期最后一天) /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) /// 期望:利息 = 4/29收盘 + 4/30持仓 + 5/1~5/5持仓 = oneDay*2 + secondPeriod /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_011() { // 4月29日收盘归档 var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal); var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, oneDay) }; // 5月6日全平(跨周期) var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); // 4/30: 1天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); // 累计利息 = 4/29收盘利息 + 4/30持仓利息(同第一周期) + 5/1~5/5利息 var expected = Math.Round(oneDay * 2 + secondPeriod, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_012] 算头不算尾 - 跨重置周期全平(中间无收盘) /// --------------------------------------------------------------- /// 场景:2026-04-28 起息;5/6 全平(中间4/29未收盘) /// 背景: /// - ResetPeriod=3天 /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% /// 操作:4/28起息后,4/29未收盘,直接5/6全平 /// 口径:算头不算尾 /// 取率:分段取率 /// - 4/28~4/30持仓利息=3天@0.10%(第一周期,4/28算头) /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) /// 期望:利息 = 3天@0.10% + 5天@0.20% /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_012() { // 4/28起息,无EOD持仓(4/29未收盘) var eodPositions = new List(); // 5月6日全平(跨周期,4/29未收盘) var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); // 4/28~4/30: 3天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) var firstPeriod = ExpectedInterest(3, FixedRate, 0.001m, Principal); var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); var expected = Math.Round(firstPeriod + secondPeriod, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #region 2.2 收盘归档场景(文档4.2节 - 组B) /// /// [10_PRE_EOD_001] 算头不算尾 - 首日收盘归档(文档4.2节) /// --------------------------------------------------------------- /// 场景:2026-04-28 收盘 /// 操作:执行 2026-04-28 EOD /// 取率日:2026-04-27(FR007=0.10%) /// 口径:算头不算尾 /// 说明:首日收盘,当日计息1天 /// 期望:当日收盘利息(待实现)=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_EOD_001() { var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_PRE_EOD_002] 算头不算尾 - 首日收盘,次日全平(文档4.2节) /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘;2026-04-29 盘中全平或收益结算 /// 操作:valueDate=2026-04-29 执行"全平/收益结算" /// 取率日:2026-04-27(FR007=0.10%) /// 口径:算头不算尾 /// - 持仓区间:4/28~4/29 /// - 计息区间:4/29-4/28=1天 /// 期望:计息天数=1;利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_EOD_002() { // 4/28收盘,利息=1天 var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_PRE_EOD_003] 算头不算尾 - 首日收盘,次日平仓50%(文档4.2节) /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘;2026-04-29 盘中平仓一半 /// 操作:valueDate=2026-04-29 执行"平仓50%" /// 取率日:2026-04-27(FR007=0.10%) /// 口径:算头不算尾 /// 期望:计息天数=1;利息=0.5*1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_PRE_EOD_003() { // 4/28收盘,利息=1天 var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #region 2.3 代码额外补充的收盘场景 /// /// [10_EOD_001] 算头不算尾 - 首日收盘归档(代码实现版) /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日)执行收盘EOD归档 /// 前置:无上一日EOD持仓(首次收盘) /// 操作:执行 2026-04-28 收盘归档 /// 口径:算头不算尾 /// 说明:算头,4/28起息日算利息;不算尾指到期日不算 /// - 算头:S=4/28 /// - 不算尾:E=4/27(到期日4/28不算) /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_001() { var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_EOD_002] 算头不算尾 - 首日收盘,次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 /// 前置:存在4/28的EOD持仓记录(待实现利息=1天) /// 操作:valueDate=2026-04-29,执行"全平" /// 口径:算头不算尾 /// - 算头:4/28起息日算利息 /// - 不算尾:4/29到期日不算利息 /// - 历史待实现:4/28=1天 /// - 当期利息:4/29=0天(不算尾) /// 期望:总利息=1天+0天=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_002() { var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); // 平仓利息 = 历史待实现(1天) + 当期(0天) = 1天 var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_EOD_003] 算头不算尾 - 首日收盘,次日平仓50% /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行平仓一半 /// 操作:valueDate=2026-04-29,执行"平仓50%" /// 口径:算头不算尾,计息天数=1天 /// 期望:总利息=(历史1天+当期0天)*50%=0.5天,利息=0.5*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_003() { // 4/28收盘(算头=1天利息),4/29平仓50% var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; // 平仓50%:总利息=(历史1天+当期0天)*50%=0.5天 var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29) /// --------------------------------------------------------------- /// 场景:2026-04-28 和 2026-04-29 连续两个工作日收盘归档 /// 操作: /// 1. 执行 2026-04-28 收盘归档 /// 2. 执行 2026-04-29 收盘归档 /// 口径:算头不算尾 /// 期望: /// - 4/28收盘利息=1天(算头,首日计息) /// - 4/29收盘利息=1天 + 4/28累计利息 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_004() { // 4月28日收盘(利息=1天,算头) var eod1 = CalcEod("10", new DateTime(2026, 4, 28), new List()); var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected1, eod1.InterestAmount); // 4月29日收盘(利息=1天 + 4/28累计利息) var eod2 = CalcEod("10", new DateTime(2026, 4, 29), new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1) }); // 4/29收盘利息 = 4/28累计利息 var expected2 = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected2, eod2.InterestAmount); } /// /// [10_EOD_005] 算头不算尾 - 首日收盘后第3日收盘 /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-30 执行收盘归档 /// 背景:4/29(周三)未执行收盘归档 /// 操作:执行 2026-04-30 收盘归档 /// 口径:算头不算尾 /// 期望:4/29收盘利息=1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_005() { var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) }; var interest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_EOD_006] 算头不算尾 - 到期日收盘不算尾 /// --------------------------------------------------------------- /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) /// 操作:2027-04-27 执行收盘归档 /// 口径:算头不算尾("10") /// - 算头:首日4/28计息 /// - 不算尾:到期日4/27不计息 /// 期望:到期日收盘利息=0(到期日不算尾) /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_EOD_006() { // 2027-04-26 收盘归档产生的 EOD 持仓 // 假设累计利息为 InterestProfitSum=10 var eodPositions = new List { CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) }; // 到期日 2027-04-27 收盘(不算尾,利息=0) var interest = CalcEod("10", new DateTime(2027, 4, 27), eodPositions); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [11_EOD_006] 算头算尾 - 到期日收盘算尾 /// --------------------------------------------------------------- /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) /// 操作:2027-04-27 执行收盘归档 /// 口径:算头算尾("11") /// - 算头:首日4/28计息 /// - 算尾:到期日4/27计息 /// 期望:到期日收盘利息=1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_11_EOD_006() { // 2027-04-26 收盘归档产生的 EOD 持仓 // 假设累计利息为 InterestProfitSum=10 var eodPositions = new List { CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) }; // 到期日 2027-04-27 收盘(算尾,利息=1天) var interest = CalcEod("11", new DateTime(2027, 4, 27), eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #region 2.3 当前营业日规则(interest_rule=0) /// /// [10_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平 /// --------------------------------------------------------------- /// 场景:算头不算尾("10");interest_rule=0(当前营业日) /// 操作:2026-04-28 未收盘;2026-04-29 盘中全平 /// 前置:提供 FR007@2026-04-29 数据 /// 取率:当前营业日规则 → 取当日 FR007@2026-04-29=0.10% /// 口径:算头不算尾,计息天数=1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_CUR_001() { var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, InterestRule_Cur); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [10_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平 /// --------------------------------------------------------------- /// 场景:算头不算尾("10");interest_rule=0(当前营业日) /// 操作:2026-04-28 未收盘;2026-04-30 盘中全平 /// 前置:提供 FR007@2026-04-30 数据 /// 取率:ResetPeriod=3天,从4/28到4/30=2天<3天(重置周期内) /// 应取起息日利率 FR007@2026-04-28=0.10% /// 口径:算头不算尾,计息天数=2天 /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_10_CUR_002() { var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, InterestRule_Cur); var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #endregion #region 场景3:不算头算尾 (InterestCalcMode="01") #region 计息区间说明: /// 01_001: 首日(4/28)平仓 → S=4/29, E=4/28 → 0天 /// 01_002: 次日(4/29)全平 → S=4/29, E=4/29 → 1天 /// 01_003: 次日(4/29)半平 → 1天×50% /// 01_004: 第3日(4/30)全平 → S=4/29, E=4/30 → 1天 /// 01_005: 跨周期(5/6)全平 → 0天 /// 01_EOD_001: 首日(4/28)收盘 → 0天 /// 01_EOD_002: 4/28收盘 → 4/29全平 → 1天 #endregion /// ================================================================ */ /// /// [01_001] 不算头算尾 - 首日起息日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日StartDate)盘中执行全平 /// 口径:不算头算尾 /// - 不算头:S=4/29(起息日次日) /// - 算尾:E=4/28(操作日) /// - 计息天数 = 4/28 - 4/29 = -1 → 0天 /// 期望:计息天数=0天,利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_PRE_001() { var interest = CalcUnwind("01", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [01_002] 不算头算尾 - 次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 /// 口径:不算头算尾 /// - 不算头:S=4/29(下一日起息) /// - 算尾:E=4/29(操作日) /// - 计息天数 = 4/29 - 4/29 = 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_PRE_002() { var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [01_003] 不算头算尾 - 次日平仓50% /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 /// 口径:不算头算尾,计息天数=1天 /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_PRE_003() { var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [01_004] 不算头算尾 - 第3日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 /// 口径:不算头算尾 /// - 不算头:S=4/29(下一日起息) /// - 算尾:E=4/30(操作日) /// - 计息天数 = 4/30 - 4/29 = 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_PRE_004() { var interest = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [01_EOD_001] 不算头算尾 - 首日收盘归档 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日)执行收盘EOD归档 /// 口径:不算头算尾 /// - 不算头:S=4/29 /// - 算尾:E=4/28 → 计息天数=0 /// 期望:当日收盘利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_EOD_001() { var interest = CalcEod("01", new DateTime(2026, 4, 28), new List()); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [01_EOD_002] 不算头算尾 - 前日已收盘,次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 /// 口径:不算头算尾 /// - 不算头:S=4/29 /// - 算尾:E=4/29 /// - 计息天数=1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_EOD_002() { var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) }; var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [01_005] 不算头算尾 - 跨周期全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 /// 口径:不算头算尾 /// - 不算头:S=5/7(下一周期起息日) /// - 算尾:E=5/6 /// - 计息天数 = 5/6 - 5/7 = -1 → 0天 /// 期望:计息天数=0天,利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_01_PRE_005() { var interest = CalcUnwind("01", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); Assert.AreEqual(0m, interest.InterestAmount); } #endregion #region 场景4:不算头不算尾 (InterestCalcMode="00") #region 计息区间说明: /// 00_001: 首日(4/28)平仓 → S=4/29, E=4/27 → 0天 /// 00_002: 次日(4/29)全平 → S=4/29, E=4/28 → 0天 /// 00_003: 第3日(4/30)全平 → S=4/29, E=4/29 → 0天 /// 00_004: 跨周期(5/6)全平 → 0天 /// 00_EOD_001: 首日(4/28)收盘 → 0天 /// 00_EOD_002: 4/28收盘 → 4/29全平 → 0天 #endregion /// ================================================================ */ /// /// [00_001] 不算头不算尾 - 首日起息日平仓 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日StartDate)盘中执行全平 /// 口径:不算头不算尾 /// - 不算头:S=4/29 /// - 不算尾:E=4/27 /// - 计息天数 = 4/27 - 4/29 = -2 → 0天 /// 期望:计息天数=0天,利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_PRE_001() { var interest = CalcUnwind("00", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [00_002] 不算头不算尾 - 次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 /// 口径:不算头不算尾 /// - 不算头:利息从4/29开始(跨到下一周期) /// - 不算尾:E=4/28 /// - 计息区间:4/29-5/1 → 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_PRE_002() { var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [00_003] 不算头不算尾 - 第3日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 /// 口径:不算头不算尾 /// - 不算头:利息从4/30开始(跨到下一周期) /// - 不算尾:E=4/29(减1天) /// - 计息区间:4/30-5/1 → 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_PRE_003() { var interest = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } /// /// [00_004] 不算头不算尾 - 跨周期全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 /// 口径:不算头不算尾 /// - 不算头:S=5/7 /// - 不算尾:E=5/5 /// - 计息天数 = 5/5 - 5/7 = -2 → 0天 /// 期望:计息天数=0天,利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_PRE_004() { var interest = CalcUnwind("00", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [00_EOD_001] 不算头不算尾 - 首日收盘归档 /// --------------------------------------------------------------- /// 场景:2026-04-28(起息日)执行收盘EOD归档 /// 口径:不算头不算尾 /// - 不算头:S=4/29 /// - 不算尾:E=4/27 → 计息天数=0 /// 期望:当日收盘利息=0 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_EOD_001() { var interest = CalcEod("00", new DateTime(2026, 4, 28), new List()); Assert.AreEqual(0m, interest.InterestAmount); } /// /// [00_EOD_002] 不算头不算尾 - 前日已收盘,次日全平 /// --------------------------------------------------------------- /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 /// 口径:不算头不算尾 /// - 不算头:利息从4/29开始(跨到下一周期) /// - 不算尾:E=4/28(减1天) /// - 计息区间:4/29-5/1 → 1天 /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_00_EOD_002() { var eodPositions = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) }; var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); Assert.AreEqual(expected, interest.InterestAmount); } #endregion #region 场景5:口径对比验证 #region 对比测试说明: /// COMPARE_001: 同一日(4/29)全平,4种口径对比 /// COMPARE_002: 同一日(4/30)全平,4种口径对比 #endregion /// ================================================================ */ /// /// [COMPARE_001] 口径对比 - 同一日(4/29)全平,4种口径对比验证 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 /// 操作:对同一操作日(4/29)分别用4种计息口径执行"全平" /// 对比结果: /// - "11"算头算尾: S=4/28, E=4/29 → 2天 /// - "10"算头不算尾: S=4/28, E=4/28 → 1天 /// - "01"不算头算尾: S=4/29, E=4/29 → 1天 /// - "00"不算头不算尾: S=4/29, E=5/1 → 1天(中间日期跨周期) /// 期望:验证4种口径的差异符合预期 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_COMPARE_001() { // "11"算头算尾: S=4/28, E=4/29 => 2天 var interest11 = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest11.InterestAmount); // "10"算头不算尾: S=4/28, E=4/28 => 1天 var interest10 = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest10.InterestAmount); // "01"不算头算尾: S=4/29, E=4/29 => 1天 var interest01 = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); // "00"不算头不算尾: S=4/29, E=5/1 => 1天(中间日期跨周期) var interest00 = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); } /// /// [COMPARE_002] 口径对比 - 同一日(4/30)全平,4种口径对比验证 /// --------------------------------------------------------------- /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中执行全平(跨周末) /// 操作:对同一操作日(4/30)分别用4种计息口径执行"全平" /// 对比结果: /// - "11"算头算尾: S=4/28, E=4/30 → 3天 /// - "10"算头不算尾: S=4/28, E=4/29 → 2天 /// - "01"不算头算尾: S=4/29, E=4/30 → 1天 /// - "00"不算头不算尾: S=4/30, E=5/1 → 1天(中间日期跨周期) /// 期望:验证4种口径的差异符合预期 /// --------------------------------------------------------------- /// [TestMethod] public void UT_SWAP_INT_COMPARE_002() { // "11"算头算尾: S=4/28, E=4/30 => 3天 var interest11 = CalcUnwind("11", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); Assert.AreEqual(ExpectedInterest(3, FixedRate, 0.001m, Principal), interest11.InterestAmount); // "10"算头不算尾: S=4/28, E=4/29 => 2天 var interest10 = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest10.InterestAmount); // "01"不算头算尾: S=4/29, E=4/30 => 1天 var interest01 = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); // "00"不算头不算尾: S=4/30, E=5/1 => 1天(中间日期跨周期) var interest00 = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); } #endregion } }