Files
zszq-trs/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs
T
2026-05-11 18:59:56 +08:00

1412 lines
66 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Models;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换利息计算单元测试
/// ================================================================
/// 测试口径说明:
/// "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
/// ================================================================
/// </summary>
[TestClass]
public class GetInterestsUnitTest
{
#region 内部类:浮动利率模拟服务
/// <summary>
/// StubSwapDealService - 模拟浮动利率获取
/// 用于单元测试中预置FR007价格,避免依赖外部数据源
/// </summary>
private sealed class StubSwapDealService : SwapDealService
{
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary<DateTime, double> 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<DateTime, double> _floatRates;
[TestInitialize]
public void Init()
{
// 预置FR007价格数据
_floatRates = new Dictionary<DateTime, double>
{
[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 测试数据构建器
/// <summary>
/// 创建测试用交易对象
/// </summary>
/// <param name="interestCalcMode">计息口径:"11"/"10"/"01"/"00"</param>
/// <param name="interestRule">取率规则:-1=前一营业日,0=当前营业日</param>
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
};
}
/// <summary>
/// 创建测试用持仓对象
/// </summary>
/// <param name="interestCalcMode">计息口径</param>
/// <param name="interestRule">取率规则</param>
private static swap_position CreateInterestPosition(string interestCalcMode, int interestRule = InterestRule_Pre)
{
var intervalModels = new List<IntervalModel>
{
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)
};
}
/// <summary>
/// 创建日终持仓记录(EOD归档数据)
/// </summary>
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
};
}
/// <summary>
/// 计算期望利息金额
/// 公式:本金 × (固定利率 + 浮动利率) × 计息天数 ÷ 年化天数
/// </summary>
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调用方法
/// <summary>
/// 通用平仓计算(不含eodPositions
/// </summary>
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<eod_swap_position>(), interestRule);
}
/// <summary>
/// 通用平仓计算(含eodPositions
/// </summary>
private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate,
decimal closePercent, List<eod_swap_position> 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<swap_position> { position },
Principal, 0, 0,
Principal, closePercent,
(int)SwapEventTypeEnum.平仓,
false, false, 0, Principal,
false,
false);
Assert.AreEqual(1, interests.Count);
return interests[0];
}
/// <summary>
/// 通用收盘计算
/// settment=true 表示收盘场景
/// </summary>
private swap_flow_event CalcEod(string interestCalcMode, DateTime valueDate,
List<eod_swap_position> 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<swap_position> { position },
Principal, 0, 0,
Principal, 1m,
(int)SwapEventTypeEnum.平仓,
false, false, 0, Principal,
false,
true); // settment=true 表示收盘
Assert.AreEqual(1, interests.Count);
return interests[0];
}
/// <summary>
/// 通用自动互换计算
/// 使用SwapEventTypeEnum.自动互换事件类型
/// </summary>
private swap_flow_event CalcAutoSwap(string interestCalcMode, DateTime valueDate,
List<eod_swap_position> 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<swap_position> { 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
/// ================================================================ */
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// 期望:分段计算利息
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_11_EOD_001()
{
var interest = CalcEod("11", new DateTime(2026, 4, 28), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
Assert.AreEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_11_EOD_002()
{
var eodPositions = new List<eod_swap_position>
{
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 盘中平仓场景
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [10_004] 算头不算尾 - 次日全平后收盘
/// ---------------------------------------------------------------
/// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中全平;2026-04-29 收盘
/// 操作:
/// 1. 2026-04-29 盘中执行"全平" → 计息1天
/// 2. 2026-04-29 执行收盘归档 → 待实现利息=0
/// 期望:
/// - 全平应计利息=1天
/// - 收盘待实现利息=0(因持仓已不存在)
/// ---------------------------------------------------------------
/// </summary>
[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(持仓已不存在)");
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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天)
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m,
ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m))
};
var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions);
var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m);
Assert.AreEqual(expectedEod, eodInterest.InterestAmount);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_008()
{
// 4月29日收盘归档后,4月30日收盘
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [10_009] 算头不算尾 - 次日自动互换
/// ---------------------------------------------------------------
/// 场景:2026-04-29 执行"自动互换"
/// 背景:自动互换是互换交易的一种定期重置操作
/// 操作:2026-04-29 执行"自动互换"
/// 取率:FR007@2026-04-27=0.10%
/// 口径:算头不算尾
/// 期望:
/// - 计息天数=1天
/// - 利息=1*(1.00%+0.10%)*1000/365
/// - 当日收盘待实现利息=0(持仓已互换)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_009()
{
var interest = CalcAutoSwap("10", new DateTime(2026, 4, 29), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
Assert.AreEqual(expected, interest.InterestAmount);
Console.WriteLine("自动互换后,当日收盘待实现利息=0");
}
/// <summary>
/// [10_010] 算头不算尾 - 自动互换后次日平仓
/// ---------------------------------------------------------------
/// 场景:2026-04-29 已发生自动互换;2026-04-30 执行"全平/收益结算"
/// 背景:自动互换已将持仓重置,累计利息清零
/// 操作:valueDate=2026-04-30,执行"全平"
/// 口径:算头不算尾
/// 期望:计息天数=0天,利息=0(持仓已互换)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_010()
{
// 4月29日自动互换后的eodPosition(自动互换后累计利息清零)
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_011()
{
// 4月29日收盘归档
var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal);
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_012()
{
// 4/28起息,无EOD持仓(4/29未收盘)
var eodPositions = new List<eod_swap_position>();
// 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
/// <summary>
/// [10_PRE_EOD_001] 算头不算尾 - 首日收盘归档(文档4.2节)
/// ---------------------------------------------------------------
/// 场景:2026-04-28 收盘
/// 操作:执行 2026-04-28 EOD
/// 取率日:2026-04-27FR007=0.10%
/// 口径:算头不算尾
/// 说明:首日收盘,当日计息1天
/// 期望:当日收盘利息(待实现)=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_EOD_001()
{
var interest = CalcEod("10", new DateTime(2026, 4, 28), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
Assert.AreEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [10_PRE_EOD_002] 算头不算尾 - 首日收盘,次日全平(文档4.2节)
/// ---------------------------------------------------------------
/// 场景:2026-04-28 已收盘;2026-04-29 盘中全平或收益结算
/// 操作:valueDate=2026-04-29 执行"全平/收益结算"
/// 取率日:2026-04-27FR007=0.10%
/// 口径:算头不算尾
/// - 持仓区间:4/28~4/29
/// - 计息区间:4/29-4/28=1天
/// 期望:计息天数=1;利息=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_EOD_002()
{
// 4/28收盘,利息=1天
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [10_PRE_EOD_003] 算头不算尾 - 首日收盘,次日平仓50%(文档4.2节)
/// ---------------------------------------------------------------
/// 场景:2026-04-28 已收盘;2026-04-29 盘中平仓一半
/// 操作:valueDate=2026-04-29 执行"平仓50%"
/// 取率日:2026-04-27FR007=0.10%
/// 口径:算头不算尾
/// 期望:计息天数=1;利息=0.5*1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_PRE_EOD_003()
{
// 4/28收盘,利息=1天
var eodPositions = new List<eod_swap_position>
{
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 代码额外补充的收盘场景
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_001()
{
var interest = CalcEod("10", new DateTime(2026, 4, 28), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
Assert.AreEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_003()
{
// 4/28收盘(算头=1天利息),4/29平仓50%
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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累计利息
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_004()
{
// 4月28日收盘(利息=1天,算头)
var eod1 = CalcEod("10", new DateTime(2026, 4, 28), new List<eod_swap_position>());
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<eod_swap_position>
{
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);
}
/// <summary>
/// [10_EOD_005] 算头不算尾 - 首日收盘后第3日收盘
/// ---------------------------------------------------------------
/// 场景:2026-04-28 已收盘归档;2026-04-30 执行收盘归档
/// 背景:4/29(周三)未执行收盘归档
/// 操作:执行 2026-04-30 收盘归档
/// 口径:算头不算尾
/// 期望:4/29收盘利息=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_005()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [10_EOD_006] 算头不算尾 - 到期日收盘不算尾
/// ---------------------------------------------------------------
/// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate
/// 操作:2027-04-27 执行收盘归档
/// 口径:算头不算尾("10"
/// - 算头:首日4/28计息
/// - 不算尾:到期日4/27不计息
/// 期望:到期日收盘利息=0(到期日不算尾)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_10_EOD_006()
{
// 2027-04-26 收盘归档产生的 EOD 持仓
// 假设累计利息为 InterestProfitSum=10
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [11_EOD_006] 算头算尾 - 到期日收盘算尾
/// ---------------------------------------------------------------
/// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate
/// 操作:2027-04-27 执行收盘归档
/// 口径:算头算尾("11"
/// - 算头:首日4/28计息
/// - 算尾:到期日4/27计息
/// 期望:到期日收盘利息=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_11_EOD_006()
{
// 2027-04-26 收盘归档产生的 EOD 持仓
// 假设累计利息为 InterestProfitSum=10
var eodPositions = new List<eod_swap_position>
{
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
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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
/// ================================================================ */
/// <summary>
/// [01_001] 不算头算尾 - 首日起息日平仓
/// ---------------------------------------------------------------
/// 场景:2026-04-28(起息日StartDate)盘中执行全平
/// 口径:不算头算尾
/// - 不算头:S=4/29(起息日次日)
/// - 算尾:E=4/28(操作日)
/// - 计息天数 = 4/28 - 4/29 = -1 → 0天
/// 期望:计息天数=0天,利息=0
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [01_003] 不算头算尾 - 次日平仓50%
/// ---------------------------------------------------------------
/// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半
/// 口径:不算头算尾,计息天数=1天
/// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [01_EOD_001] 不算头算尾 - 首日收盘归档
/// ---------------------------------------------------------------
/// 场景:2026-04-28(起息日)执行收盘EOD归档
/// 口径:不算头算尾
/// - 不算头:S=4/29
/// - 算尾:E=4/28 → 计息天数=0
/// 期望:当日收盘利息=0
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_01_EOD_001()
{
var interest = CalcEod("01", new DateTime(2026, 4, 28), new List<eod_swap_position>());
Assert.AreEqual(0m, interest.InterestAmount);
}
/// <summary>
/// [01_EOD_002] 不算头算尾 - 前日已收盘,次日全平
/// ---------------------------------------------------------------
/// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平
/// 口径:不算头算尾
/// - 不算头:S=4/29
/// - 算尾:E=4/29
/// - 计息天数=1天
/// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_01_EOD_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [01_005] 不算头算尾 - 跨周期全平
/// ---------------------------------------------------------------
/// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平
/// 口径:不算头算尾
/// - 不算头:S=5/7(下一周期起息日)
/// - 算尾:E=5/6
/// - 计息天数 = 5/6 - 5/7 = -1 → 0天
/// 期望:计息天数=0天,利息=0
/// ---------------------------------------------------------------
/// </summary>
[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
/// ================================================================ */
/// <summary>
/// [00_001] 不算头不算尾 - 首日起息日平仓
/// ---------------------------------------------------------------
/// 场景:2026-04-28(起息日StartDate)盘中执行全平
/// 口径:不算头不算尾
/// - 不算头:S=4/29
/// - 不算尾:E=4/27
/// - 计息天数 = 4/27 - 4/29 = -2 → 0天
/// 期望:计息天数=0天,利息=0
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [00_004] 不算头不算尾 - 跨周期全平
/// ---------------------------------------------------------------
/// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平
/// 口径:不算头不算尾
/// - 不算头:S=5/7
/// - 不算尾:E=5/5
/// - 计息天数 = 5/5 - 5/7 = -2 → 0天
/// 期望:计息天数=0天,利息=0
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [00_EOD_001] 不算头不算尾 - 首日收盘归档
/// ---------------------------------------------------------------
/// 场景:2026-04-28(起息日)执行收盘EOD归档
/// 口径:不算头不算尾
/// - 不算头:S=4/29
/// - 不算尾:E=4/27 → 计息天数=0
/// 期望:当日收盘利息=0
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_00_EOD_001()
{
var interest = CalcEod("00", new DateTime(2026, 4, 28), new List<eod_swap_position>());
Assert.AreEqual(0m, interest.InterestAmount);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_00_EOD_002()
{
var eodPositions = new List<eod_swap_position>
{
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
/// ================================================================ */
/// <summary>
/// [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种口径的差异符合预期
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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种口径的差异符合预期
/// ---------------------------------------------------------------
/// </summary>
[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
}
}