using Newtonsoft.Json; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// 互换利息计算单元测试 - T+0场景 /// ================================================================ /// T+0定义:起息日(StartDate) = 成交日(TradeDate),不额外加1天 /// TradeDate=2026-04-27, StartDate=2026-04-27 /// ---------------------------------------------------------------- /// 测试口径: /// "10" = 算头不算尾(含起息日,不含操作日) /// "11" = 算头算尾(含起息日和操作日) /// ---------------------------------------------------------------- /// 与T+1的关键差异: /// T+1: StartDate=4/28, 4/29平仓(算头不算尾)→1天 /// T+0: StartDate=4/27, 4/28平仓(算头不算尾)→1天 (所有天数+1) /// ---------------------------------------------------------------- /// Excel覆盖的T+0算头不算尾场景: /// 固定利率:T+0固定正利率、T+0固定负利率 /// 浮动利率:T+0浮动加点(当前营业日/前一营业日/单利) /// 每个场景 × 4业务场景(浮动×3) /// ================================================================ /// [TestClass] public class GetInterestsUnitTest_T0 { #region 内部类:浮动利率模拟服务 private sealed class StubSwapDealService : SwapDealService { private readonly IReadOnlyDictionary _floatRates; public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) { _floatRates = floatRates; } protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) { rate = 0; return false; } if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true; rate = 0; return false; } } #endregion #region 测试常量 private const decimal Principal = 1000m; private const decimal FixedRate = 0.01m; private const decimal FixedRatePositive = 0.0075m; private const decimal FixedRateNegative = -0.0105m; private const decimal FloatMinusRate = -0.021m; private const decimal FloatPlusRate = 0.0025m; private const int AnnualDays = 365; private const int ResetPeriod = 3; private const int ResetPeriodFixed = 1; private const int InterestRule_Pre = -1; private const int InterestRule_Cur = 0; // T+0: StartDate = TradeDate(不额外加1天) private static readonly DateTime TradeDate = new(2026, 4, 27); private static readonly DateTime StartDate = new(2026, 4, 27); // = TradeDate private static readonly DateTime ExerciseDate = new(2027, 4, 27); private SwapDealService _service; private IReadOnlyDictionary _floatRates; [TestInitialize] public void Init() { _floatRates = new Dictionary { [new DateTime(2026, 4, 24)] = 0.001, // InterestRule_Pre: GetNonHolidayDefore(4/26日)→4/24 [new DateTime(2026, 4, 26)] = 0.001, // 新增:T+0前一营业日场景需要 [new DateTime(2026, 4, 27)] = 0.001, [new DateTime(2026, 4, 28)] = 0.001, [new DateTime(2026, 4, 29)] = 0.001, [new DateTime(2026, 4, 30)] = 0.002, [new DateTime(2026, 5, 1)] = 0.002, // 复利从头算需要完整日期范围 [new DateTime(2026, 5, 3)] = 0.002, // 复利重置日取FR007 [new DateTime(2026, 5, 5)] = 0.002, // InterestRule_Pre取率日 [new DateTime(2026, 5, 6)] = 0.002, [new DateTime(2027, 4, 23)] = 0.001, [new DateTime(2027, 4, 24)] = 0.001, [new DateTime(2027, 4, 25)] = 0.001, [new DateTime(2027, 4, 26)] = 0.001, [new DateTime(2027, 4, 27)] = 0.001 }; _service = new StubSwapDealService( new OptUserInfo(0, nameof(GetInterestsUnitTest_T0), OptUserFrom.UnitTest), _floatRates); } #endregion #region 测试数据构建器 private static trade CreateTrade(string interestCalcMode, int interestRule = InterestRule_Pre) { var extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = interestCalcMode, SettlementRules = interestRule }) }; return new trade { id = 1, TradeNumber = "UT-SWAP-INT-T0-001", ClientId = 999998, TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate, ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", trade_extend = extend }; } private static swap_position CreateFloatInterestPosition( int interestRule = InterestRule_Cur, InterestTypeEnum interestType = InterestTypeEnum.单利, decimal fixedRate = 0.01m, SwapDirectionEnum direction = SwapDirectionEnum.收取) { var intervalModels = new List { new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 } }; return new swap_position { id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, InterestDirection = (int)direction, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, IsInitial = true, Invalid = false, InterestType = (int)interestType, IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } private static swap_position CreateFixedInterestPosition( decimal fixedRate = 0.0075m, int interestRule = InterestRule_Cur, SwapDirectionEnum direction = SwapDirectionEnum.收取) { var intervalModels = new List { new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 } }; return new swap_position { id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, InterestDirection = (int)direction, InterestMode = (int)InterestModeEnum.合约名义本金规模, InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = ResetPeriodFixed, interest_rule = interestRule, FloatRateUnderlyingCode = null, InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) { return new eod_swap_position { id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate, ClientId = 999998, FloatRate = floatRate, TdInterestPrincipal = tdPrincipal, PosiNotionalValue = tdPrincipal, InterestIncomeSum = interestSum, InterestProfitSum = interestSum }; } /// /// 计算含预EOD利息的总期望利息(匹配生产代码中间舍入行为) /// --------------------------------------------------------------- /// 生产代码先取 preEod.InterestProfitSum(已舍入到11位的DB值), /// 再加上新期间日度原始利息,最后再舍入一次 /// private static decimal ExpectedInterestWithPreEod( int newDays, decimal fixedRate, decimal floatRate, decimal principal, decimal preEodInterestSum, decimal closePercent) { var yearlyRate = fixedRate + floatRate; var newRawInterest = principal * yearlyRate * newDays / AnnualDays; return Math.Round(preEodInterestSum * closePercent + newRawInterest * closePercent, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } /// /// 容忍末位差异的利息比较(允许相差2位) /// private static void AssertInterestEqual(decimal expected, decimal actual) { var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, string.Format("Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, expected - actual)); } private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) { var yearlyRate = fixedRate + floatRate; var interest = principal * yearlyRate * days / AnnualDays; return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } #endregion #region 通用调用方法 // --- 浮动利率 --- private swap_flow_event CalcFloatUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, decimal closePercent, int interestRule, decimal fixedRate, InterestTypeEnum interestType, List eodPositions = null, decimal posiNotional = Principal, List closeList = null, bool newCalcLast = false) { eodPositions ??= new List(); var td = CreateTrade(calcMode, interestRule); var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, eodPositions, new List { position }, posiNotional, posiNotional, posiNotional, posiNotional, closePercent, (int)SwapEventTypeEnum.平仓, false, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); AssertInterestEqual(1, interests.Count); return interests[0]; } private swap_flow_event CalcFloatEod(string calcMode, DateTime valueDate, int interestRule, decimal fixedRate, InterestTypeEnum interestType, List eodPositions = null, List closeList = null) { eodPositions ??= new List(); var td = CreateTrade(calcMode, interestRule); var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, eodPositions, new List { position }, Principal, Principal, Principal, Principal, 1m, (int)SwapEventTypeEnum.平仓, false, Principal, false, settment: true, newCalcLast: false, closeList: closeList); AssertInterestEqual(1, interests.Count); return interests[0]; } // --- 固定利率 --- private swap_flow_event CalcFixedUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, decimal closePercent, int interestRule, decimal fixedRate, List eodPositions = null, decimal posiNotional = Principal, List closeList = null, bool newCalcLast = false) { eodPositions ??= new List(); var td = CreateTrade(calcMode, interestRule); var position = CreateFixedInterestPosition(fixedRate, interestRule); var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, eodPositions, new List { position }, posiNotional, posiNotional, posiNotional, posiNotional, closePercent, (int)SwapEventTypeEnum.平仓, false, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); AssertInterestEqual(1, interests.Count); return interests[0]; } private swap_flow_event CalcFixedEod(string calcMode, DateTime valueDate, int interestRule, decimal fixedRate, List eodPositions = null, List closeList = null) { eodPositions ??= new List(); var td = CreateTrade(calcMode, interestRule); var position = CreateFixedInterestPosition(fixedRate, interestRule); var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, eodPositions, new List { position }, Principal, Principal, Principal, Principal, 1m, (int)SwapEventTypeEnum.平仓, false, Principal, false, settment: true, newCalcLast: false, closeList: closeList); AssertInterestEqual(1, interests.Count); return interests[0]; } #endregion // ================================================================ // T+0场景:StartDate = TradeDate = 2026-04-27 // 算头不算尾("10"):4/27平仓→0天, 4/28平仓→1天, 4/29平仓→2天 // 算头算尾("11"):4/27平仓→1天, 4/28平仓→2天, 4/29平仓→3天 // ================================================================ #region 1. T+0固定正利率 算头不算尾("10") - interest_rule=0 /// /// [UT_T0_FIX_POS_001] T+0固定正利率算头不算尾-未收盘平仓 /// --------------------------------------------------------------- /// StartDate=4/27, 4/28平仓, 算头不算尾→S=4/27,E=4/27→1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_001() { var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRatePositive); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); } /// /// [UT_T0_FIX_POS_002] T+0固定正利率算头不算尾-收盘后次日全部平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→1+1=2天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(1, FixedRatePositive, 0m, Principal)) }; var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRatePositive, eod); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); } /// /// [UT_T0_FIX_POS_003] T+0固定正利率算头不算尾-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(1, FixedRatePositive, 0m, Principal)) }; var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRatePositive, eod); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), interest.InterestAmount); } /// /// [UT_T0_FIX_POS_004] T+0固定正利率算头不算尾-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平50%→1天×50%; 5/6全平剩余→EOD=4/28, newCalcLast=true /// 算头不算尾,newCalcLast强制算尾: 4/29~5/6=8天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_004() { // 4/28部分平仓50%(算头不算尾→S=4/27,E=4/27→1天) var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRatePositive); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); // 5/6全平剩余50%(EOD=4/28, newCalcLast=true强制算尾: 4/29~5/6=8天) var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) }; var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); var expected = ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); AssertInterestEqual(expected, u2.InterestAmount); } #endregion #region 2. T+0固定负利率 算头不算尾("10") - interest_rule=0 /// /// [UT_T0_FIX_NEG_001] T+0固定负利率算头不算尾-未收盘平仓 /// --------------------------------------------------------------- /// StartDate=4/27, 4/28平仓, 算头不算尾→1天, 负利率-1.05% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_NEG_001() { var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRateNegative); AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); } /// /// [UT_T0_FIX_NEG_002] T+0固定负利率算头不算尾-收盘后次日全部平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→2天, 负利率-1.05% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_NEG_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(1, FixedRateNegative, 0m, Principal)) }; var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRateNegative, eod); AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); } /// /// [UT_T0_FIX_NEG_003] T+0固定负利率算头不算尾-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_NEG_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) }; var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRateNegative, eod); AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal)*0.5m, interest.InterestAmount); } /// /// [UT_T0_FIX_NEG_004] T+0固定负利率算头不算尾-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平50%→1天×50%; 5/6全平剩余→newCalcLast=true, 8天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_NEG_004() { var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRateNegative); AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) }; var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Cur, FixedRateNegative, eod, Principal * 0.5m, newCalcLast: true); var expected = ExpectedInterestWithPreEod(8, FixedRateNegative, 0m, Principal * 0.5m, ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m), 1m); AssertInterestEqual(expected, u2.InterestAmount); } #endregion #region 3. T+0浮动加点(当前营业日,复利)算头不算尾("10") /// /// [UT_T0_FLT_PLUS_CUR_002] T+0浮动加点(当前营业日,复利)算头不算尾-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→2天, FR007+0.25% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_CUR_003] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头不算尾→2天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_CUR_004] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平50%→1天; 5/6全平剩余→复利从头算9天[27-29][30-2][3-5] /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_004() { var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List(); var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); // 复利从头算:9天, 每3天重置, [27-29]@0.35%, [30-2,3-5]@0.45% var principal = Principal * 0.5m; var rate1 = FloatPlusRate + 0.001m; var rate2 = FloatPlusRate + 0.002m; decimal interest = 0m, dynomic = principal; for (int d = 0; d < 9; d++) { if (d % 3 == 0) dynomic = principal + interest; interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; } var expected = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected, u2.InterestAmount); } #endregion #region 4. T+0浮动加点(前一营业日,复利)算头不算尾("10") /// /// [UT_T0_FLT_PLUS_PRE_002] T+0浮动加点(前一营业日,复利)算头不算尾-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_PRE_003] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头不算尾→1天, 复利从头算 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_PRE_004] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→1天@0.10%; 5/6全平→复利从头算9天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_004() { var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); // 复利从头算:9天, 每3天重置, [27-29]@0.001, [30-2]@0.001, [3-5]@0.002 → 6@0.35%+3@0.45% var principal = Principal * 0.5m; var rate1 = FloatPlusRate + 0.001m; var rate2 = FloatPlusRate + 0.002m; decimal interest2 = 0m, dynomic = principal; for (int d = 0; d < 9; d++) { if (d % 3 == 0) dynomic = principal + interest2; interest2 += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; } var expected = Math.Round(interest2, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected, u2.InterestAmount); } #endregion #region 5. T+0浮动加点(单利)算头不算尾("10") /// /// [UT_T0_FLT_PLUS_SI_002] T+0浮动加点(单利)算头不算尾-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→2天, 单利 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_SI_003] T+0浮动加点(单利)算头不算尾-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); } /// /// [UT_T0_FLT_PLUS_SI_004] T+0浮动加点(单利)算头不算尾-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→1天; 5/6全平→9天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_004() { var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); // 单利: 9天, [27,28,29]@0.001 +[30,1,2]@0.001 + [3,4,5]@0.002 var raw = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + Principal * 0.5m * (FloatPlusRate + 0.002m) * 3 / AnnualDays; var expected = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected, u2.InterestAmount); } #endregion #region 6. T+0固定正利率 算头算尾("11") - interest_rule=0 /// /// [UT_T0_FIX_POS_11_001] 算头算尾 T+0固定正利率-未收盘平仓 /// --------------------------------------------------------------- /// StartDate=4/27, 4/28平仓, 算头算尾→S=4/27,E=4/28→2天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_11_001() { var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRatePositive); AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); } /// /// [UT_T0_FIX_POS_11_002] 算头算尾 T+0固定正利率-收盘后次日全部平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头算尾→1+1=2天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_11_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(1, FixedRatePositive, 0m, Principal)) }; var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRatePositive, eod); AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); } /// /// [UT_T0_FIX_POS_11_003] 算头算尾 T+0固定正利率-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_11_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) }; var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRatePositive, eod); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); } /// /// [UT_T0_FIX_POS_11_004] 算头算尾 T+0固定正利率-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→2天×50%; 5/6全平→EOD=4/28, 算头算尾(newCalcLast=true无影响) /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_POS_11_004() { var u1 = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FixedRatePositive); AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) }; var u2 = CalcFixedUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); AssertInterestEqual(ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m), u2.InterestAmount); } #endregion #region 7. T+0浮动加点(当前营业日)算头算尾("11") /// /// [UT_T0_FLT_PLUS_CUR_11_002] 算头算尾 T+0浮动加点(当前营业日,复利)-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_11_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_CUR_11_003] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_11_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_CUR_11_004] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→2天; 5/6全平→复利从头算10天 [27-29] [30-2] [3-5] [6] /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_CUR_11_004() { var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m, newCalcLast: false); // 复利从头算:10天, 每3天重置, [27-29]@0.35%, [30-2,3-5,6]@0.45% var principal = Principal * 0.5m; var rate1 = FloatPlusRate + 0.001m; var rate2 = FloatPlusRate + 0.002m; decimal interest = 0m, dynomic = principal; for (int d = 0; d < 10; d++) { if (d % 3 == 0) dynomic = principal + interest; interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; } var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected1, u2.InterestAmount); } #endregion #region 8. T+0浮动加点(前一营业日)算头算尾("11") /// /// [UT_T0_FLT_PLUS_PRE_11_002] 算头算尾 T+0浮动加点(前一营业日,复利)-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_11_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_PRE_11_003] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_11_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_PRE_11_004] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→2天@0.10%; 5/6全平→复利从头算10天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_PRE_11_004() { var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); // 复利从头算:10天, 每3天重置, [27-29,30-2]@0.001, [3-5,6]@0.002 → 6@0.35%+4@0.45% var principal = Principal * 0.5m; var rate1 = FloatPlusRate + 0.001m; var rate2 = FloatPlusRate + 0.002m; decimal interest = 0m, dynomic = principal; for (int d = 0; d < 10; d++) { if (d % 3 == 0) dynomic = principal + interest; interest += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; } var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected1, u2.InterestAmount); } #endregion #region 9. T+0浮动加点(单利)算头算尾("11") /// /// [UT_T0_FLT_PLUS_SI_11_002] 算头算尾 T+0浮动加点(单利)-收盘后次日全平 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头算尾→2天, 单利 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_11_002() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_SI_11_003] 算头算尾 T+0浮动加点(单利)-部分平仓 /// --------------------------------------------------------------- /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_11_003() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_PLUS_SI_11_004] 算头算尾 T+0浮动加点(单利)-部分平仓后全平 /// --------------------------------------------------------------- /// 4/28半平→2天; 5/6全平→算头算尾 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_PLUS_SI_11_004() { var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); var eod = new List { CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) }; var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); // 单利: 10天, [27,28,29]@0.001 + [30,1,2]@0.001 + [3,4,5,6]@0.002 → 3@0.35% + 3@0.35% + 3@0.45% var raw1 = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + Principal * 0.5m * (FloatPlusRate + 0.002m) * 4 / AnnualDays; var expected1 = Math.Round(raw1, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); AssertInterestEqual(expected1, u2.InterestAmount); } #endregion #region 10. T+0固定利率 - 收盘归档 /// /// [UT_T0_FIX_EOD_001] T+0固定利率算头不算尾-首日收盘归档 /// --------------------------------------------------------------- /// 4/27(起息日)执行收盘, 算头不算尾→1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_EOD_001() { var i = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); } /// /// [UT_T0_FIX_EOD_002] T+0固定利率算头不算尾-连续收盘 /// --------------------------------------------------------------- /// 4/27收盘+4/28收盘, 算头不算尾→各1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FIX_EOD_002() { var e1 = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); AssertInterestEqual(expected1, e1.InterestAmount); var e2 = CalcFixedEod("10", new DateTime(2026, 4, 28), InterestRule_Cur, FixedRatePositive, new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, expected1) }); AssertInterestEqual(expected1*2, e2.InterestAmount); } #endregion #region 11. T+0浮动利率 - 基础场景 /// /// [UT_T0_FLT_BASE_001] T+0浮动利率基础-起息日平仓→0天 /// --------------------------------------------------------------- /// StartDate=4/27, 4/27平仓, 算头不算尾→0天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_BASE_001() { var i = CalcFloatUnwind("10", new DateTime(2026, 4, 27), new DateTime(2026, 4, 27), 1m, InterestRule_Cur, FixedRate, InterestTypeEnum.单利); AssertInterestEqual(0m, i.InterestAmount); } /// /// [UT_T0_FLT_BASE_002] T+0浮动利率基础-第2天全平→1天 /// --------------------------------------------------------------- /// StartDate=4/27, 4/28平仓, 算头不算尾→1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_BASE_002() { var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRate, InterestTypeEnum.单利); AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_BASE_003] T+0浮动利率基础-首日收盘→1天 /// --------------------------------------------------------------- /// 4/27执行收盘, 算头不算尾→1天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_BASE_003() { var i = CalcFloatEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRate, InterestTypeEnum.单利); AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); } /// /// [UT_T0_FLT_BASE_004] T+0浮动利率基础-首日收盘+次日全平→2天 /// --------------------------------------------------------------- /// 4/27收盘+4/28全平, 算头不算尾→2天 /// --------------------------------------------------------------- /// [TestMethod] public void UT_T0_FLT_BASE_004() { var eod = new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, ExpectedInterest(1, FixedRate, 0.001m, Principal)) }; var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, InterestRule_Cur, FixedRate, InterestTypeEnum.单利, eod); AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); } #endregion } }