1761 lines
86 KiB
C#
1761 lines
86 KiB
C#
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,
|
||
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
|
||
|
||
}
|
||
}
|