Files
zszq-trs/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs
T
张名锐 2ee38384ac fix(swap): 修复预付金腿计息基数计算问题
- 在多个单元测试文件中添加InterestIncomeSum字段以保持数据一致性
- 修改GLMS20260105PartialCloseDividendBugTest测试,改进EOD快照验证逻辑
- 修复SwapDealService中预付金腿的orginPv计算逻辑,使用上一日保证金本金作为基准
- 更新SwapPositionComposeScenarioTest中的测试数据结构和利率设置
- 修正平仓日利息计算精度问题,使用Math.Round确保计算准确性
2026-08-09 22:28:40 +08:00

1762 lines
86 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 System.Security.Principal;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换利息计算单元测试 - 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)
/// ================================================================
/// </summary>
[TestClass]
public class GetInterestsUnitTest_T1
{
#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 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<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, 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 测试数据构建器
/// <summary>
/// 创建测试用交易对象
/// </summary>
/// <param name="interestCalcMode">计息口径:"10"=算头不算尾</param>
/// <param name="interestRule">取率规则:-1=前一营业日,0=当前营业日</param>
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
};
}
/// <summary>
/// 创建浮动利率测试用持仓对象(Excel场景:FR007+固定利率)
/// </summary>
private static swap_position CreateFloatInterestPosition(
int interestRule = InterestRule_Pre,
InterestTypeEnum interestType = InterestTypeEnum.单利,
decimal fixedRate = 0.01m,
SwapDirectionEnum direction = SwapDirectionEnum.收取)
{
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)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)
};
}
/// <summary>
/// 创建固定利率测试用持仓对象(无浮动利率标的,纯固定利率)
/// 对应Excel中的固定利率场景
/// </summary>
private static swap_position CreateFixedInterestPosition(
decimal fixedRate = 0.0075m,
int interestRule = InterestRule_Pre,
SwapDirectionEnum direction = SwapDirectionEnum.收取)
{
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)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)
};
}
/// <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,
InterestIncomeSum = interestSum,
InterestProfitSum = interestSum
};
}
/// <summary>
/// 计算期望利息金额(先累加原始值,最后一次性舍入,比较时比生产少2位容错)
/// </summary>
/// <summary>
/// 容忍末位差异的利息比较(允许相差2位)
/// </summary>
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);
}
/// <summary>
/// 计算含预EOD利息的总期望利息(比生产少2位精度容错)
/// </summary>
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调用方法
/// <summary>
/// 通用浮动利率平仓计算(不含eodPositions
/// </summary>
private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate,
decimal closePercent, int interestRule = InterestRule_Pre,
decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利,
List<swap_flow_event> closeList = null, bool newCalcLast = false)
{
return CalcFloatUnwind(valueDate, unwindDate, closePercent,
new List<eod_swap_position>(), interestRule, fixedRate, interestType, closeList: closeList, newCalcLast: newCalcLast);
}
/// <summary>
/// 通用浮动利率平仓计算(含eodPositions
/// </summary>
private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate,
decimal closePercent, List<eod_swap_position> eodPositions,
int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m,
InterestTypeEnum interestType = InterestTypeEnum.单利,
decimal posiNotional = Principal, List<swap_flow_event> 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<swap_position> { 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];
}
/// <summary>
/// 通用浮动利率收盘计算
/// </summary>
private swap_flow_event CalcFloatEod(DateTime valueDate,
List<eod_swap_position> eodPositions, int interestRule = InterestRule_Pre,
decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利,
List<swap_flow_event> 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<swap_position> { 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];
}
/// <summary>
/// 通用浮动利率自动互换计算
/// </summary>
private swap_flow_event CalcFloatAutoSwap(DateTime valueDate,
List<eod_swap_position> eodPositions, decimal closePercent = 1m,
int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m,
InterestTypeEnum interestType = InterestTypeEnum.单利,
List<swap_flow_event> 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<swap_position> { 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];
}
/// <summary>
/// 通用固定利率平仓计算
/// </summary>
private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate,
decimal closePercent, int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m,
List<swap_flow_event> closeList = null, bool newCalcLast = false)
{
return CalcFixedUnwind(valueDate, unwindDate, closePercent,
new List<eod_swap_position>(), interestRule, fixedRate, closeList: closeList, newCalcLast: newCalcLast);
}
/// <summary>
/// 通用固定利率平仓计算(含eodPositions
/// </summary>
private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate,
decimal closePercent, List<eod_swap_position> eodPositions,
int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m,
decimal posiNotional = Principal, List<swap_flow_event> 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<swap_position> { 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];
}
/// <summary>
/// 通用固定利率收盘计算
/// </summary>
private swap_flow_event CalcFixedEod(DateTime valueDate,
List<eod_swap_position> eodPositions, int interestRule = InterestRule_Pre,
decimal fixedRate = 0.0075m, List<swap_flow_event> 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<swap_position> { 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:浮动利率算头不算尾 - 盘中平仓场景
/// <summary>
/// [FLOAT_UNWIND_001] 算头不算尾 - 首日起息日平仓
/// ---------------------------------------------------------------
/// 场景:StartDate=4/28盘中执行全平
/// 口径:算头不算尾
/// - 算头:S=4/28
/// - 不算尾:E=4/27(操作日前一日)
/// - 计息天数 = 0天
/// 期望:利息=0
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FLOAT_UNWIND_002] 算头不算尾 - 次日全平(基准场景)
/// ---------------------------------------------------------------
/// 场景:4/28未平仓;4/29盘中全平
/// 口径:算头不算尾 → 计息区间:4/28(1天)
/// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FLOAT_UNWIND_003] 算头不算尾 - 次日平仓50%
/// ---------------------------------------------------------------
/// 场景:4/28未平仓;4/29盘中平仓一半
/// 口径:算头不算尾,计息天数=1天
/// 期望:利息=0.5*1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FLOAT_UNWIND_004] 算头不算尾 - 第3日全平(跨周末)
/// ---------------------------------------------------------------
/// 场景:4/28未平仓;4/30盘中全平
/// 口径:算头不算尾 → 计息区间:4/28~4/29(2天)
/// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FLOAT_UNWIND_005] 算头不算尾 - 第3日平仓50%(跨周末)
/// ---------------------------------------------------------------
/// 场景:4/28未平仓;4/30盘中平仓一半
/// 口径:算头不算尾,计息天数=2天
/// 期望:利息=0.5*2*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [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天利息
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_UNWIND_007] 算头不算尾 - 第3日直接收盘(已有前日EOD)
/// ---------------------------------------------------------------
/// 场景:4/28未平仓;4/29已收盘归档;4/30收盘
/// 期望:4/30收盘待实现利息=3天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_007()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_UNWIND_008] 算头不算尾 - 次日自动互换
/// ---------------------------------------------------------------
/// 场景:4/29执行"自动互换"
/// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_008()
{
var interest = CalcFloatAutoSwap(new DateTime(2026, 4, 29), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
AssertInterestEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [FLOAT_UNWIND_009] 算头不算尾 - 自动互换后次日平仓
/// ---------------------------------------------------------------
/// 场景:4/29已自动互换;4/30执行"全平"
/// 期望:计息天数=0天,利息=0
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_009()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_UNWIND_010] 算头不算尾 - 跨重置周期全平(中间有收盘)
/// ---------------------------------------------------------------
/// 场景:4/29收盘归档;5/6全平(跨周期)
/// 期望:分段计息,累计利息=4/29收盘+4/30持仓+5/1~5/5持仓
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_010()
{
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)
};
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);
}
/// <summary>
/// [FLOAT_UNWIND_010A] 已有前次结算时,浮动利率重置起点应按当前计息段起点计算
/// ---------------------------------------------------------------
/// 场景:5/3已有EOD;5/6全平;重置周期=3天,取率规则=当前营业日
/// 期望:应按 5/3~5/6 这一段判断重置,取到 5/6 的 0.20%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_010A()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_UNWIND_011] 算头不算尾 - 跨重置周期全平(中间无收盘)
/// ---------------------------------------------------------------
/// 场景:4/28起息;5/6全平(4/29未收盘)
/// 期望:3天@0.10% + 5天@0.20%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_UNWIND_011()
{
var eodPositions = new List<eod_swap_position>();
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:浮动利率算头不算尾 - 收盘归档场景
/// <summary>
/// [FLOAT_EOD_001] 算头不算尾 - 首日收盘归档
/// ---------------------------------------------------------------
/// 场景:4/28执行收盘EOD归档(首次收盘)
/// 期望:当日收盘利息=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_EOD_001()
{
var interest = CalcFloatEod(new DateTime(2026, 4, 28), new List<eod_swap_position>());
var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal);
AssertInterestEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [FLOAT_EOD_002] 算头不算尾 - 首日收盘,次日全平
/// ---------------------------------------------------------------
/// 场景:4/28已收盘;4/29盘中全平
/// 期望:总利息=历史1天+当期0天=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_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 = 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);
}
/// <summary>
/// [FLOAT_EOD_003] 算头不算尾 - 首日收盘,次日平仓50%
/// ---------------------------------------------------------------
/// 场景:4/28已收盘;4/29盘中平仓一半
/// 期望:总利息=(历史1天+当期0天)*50%=0.5天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_EOD_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29)
/// ---------------------------------------------------------------
/// 场景:4/28和4/29连续两个工作日收盘归档
/// 期望:4/29收盘累计利息=2天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_EOD_004()
{
var eod1 = CalcFloatEod(new DateTime(2026, 4, 28), new List<eod_swap_position>());
var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal);
AssertInterestEqual(expected1, eod1.InterestAmount);
var eod2 = CalcFloatEod(new DateTime(2026, 4, 29), new List<eod_swap_position>
{
CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1)
});
var expected2 = ExpectedInterest(2, FixedRate, 0.001m, Principal);
AssertInterestEqual(expected2, eod2.InterestAmount);
}
/// <summary>
/// [FLOAT_EOD_005] 算头不算尾 - 首日收盘后第3日收盘
/// ---------------------------------------------------------------
/// 场景:4/28已收盘;4/30执行收盘(4/29未收盘)
/// 期望:4/29收盘利息=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_EOD_005()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLOAT_EOD_006] 算头不算尾 - 到期日收盘不算尾
/// ---------------------------------------------------------------
/// 场景:4/28起息,2027-04-27到期
/// 操作:2027-04-27执行收盘归档
/// 期望:到期日收盘利息=0(不算尾)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLOAT_EOD_006()
{
var eodPositions = new List<eod_swap_position>
{
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)
/// <summary>
/// [FLOAT_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平
/// ---------------------------------------------------------------
/// 场景:interest_rule=0(当前营业日),4/29盘中全平
/// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FLOAT_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平
/// ---------------------------------------------------------------
/// 场景:interest_rule=0(当前营业日),4/30盘中全平
/// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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%
/// <summary>
/// [FIX_POS_T1_001] T+1固定正利率算头不算尾 - 未收盘平仓
/// ---------------------------------------------------------------
/// 业务场景1:固定利率未收盘平仓
/// 参数:interest_rule=-1, FixedRate=0.75%
/// 操作:4/28起息,4/29盘中全平
/// 期望:计息天数=1天,利息=1*0.75%*1000/365
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FIX_POS_T1_002] T+1固定正利率算头不算尾 - 收盘后次日全部平仓
/// ---------------------------------------------------------------
/// 业务场景2:收盘后次日全部平仓
/// 参数:interest_rule=-1, FixedRate=0.75%
/// 操作:4/28收盘归档;4/29盘中全平
/// 期望:总利息=历史1天+当期0天=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_POS_T1_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_POS_T1_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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平仓=剩余×累计天数
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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%
/// <summary>
/// [FIX_NEG_T1_001] T+1固定负利率算头不算尾 - 未收盘平仓
/// ---------------------------------------------------------------
/// 业务场景1:固定利率未收盘平仓
/// 参数:interest_rule=-1, FixedRate=-1.05%
/// 操作:4/28起息,4/29盘中全平
/// 期望:计息天数=1天,利息=1*(-1.05%)*1000/365(负利息)
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FIX_NEG_T1_002] T+1固定负利率算头不算尾 - 收盘后次日全部平仓
/// ---------------------------------------------------------------
/// 业务场景2:收盘后次日全部平仓
/// 参数:interest_rule=-1, FixedRate=-1.05%
/// 操作:4/28收盘归档;4/29盘中全平
/// 期望:总利息=1天(负利息)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_NEG_T1_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_NEG_T1_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FIX_NEG_T1_004] T+1固定负利率算头不算尾 - 部分平仓后经过数日再全部平仓
/// ---------------------------------------------------------------
/// 业务场景4:部分平仓一次后,经过数日再全部平仓
/// 参数:interest_rule=-1, FixedRate=-1.05%
/// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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场景)
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_CUR_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_CUR_003] T+1浮动减点算头不算尾(当前营业日) - 部分平仓
/// ---------------------------------------------------------------
/// 业务场景3:部分平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_CUR_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_CUR_004] T+1浮动减点算头不算尾(当前营业日) - 部分平仓后全平
/// ---------------------------------------------------------------
/// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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浮动减点(前一营业日,复利)
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_PRE_003] T+1浮动减点算头不算尾(前一营业日) - 部分平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_PRE_004] T+1浮动减点算头不算尾(前一营业日) - 部分平仓后全平
/// ---------------------------------------------------------------
/// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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浮动减点(前一营业日,单利)
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_PRE_SI_004] T+1浮动减点算头不算尾(前一营业日,单利) - 部分平仓后全平
/// ---------------------------------------------------------------
/// 业务场景4:部分平仓一次后,经过数日再全部平仓
/// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利
/// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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:固定利率算头不算尾 - 收盘归档场景
/// <summary>
/// [FIX_EOD_001] 固定利率算头不算尾 - 首日收盘归档
/// ---------------------------------------------------------------
/// 场景:4/28执行收盘EOD归档(首次收盘)
/// 参数:FixedRate=0.75%, interest_rule=-1
/// 期望:当日收盘利息=1天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_EOD_001()
{
var interest = CalcFixedEod(new DateTime(2026, 4, 28), new List<eod_swap_position>(),
InterestRule_Pre, FixedRatePositive);
var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal);
AssertInterestEqual(expected, interest.InterestAmount);
}
/// <summary>
/// [FIX_EOD_002] 固定利率算头不算尾 - 连续收盘
/// ---------------------------------------------------------------
/// 场景:4/28和4/29连续两个工作日收盘归档
/// 参数:FixedRate=0.75%, interest_rule=-1
/// 期望:4/28和4/29收盘利息=2天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_EOD_002()
{
var eod1 = CalcFixedEod(new DateTime(2026, 4, 28), new List<eod_swap_position>(),
InterestRule_Pre, FixedRatePositive);
var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal);
AssertInterestEqual(expected1, eod1.InterestAmount);
var eod2 = CalcFixedEod(new DateTime(2026, 4, 29), new List<eod_swap_position>
{
CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, expected1)
}, InterestRule_Pre, FixedRatePositive);
var expected2 = ExpectedInterest(1, FixedRatePositive, 0m, Principal);
AssertInterestEqual(expected1+expected2, eod2.InterestAmount);
}
/// <summary>
/// [FIX_EOD_003] 固定利率算头不算尾 - 到期日收盘不算尾
/// ---------------------------------------------------------------
/// 场景:4/28起息,2027-04-27到期
/// 参数:FixedRate=0.75%, interest_rule=-1
/// 操作:2027-04-27执行收盘归档
/// 期望:到期日收盘利息=0(不算尾)
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_EOD_003()
{
var eodPositions = new List<eod_swap_position>
{
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%
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[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);
}
/// <summary>
/// [FIX_POS_T1_11_002] 算头算尾 T+1固定正利率 - 收盘后次日全部平仓
/// ---------------------------------------------------------------
/// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75%
/// 操作:4/28收盘归档;4/29盘中全平
/// 算头算尾:历史1天+当期1天=2天
/// 期望:总利息=2天
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_POS_T1_11_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FIX_POS_T1_11_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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浮动减点(当前营业日,复利)
/// <summary>
/// [FLT_MINUS_T1_CUR_11_002] 算头算尾 T+1浮动减点(当前营业日) - 收盘后次日全部平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_CUR_11_003] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_CUR_11_004] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓后全平, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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浮动减点(前一营业日,复利)
/// <summary>
/// [FLT_MINUS_T1_PRE_11_002] 算头算尾 T+1浮动减点(前一营业日) - 收盘后次日全部平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_PRE_11_003] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓, 复利从头算
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [FLT_MINUS_T1_PRE_11_004] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓后全平
/// ---------------------------------------------------------------
/// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 复利
/// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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浮动减点(单利)
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_002()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_003()
{
var eodPositions = new List<eod_swap_position>
{
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);
}
/// <summary>
/// [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%
/// ---------------------------------------------------------------
/// </summary>
[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<eod_swap_position>
{
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<swap_flow_event> closeList = null,
bool newCalcLast = false)
{
return CalcFixedUnwind11(valueDate, unwindDate, closePercent,
new List<eod_swap_position>(), interestRule, fixedRate, posiNotional, closeList, newCalcLast);
}
private swap_flow_event CalcFixedUnwind11(DateTime valueDate, DateTime unwindDate,
decimal closePercent, List<eod_swap_position> eodPositions,
int interestRule, decimal fixedRate,
decimal posiNotional = Principal, List<swap_flow_event> 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<swap_position> { 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<swap_flow_event> closeList = null, bool newCalcLast = false)
{
return CalcFloatUnwind11(valueDate, unwindDate, closePercent,
new List<eod_swap_position>(), interestRule, fixedRate, interestType, posiNotional, closeList: closeList, newCalcLast: newCalcLast);
}
private swap_flow_event CalcFloatUnwind11(DateTime valueDate, DateTime unwindDate,
decimal closePercent, List<eod_swap_position> eodPositions,
int interestRule, decimal fixedRate, InterestTypeEnum interestType,
decimal posiNotional = Principal, List<swap_flow_event> 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<swap_position> { 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
}
}