diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs deleted file mode 100644 index 2c748aca..00000000 --- a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs +++ /dev/null @@ -1,1411 +0,0 @@ -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, - 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, - 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, - 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(2, 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 - - } -} diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs new file mode 100644 index 00000000..90067ce5 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs @@ -0,0 +1,1078 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换利息计算单元测试 - T+0场景 + /// ================================================================ + /// T+0定义:起息日(StartDate) = 成交日(TradeDate),不额外加1天 + /// TradeDate=2026-04-27, StartDate=2026-04-27 + /// ---------------------------------------------------------------- + /// 测试口径: + /// "10" = 算头不算尾(含起息日,不含操作日) + /// "11" = 算头算尾(含起息日和操作日) + /// ---------------------------------------------------------------- + /// 与T+1的关键差异: + /// T+1: StartDate=4/28, 4/29平仓(算头不算尾)→1天 + /// T+0: StartDate=4/27, 4/28平仓(算头不算尾)→1天 (所有天数+1) + /// ---------------------------------------------------------------- + /// Excel覆盖的T+0算头不算尾场景: + /// 固定利率:T+0固定正利率、T+0固定负利率 + /// 浮动利率:T+0浮动加点(当前营业日/前一营业日/单利) + /// 每个场景 × 4业务场景(浮动×3) + /// ================================================================ + /// + [TestClass] + public class GetInterestsUnitTest_T0 + { + #region 内部类:浮动利率模拟服务 + + 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; + private const decimal FixedRate = 0.01m; + private const decimal FixedRatePositive = 0.0075m; + private const decimal FixedRateNegative = -0.0105m; + private const decimal FloatMinusRate = -0.021m; + private const decimal FloatPlusRate = 0.0025m; + private const int AnnualDays = 365; + private const int ResetPeriod = 3; + private const int ResetPeriodFixed = 1; + private const int InterestRule_Pre = -1; + private const int InterestRule_Cur = 0; + + // T+0: StartDate = TradeDate(不额外加1天) + private static readonly DateTime TradeDate = new(2026, 4, 27); + private static readonly DateTime StartDate = new(2026, 4, 27); // = TradeDate + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + + private SwapDealService _service; + private IReadOnlyDictionary _floatRates; + + [TestInitialize] + public void Init() + { + _floatRates = new Dictionary + { + [new DateTime(2026, 4, 24)] = 0.001, // InterestRule_Pre: GetNonHolidayDefore(4/26日)→4/24 + [new DateTime(2026, 4, 26)] = 0.001, // 新增:T+0前一营业日场景需要 + [new DateTime(2026, 4, 27)] = 0.001, + [new DateTime(2026, 4, 28)] = 0.001, + [new DateTime(2026, 4, 29)] = 0.001, + [new DateTime(2026, 4, 30)] = 0.002, + [new DateTime(2026, 5, 1)] = 0.002, // 复利从头算需要完整日期范围 + [new DateTime(2026, 5, 3)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 5)] = 0.002, // InterestRule_Pre取率日 + [new DateTime(2026, 5, 6)] = 0.002, + [new DateTime(2027, 4, 23)] = 0.001, + [new DateTime(2027, 4, 24)] = 0.001, + [new DateTime(2027, 4, 25)] = 0.001, + [new DateTime(2027, 4, 26)] = 0.001, + [new DateTime(2027, 4, 27)] = 0.001 + }; + _service = new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsUnitTest_T0), OptUserFrom.UnitTest), + _floatRates); + } + + #endregion + + #region 测试数据构建器 + + 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-T0-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + trade_extend = extend + }; + } + + private static swap_position CreateFloatInterestPosition( + int interestRule = InterestRule_Cur, InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal fixedRate = 0.01m, SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + 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)direction, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)interestType, + IsAnnualized = true, interest_rest_days = ResetPeriod, + interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + private static swap_position CreateFixedInterestPosition( + decimal fixedRate = 0.0075m, int interestRule = InterestRule_Cur, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + 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)direction, InterestMode = (int)InterestModeEnum.合约名义本金规模, + InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = ResetPeriodFixed, + interest_rule = interestRule, FloatRateUnderlyingCode = null, + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + 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 + }; + } + + /// + /// 计算含预EOD利息的总期望利息(匹配生产代码中间舍入行为) + /// --------------------------------------------------------------- + /// 生产代码先取 preEod.InterestProfitSum(已舍入到11位的DB值), + /// 再加上新期间日度原始利息,最后再舍入一次 + /// + private static decimal ExpectedInterestWithPreEod( + int newDays, decimal fixedRate, decimal floatRate, decimal principal, + decimal preEodInterestSum, decimal closePercent) + { + var yearlyRate = fixedRate + floatRate; + var newRawInterest = principal * yearlyRate * newDays / AnnualDays; + return Math.Round(preEodInterestSum * closePercent + newRawInterest * closePercent, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + /// + /// 容忍末位差异的利息比较(允许相差2位) + /// + private static void AssertInterestEqual(decimal expected, decimal actual) + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + string.Format("Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, expected - actual)); + } + + 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 通用调用方法 + + // --- 浮动利率 --- + private swap_flow_event CalcFloatUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, InterestTypeEnum interestType, + List eodPositions = null, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, + eodPositions, new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFloatEod(string calcMode, DateTime valueDate, + int interestRule, decimal fixedRate, InterestTypeEnum interestType, + List eodPositions = null, List closeList = null) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, + eodPositions, new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + // --- 固定利率 --- + private swap_flow_event CalcFixedUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + List eodPositions = null, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, + eodPositions, new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFixedEod(string calcMode, DateTime valueDate, + int interestRule, decimal fixedRate, List eodPositions = null, + List closeList = null) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, + eodPositions, new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + // ================================================================ + // T+0场景:StartDate = TradeDate = 2026-04-27 + // 算头不算尾("10"):4/27平仓→0天, 4/28平仓→1天, 4/29平仓→2天 + // 算头算尾("11"):4/27平仓→1天, 4/28平仓→2天, 4/29平仓→3天 + // ================================================================ + + #region 1. T+0固定正利率 算头不算尾("10") - interest_rule=0 + + /// + /// [UT_T0_FIX_POS_001] T+0固定正利率算头不算尾-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→S=4/27,E=4/27→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_001() + { + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_002] T+0固定正利率算头不算尾-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→1+1=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_003] T+0固定正利率算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_004] T+0固定正利率算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天×50%; 5/6全平剩余→EOD=4/28, newCalcLast=true + /// 算头不算尾,newCalcLast强制算尾: 4/29~5/6=8天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_004() + { + // 4/28部分平仓50%(算头不算尾→S=4/27,E=4/27→1天) + var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); + + // 5/6全平剩余50%(EOD=4/28, newCalcLast=true强制算尾: 4/29~5/6=8天) + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); + var expected = ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 2. T+0固定负利率 算头不算尾("10") - interest_rule=0 + + /// + /// [UT_T0_FIX_NEG_001] T+0固定负利率算头不算尾-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→1天, 负利率-1.05% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_001() + { + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRateNegative); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_002] T+0固定负利率算头不算尾-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, 负利率-1.05% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRateNegative, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_003] T+0固定负利率算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRateNegative, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal)*0.5m, interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_004] T+0固定负利率算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天×50%; 5/6全平剩余→newCalcLast=true, 8天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_004() + { + var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRateNegative); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRateNegative, eod, Principal * 0.5m, newCalcLast: true); + var expected = ExpectedInterestWithPreEod(8, FixedRateNegative, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 3. T+0浮动加点(当前营业日,复利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_CUR_002] T+0浮动加点(当前营业日,复利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, FR007+0.25% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_003] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_004] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天; 5/6全平剩余→复利从头算9天[27-29][30-2][3-5] + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List(); + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:9天, 每3天重置, [27-29]@0.35%, [30-2,3-5]@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expected = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 4. T+0浮动加点(前一营业日,复利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_PRE_002] T+0浮动加点(前一营业日,复利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_003] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_004] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→1天@0.10%; 5/6全平→复利从头算9天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:9天, 每3天重置, [27-29]@0.001, [30-2]@0.001, [3-5]@0.002 → 6@0.35%+3@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest2 = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest2; + interest2 += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; + } + var expected = Math.Round(interest2, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 5. T+0浮动加点(单利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_SI_002] T+0浮动加点(单利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, 单利 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_003] T+0浮动加点(单利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_004] T+0浮动加点(单利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→1天; 5/6全平→9天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); + // 单利: 9天, [27,28,29]@0.001 +[30,1,2]@0.001 + [3,4,5]@0.002 + var raw = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + + Principal * 0.5m * (FloatPlusRate + 0.002m) * 3 / AnnualDays; + var expected = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 6. T+0固定正利率 算头算尾("11") - interest_rule=0 + + /// + /// [UT_T0_FIX_POS_11_001] 算头算尾 T+0固定正利率-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头算尾→S=4/27,E=4/28→2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_001() + { + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_002] 算头算尾 T+0固定正利率-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→1+1=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_003] 算头算尾 T+0固定正利率-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_004] 算头算尾 T+0固定正利率-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天×50%; 5/6全平→EOD=4/28, 算头算尾(newCalcLast=true无影响) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_004() + { + var u1 = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); + AssertInterestEqual(ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m), u2.InterestAmount); + } + + #endregion + + #region 7. T+0浮动加点(当前营业日)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_CUR_11_002] 算头算尾 T+0浮动加点(当前营业日,复利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_11_003] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_11_004] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天; 5/6全平→复利从头算10天 [27-29] [30-2] [3-5] [6] + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m, newCalcLast: false); + // 复利从头算:10天, 每3天重置, [27-29]@0.35%, [30-2,3-5,6]@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 10; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 8. T+0浮动加点(前一营业日)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_PRE_11_002] 算头算尾 T+0浮动加点(前一营业日,复利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_11_003] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_11_004] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天@0.10%; 5/6全平→复利从头算10天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:10天, 每3天重置, [27-29,30-2]@0.001, [3-5,6]@0.002 → 6@0.35%+4@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 10; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; + } + var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 9. T+0浮动加点(单利)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_SI_11_002] 算头算尾 T+0浮动加点(单利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 单利 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_11_003] 算头算尾 T+0浮动加点(单利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_11_004] 算头算尾 T+0浮动加点(单利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天; 5/6全平→算头算尾 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); + // 单利: 10天, [27,28,29]@0.001 + [30,1,2]@0.001 + [3,4,5,6]@0.002 → 3@0.35% + 3@0.35% + 3@0.45% + var raw1 = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + + Principal * 0.5m * (FloatPlusRate + 0.002m) * 4 / AnnualDays; + var expected1 = Math.Round(raw1, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 10. T+0固定利率 - 收盘归档 + + /// + /// [UT_T0_FIX_EOD_001] T+0固定利率算头不算尾-首日收盘归档 + /// --------------------------------------------------------------- + /// 4/27(起息日)执行收盘, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_EOD_001() + { + var i = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_EOD_002] T+0固定利率算头不算尾-连续收盘 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28收盘, 算头不算尾→各1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_EOD_002() + { + var e1 = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); + var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1, e1.InterestAmount); + + var e2 = CalcFixedEod("10", new DateTime(2026, 4, 28), InterestRule_Cur, FixedRatePositive, + new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, expected1) }); + AssertInterestEqual(expected1*2, e2.InterestAmount); + } + + #endregion + + #region 11. T+0浮动利率 - 基础场景 + + /// + /// [UT_T0_FLT_BASE_001] T+0浮动利率基础-起息日平仓→0天 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/27平仓, 算头不算尾→0天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_001() + { + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 27), new DateTime(2026, 4, 27), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(0m, i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_002] T+0浮动利率基础-第2天全平→1天 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_002() + { + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_003] T+0浮动利率基础-首日收盘→1天 + /// --------------------------------------------------------------- + /// 4/27执行收盘, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_003() + { + var i = CalcFloatEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_004] T+0浮动利率基础-首日收盘+次日全平→2天 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_004() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs new file mode 100644 index 00000000..d1a94453 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs @@ -0,0 +1,1760 @@ +using Newtonsoft.Json; +using System.Security.Principal; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换利息计算单元测试 - T+1场景 + /// ================================================================ + /// T+1定义:起息日(StartDate) = 成交日(TradeDate) + 1天 + /// TradeDate=2026-04-27, StartDate=2026-04-28 + /// ---------------------------------------------------------------- + /// 测试口径: + /// "10" = 算头不算尾(含起息日,不含操作日) + /// "11" = 算头算尾(含起息日和操作日) + /// ---------------------------------------------------------------- + /// 计息逻辑说明(算头不算尾): + /// calcFirst=true(算头), calcLast=false(不算尾) + /// 计息区间:从StartDate到valueDate-1天 + /// 例如:StartDate=4/28, valueDate=4/29 → 计息区间=4/28(1天) + /// ---------------------------------------------------------------- + /// 统一测试数据: + /// - Principal=1000, AnnualDays=365 + /// - ResetPeriod=3天(浮动利率)/ 1天(固定利率) + /// - InterestRule=-1(前一营业日), 0(当前营业日) + /// - FR007@2026-04-27=0.10%, FR007@2026-04-30=0.20% + /// ---------------------------------------------------------------- + /// Excel覆盖的T+1场景: + /// 固定利率:T+1固定正利率、T+1固定负利率 + /// 浮动利率:T+1浮动减点(前一/当前营业日,单/复利) + /// 每个场景 × 4业务场景(浮动×3) + /// ================================================================ + /// + [TestClass] + public class GetInterestsUnitTest_T1 + { + #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 decimal FixedRatePositive = 0.0075m; // 固定正利率:0.75%(Excel场景) + private const decimal FixedRateNegative = -0.0105m; // 固定负利率:-1.05%(Excel场景) + private const int AnnualDays = 365; // 年化天数 + private const int ResetPeriod = 3; // 重置周期:3天(浮动利率) + private const int ResetPeriodFixed = 1; // 重置周期:1天(固定利率) + 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, 1)] = 0.002, // 复利从头算需要完整日期范围 + [new DateTime(2026, 5, 3)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 4)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 6)] = 0.002, // FR007@2026-05-06 = 0.20% + // 到期日测试用例需要的利率数据(2027年) + [new DateTime(2027, 4, 23)] = 0.001, + [new DateTime(2027, 4, 24)] = 0.001, + [new DateTime(2027, 4, 25)] = 0.001, + [new DateTime(2027, 4, 26)] = 0.001, + [new DateTime(2027, 4, 27)] = 0.001 + }; + + _service = new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsUnitTest_T1), OptUserFrom.UnitTest), + _floatRates); + } + + #endregion + + #region 测试数据构建器 + + /// + /// 创建测试用交易对象 + /// + /// 计息口径:"10"=算头不算尾 + /// 取率规则:-1=前一营业日,0=当前营业日 + private static trade CreateTrade(string interestCalcMode = "10", 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 + }; + } + + /// + /// 创建浮动利率测试用持仓对象(Excel场景:FR007+固定利率) + /// + private static swap_position CreateFloatInterestPosition( + int interestRule = InterestRule_Pre, + InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal fixedRate = 0.01m, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + 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)direction, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = fixedRate, + InterestPrincipalFix = Principal, + PosiStartDate = StartDate, + PosiMatuirityDate = ExerciseDate, + IsInitial = true, + Invalid = false, + InterestType = (int)interestType, + IsAnnualized = true, + interest_rest_days = ResetPeriod, + interest_rule = interestRule, + FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + /// + /// 创建固定利率测试用持仓对象(无浮动利率标的,纯固定利率) + /// 对应Excel中的固定利率场景 + /// + private static swap_position CreateFixedInterestPosition( + decimal fixedRate = 0.0075m, + int interestRule = InterestRule_Pre, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + 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)direction, + InterestMode = (int)InterestModeEnum.合约名义本金规模, + InterestRateDefault = fixedRate, + InterestPrincipalFix = Principal, + PosiStartDate = StartDate, + PosiMatuirityDate = ExerciseDate, + IsInitial = true, + Invalid = false, + InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, + interest_rest_days = ResetPeriodFixed, + interest_rule = interestRule, + FloatRateUnderlyingCode = null, // 无浮动利率标的 + 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 + }; + } + + /// + /// 计算期望利息金额(先累加原始值,最后一次性舍入,比较时比生产少2位容错) + /// + /// + /// 容忍末位差异的利息比较(允许相差2位) + /// + private static void AssertInterestEqual(decimal expected, decimal actual) + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + string.Format("Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, expected - actual)); + } + + 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); + } + + /// + /// 计算含预EOD利息的总期望利息(比生产少2位精度容错) + /// + private static decimal ExpectedInterestWithPreEod( + int newDays, decimal fixedRate, decimal floatRate, decimal principal, + decimal preEodInterestSum, decimal closePercent) + { + var yearlyRate = fixedRate + floatRate; + var newRawInterest = principal * yearlyRate * newDays / AnnualDays; + return Math.Round(preEodInterestSum * closePercent + newRawInterest * closePercent, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + #endregion + + #region 通用的GetInterests调用方法 + + /// + /// 通用浮动利率平仓计算(不含eodPositions) + /// + private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null, bool newCalcLast = false) + { + return CalcFloatUnwind(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, interestType, closeList: closeList, newCalcLast: newCalcLast); + } + + /// + /// 通用浮动利率平仓计算(含eodPositions) + /// + private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m, + InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用浮动利率收盘计算 + /// + private swap_flow_event CalcFloatEod(DateTime valueDate, + List eodPositions, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用浮动利率自动互换计算 + /// + private swap_flow_event CalcFloatAutoSwap(DateTime valueDate, + List eodPositions, decimal closePercent = 1m, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m, + InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, closePercent, + (int)SwapEventTypeEnum.自动互换, + false, false, 0, Principal, false, settment: false, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用固定利率平仓计算 + /// + private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m, + List closeList = null, bool newCalcLast = false) + { + return CalcFixedUnwind(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, closeList: closeList, newCalcLast: newCalcLast); + } + + /// + /// 通用固定利率平仓计算(含eodPositions) + /// + private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("10", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用固定利率收盘计算 + /// + private swap_flow_event CalcFixedEod(DateTime valueDate, + List eodPositions, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.0075m, List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + // ================================================================ + // 所有测试均使用 InterestCalcMode="10"(算头不算尾) + // ================================================================ + + #region 场景1:浮动利率算头不算尾 - 盘中平仓场景 + + /// + /// [FLOAT_UNWIND_001] 算头不算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:StartDate=4/28盘中执行全平 + /// 口径:算头不算尾 + /// - 算头:S=4/28 + /// - 不算尾:E=4/27(操作日前一日) + /// - 计息天数 = 0天 + /// 期望:利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_001() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + AssertInterestEqual(0m, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_002] 算头不算尾 - 次日全平(基准场景) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中全平 + /// 口径:算头不算尾 → 计息区间:4/28(1天) + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_002() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_003] 算头不算尾 - 次日平仓50% + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中平仓一半 + /// 口径:算头不算尾,计息天数=1天 + /// 期望:利息=0.5*1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_003() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expected = ExpectedInterestWithPreEod(0, FixedRate, 0.001m, Principal, ExpectedInterest(1, FixedRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_004] 算头不算尾 - 第3日全平(跨周末) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/30盘中全平 + /// 口径:算头不算尾 → 计息区间:4/28~4/29(2天) + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_004() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_005] 算头不算尾 - 第3日平仓50%(跨周末) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/30盘中平仓一半 + /// 口径:算头不算尾,计息天数=2天 + /// 期望:利息=0.5*2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_005() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 0.5m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_006] 算头不算尾 - 次日半平 + 第3日收盘 + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中平仓一半;4/30收盘 + /// 期望: + /// - 4/29平仓利息=0.5*1*(1.00%+0.10%)*1000/365 + /// - 4/30收盘利息=剩余50%*1天利息 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_006() + { + // 第一步:4月29日平仓50% + var unwindInterest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(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 = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_007] 算头不算尾 - 第3日直接收盘(已有前日EOD) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29已收盘归档;4/30收盘 + /// 期望:4/30收盘待实现利息=3天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_007() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, + ExpectedInterest(2, FixedRate, 0.001m, Principal)) + }; + var eodInterest = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(3, FixedRate, 0.001m, Principal * 1m); + AssertInterestEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_008] 算头不算尾 - 次日自动互换 + /// --------------------------------------------------------------- + /// 场景:4/29执行"自动互换" + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_008() + { + var interest = CalcFloatAutoSwap(new DateTime(2026, 4, 29), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_009] 算头不算尾 - 自动互换后次日平仓 + /// --------------------------------------------------------------- + /// 场景:4/29已自动互换;4/30执行"全平" + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_009() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, 0m) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, eodPositions); + AssertInterestEqual(0m, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_010] 算头不算尾 - 跨重置周期全平(中间有收盘) + /// --------------------------------------------------------------- + /// 场景:4/29收盘归档;5/6全平(跨周期) + /// 期望:分段计息,累计利息=4/29收盘+4/30持仓+5/1~5/5持仓 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_010() + { + var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal); + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, oneDay) + }; + + var interest = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); + var expected = Math.Round(oneDay * 2 + secondPeriod, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_010A] 已有前次结算时,浮动利率重置起点应按当前计息段起点计算 + /// --------------------------------------------------------------- + /// 场景:5/3已有EOD;5/6全平;重置周期=3天,取率规则=当前营业日 + /// 期望:应按 5/3~5/6 这一段判断重置,取到 5/6 的 0.20% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_010A() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 5, 3), Principal, 0.001m, 0m) + }; + + var interest = CalcFloatUnwind( + new DateTime(2026, 5, 6), + new DateTime(2026, 5, 6), + 1m, + eodPositions, + InterestRule_Cur); + + Assert.AreEqual(0.002m, interest.FloatRate); + } + + /// + /// [FLOAT_UNWIND_011] 算头不算尾 - 跨重置周期全平(中间无收盘) + /// --------------------------------------------------------------- + /// 场景:4/28起息;5/6全平(4/29未收盘) + /// 期望:3天@0.10% + 5天@0.20% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_011() + { + var eodPositions = new List(); + var interest = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + 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); + AssertInterestEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 场景2:浮动利率算头不算尾 - 收盘归档场景 + + /// + /// [FLOAT_EOD_001] 算头不算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:4/28执行收盘EOD归档(首次收盘) + /// 期望:当日收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_001() + { + var interest = CalcFloatEod(new DateTime(2026, 4, 28), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_002] 算头不算尾 - 首日收盘,次日全平 + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/29盘中全平 + /// 期望:总利息=历史1天+当期0天=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_003] 算头不算尾 - 首日收盘,次日平仓50% + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/29盘中平仓一半 + /// 期望:总利息=(历史1天+当期0天)*50%=0.5天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); + var expected = ExpectedInterestWithPreEod(0, FixedRate, 0.001m, Principal, ExpectedInterest(1, FixedRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29) + /// --------------------------------------------------------------- + /// 场景:4/28和4/29连续两个工作日收盘归档 + /// 期望:4/29收盘累计利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_004() + { + var eod1 = CalcFloatEod(new DateTime(2026, 4, 28), new List()); + var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected1, eod1.InterestAmount); + + var eod2 = CalcFloatEod(new DateTime(2026, 4, 29), new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1) + }); + var expected2 = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected2, eod2.InterestAmount); + } + + /// + /// [FLOAT_EOD_005] 算头不算尾 - 首日收盘后第3日收盘 + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/30执行收盘(4/29未收盘) + /// 期望:4/29收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_005() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) + }; + var interest = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_006] 算头不算尾 - 到期日收盘不算尾 + /// --------------------------------------------------------------- + /// 场景:4/28起息,2027-04-27到期 + /// 操作:2027-04-27执行收盘归档 + /// 期望:到期日收盘利息=0(不算尾) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_006() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) + }; + var interest = CalcFloatEod(new DateTime(2027, 4, 27), eodPositions); + AssertInterestEqual(0m, interest.InterestAmount); + } + + #endregion + + #region 场景3:浮动利率算头不算尾 - 当前营业日规则(interest_rule=0) + + /// + /// [FLOAT_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:interest_rule=0(当前营业日),4/29盘中全平 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_CUR_001() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, InterestRule_Cur); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平 + /// --------------------------------------------------------------- + /// 场景:interest_rule=0(当前营业日),4/30盘中全平 + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_CUR_002() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, InterestRule_Cur); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:固定利率算头不算尾 + // 维度:T+1/T+0 × 正利率/负利率 × 4业务场景 + // ================================================================ + + #region 场景4:固定利率算头不算尾 - T+1固定正利率(前一营业日,正利率0.75%) + + /// + /// [FIX_POS_T1_001] T+1固定正利率算头不算尾 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 业务场景1:固定利率未收盘平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28起息,4/29盘中全平 + /// 期望:计息天数=1天,利息=1*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_001() + { + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_002] T+1固定正利率算头不算尾 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=历史1天+当期0天=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_003] T+1固定正利率算头不算尾 - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterestWithPreEod(0, FixedRatePositive, 0m, Principal, ExpectedInterest(1, FixedRatePositive, 0m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_004] T+1固定正利率算头不算尾 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/29部分平仓50%;经过4/29收盘、4/30收盘;5/6全部平仓剩余50% + /// 期望:4/29平仓=0.5天+5/6平仓=剩余×累计天数 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRatePositive); + var expectedUnwind1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%(经过4/29收盘和4/30收盘) + // newCalcLast=true: 4/30~5/6=7天 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive, Principal * 0.5m, newCalcLast: true); + // newCalcLast=true强制算尾: 4/30~5/6=7天 + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景6:固定利率算头不算尾 - T+1固定负利率(前一营业日,负利率-1.05%) + + /// + /// [FIX_NEG_T1_001] T+1固定负利率算头不算尾 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 业务场景1:固定利率未收盘平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28起息,4/29盘中全平 + /// 期望:计息天数=1天,利息=1*(-1.05%)*1000/365(负利息) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_001() + { + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterest(1, FixedRateNegative, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_002] T+1固定负利率算头不算尾 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(负利息) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterest(1, FixedRateNegative, 0m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_003] T+1固定负利率算头不算尾 - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*(-1.05%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterestWithPreEod(0, FixedRateNegative, 0m, Principal, ExpectedInterest(1, FixedRateNegative, 0m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_004] T+1固定负利率算头不算尾 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRateNegative); + var expectedUnwind1 = ExpectedInterest(1, FixedRateNegative, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%,newCalcLast=true + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRateNegative, Principal * 0.5m, newCalcLast: true); + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRateNegative, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:浮动利率算头不算尾 - 扩展维度 + // 维度:T+1/T+0 × 加减点 × 前一/当前营业日 × 单/复利 + // ================================================================ + + #region 场景8:浮动利率算头不算尾 - T+1浮动减点(当前营业日,复利) + + private const decimal FloatMinusRate = -0.021m; // 浮动减点固定端-2.10%(Excel场景) + private const decimal FloatPlusRate = 0.0025m; // 浮动加点固定端+0.25%(Excel场景) + + /// + /// [FLT_MINUS_T1_CUR_002] T+1浮动减点算头不算尾(当前营业日) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=0, FixedRate=-2.10%, InterestType=复利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(固定-2.10%+浮动0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_003] T+1浮动减点算头不算尾(当前营业日) - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_004] T+1浮动减点算头不算尾(当前营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%,复利从头算 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:8天, 每3天重置, [28-30]@-2.0%, [1-5]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 8; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景10:浮动利率算头不算尾 - T+1浮动减点(前一营业日,复利) + + /// + /// [FLT_MINUS_T1_PRE_002] T+1浮动减点算头不算尾(前一营业日) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=复利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(-2.10%+0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_003] T+1浮动减点算头不算尾(前一营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_004] T+1浮动减点算头不算尾(前一营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_004() + { + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:8天, 每3天重置, [28-30]@-2.0%, [1-5]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 8; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景12:浮动利率算头不算尾 - T+1浮动减点(前一营业日,单利) + + /// + /// [FLT_MINUS_T1_PRE_SI_002] T+1浮动减点算头不算尾(前一营业日,单利) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(-2.10%+0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_003] T+1浮动减点算头不算尾(前一营业日,单利) - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_004] T+1浮动减点算头不算尾(前一营业日,单利) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_004() + { + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利, Principal * 0.5m); + // 单利: 6天(EOD后), [30]@0.001 + [1-5]@0.002 → 1@-2.0% + 5@-1.9% + var raw = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m) + + Principal * 0.5m * (FloatMinusRate + 0.001m) * 1 / AnnualDays + + Principal * 0.5m * (FloatMinusRate + 0.002m) * 5 / AnnualDays; + var expectedTotal = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景14:固定利率算头不算尾 - 收盘归档场景 + + /// + /// [FIX_EOD_001] 固定利率算头不算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:4/28执行收盘EOD归档(首次收盘) + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 期望:当日收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_001() + { + var interest = CalcFixedEod(new DateTime(2026, 4, 28), new List(), + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_EOD_002] 固定利率算头不算尾 - 连续收盘 + /// --------------------------------------------------------------- + /// 场景:4/28和4/29连续两个工作日收盘归档 + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 期望:4/28和4/29收盘利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_002() + { + var eod1 = CalcFixedEod(new DateTime(2026, 4, 28), new List(), + InterestRule_Pre, FixedRatePositive); + var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1, eod1.InterestAmount); + + var eod2 = CalcFixedEod(new DateTime(2026, 4, 29), new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, expected1) + }, InterestRule_Pre, FixedRatePositive); + var expected2 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1+expected2, eod2.InterestAmount); + } + + /// + /// [FIX_EOD_003] 固定利率算头不算尾 - 到期日收盘不算尾 + /// --------------------------------------------------------------- + /// 场景:4/28起息,2027-04-27到期 + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 操作:2027-04-27执行收盘归档 + /// 期望:到期日收盘利息=0(不算尾) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0m, 10m) + }; + var interest = CalcFixedEod(new DateTime(2027, 4, 27), eodPositions, + InterestRule_Pre, FixedRatePositive); + AssertInterestEqual(0m, interest.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:算头算尾(InterestCalcMode="11") + // 这些场景在Excel中标记为"通过",同样需要单元测试覆盖 + // 口径说明:"11"=算头算尾(含起息日和到期日/操作日) + // 与算头不算尾("10")的关键区别: + // - "10":计息区间 S=startDate, E=valueDate-1 → days天 + // - "11":计息区间 S=startDate, E=valueDate → days+1天 + // ================================================================ + + #region 场景A:固定利率算头算尾 - T+1固定正利率(前一营业日,正利率0.75%) + + /// + /// [FIX_POS_T1_11_001] 算头算尾 T+1固定正利率 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28起息,4/29盘中全平 + /// 算头算尾:S=4/28, E=4/29 → 2天 + /// 期望:利息=2*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_001() + { + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_002] 算头算尾 T+1固定正利率 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 算头算尾:历史1天+当期1天=2天 + /// 期望:总利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_003] 算头算尾 T+1固定正利率 - 部分平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 算头算尾:历史1天+当期1天=2天×50% + /// 期望:利息=0.5*2*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m,ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal*0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_004] 算头算尾 T+1固定正利率 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// 算头算尾:4/29半平=2天×50%;5/6全平剩余=8天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_004() + { + // 4/29部分平仓50%(算头算尾→2天) + var unwind1 = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRatePositive); + var expectedUnwind1 = ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 5/6全平剩余50%(EOD=4/29, 算头算尾→4/29~5/6=8天) + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive, Principal * 0.5m, newCalcLast: true); + // 算头算尾: 4/29~5/6(算尾)=8天(newCalcLast=true无影响) + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景C:浮动利率算头算尾 - T+1浮动减点(当前营业日,复利) + + /// + /// [FLT_MINUS_T1_CUR_11_002] 算头算尾 T+1浮动减点(当前营业日) - 收盘后次日全部平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_11_003] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_11_004] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓后全平, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:9天, 每3天重置, [28-30]@-2.0%, [1-3,4-6]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景E:浮动利率算头算尾 - T+1浮动减点(前一营业日,复利) + + /// + /// [FLT_MINUS_T1_PRE_11_002] 算头算尾 T+1浮动减点(前一营业日) - 收盘后次日全部平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_11_003] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_11_004] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 复利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:9天, 每3天重置, [28-30]@-2.0%, [1-3,4-6]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景G:浮动利率算头算尾 - T+1浮动减点(单利) + + /// + /// [FLT_MINUS_T1_PRE_SI_11_002] 算头算尾 T+1浮动减点(单利) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:利息=2*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_11_003] 算头算尾 T+1浮动减点(单利) - 部分平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*2*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 1m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal*0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_11_004] 算头算尾 T+1浮动减点(单利) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利, Principal * 0.5m); + // 单利: 9天, [28,29,30]@0.001 + [1-6]@0.002 → 1@-2.0% + 6@-1.9% + var raw = Principal * 0.5m * (FloatMinusRate + 0.001m) * 3 / AnnualDays + + Principal * 0.5m * (FloatMinusRate + 0.002m) * 6 / AnnualDays; + var expectedTotal = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + // ================================================================ + // 算头算尾("11")通用调用方法 + // ================================================================ + + #region 算头算尾("11")辅助方法 + + private swap_flow_event CalcFixedUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + return CalcFixedUnwind11(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, posiNotional, closeList, newCalcLast); + } + + private swap_flow_event CalcFixedUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule, decimal fixedRate, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("11", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFloatUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + InterestTypeEnum interestType, decimal posiNotional = Principal, + List closeList = null, bool newCalcLast = false) + { + return CalcFloatUnwind11(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, interestType, posiNotional, closeList: closeList, newCalcLast: newCalcLast); + } + + private swap_flow_event CalcFloatUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule, decimal fixedRate, InterestTypeEnum interestType, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("11", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + } +}