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
+
+ }
+}