diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx index e99377d3..8719402e 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【债券ETF】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx index ff441d10..83c478c8 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看多】-【现券】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx index 423071ed..e60e2ce2 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【债券ETF】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx index 048b9eb1..ed0f6197 100644 Binary files a/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx and b/Plugins/YLErp.Plugins.GuoLian/App_Docs/contract_template/国联民生-收益互换交易确认书-境内模板-【客户看空】-【现券】-清洁版.docx differ diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs index dc16cf57..34ae5a7a 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs @@ -124,7 +124,7 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator protected override string GetOutputFileName(string contractNo, string contractIndex) { var trade = Context.Trade; - var rule = $"{trade.TradeType}交易确认书_{contractNo}"; + var rule = $"【待用印】{trade.TradeType}交易确认书_{contractNo}"; return $"{rule}.{DocType.ToLower()}"; } @@ -152,9 +152,9 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator dic["乙方名称"] = client.Name; // 交易日期相关 - dic["成交日期"] = trade.TradeDate?.ToString("yyyy年M月d日"); - dic["开始日期"] = trade.StartDate?.ToString("yyyy年M月d日"); - dic["到期日期"] = trade.ExerciseDate?.ToString("yyyy年M月d日"); + dic["成交日期"] = trade.TradeDate?.ToString("【yyyy】年【M】月【d】日"); + dic["开始日期"] = trade.StartDate?.ToString("【yyyy】年【M】月【d】日"); + dic["到期日期"] = trade.ExerciseDate?.ToString("【yyyy】年【M】月【d】日"); // 根据交易类型填充不同数据 switch (trade.TradeType) @@ -222,8 +222,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator var bond = JsonHelper.Deserialize(underlying.ExJson) ?? new UnderlyingBond(); dic["参考标的发行人"] = bond.UnderlyingIssuer ?? ""; dic["参考标的担保人"] = ""; - dic["票面利率"] = ((double)(bond.CouponRate ?? 0) * 100).ToString("N4"); - dic["参考标的到期日"] = underlying.MaturityDate?.ToString("yyyy年M月d日") ?? ""; + dic["票面利率"] = ((double)(bond.CouponRate ?? 0)).ToString("0.00"); + dic["参考标的到期日"] = underlying.MaturityDate?.ToString("【yyyy】年【M】月【d】日") ?? ""; } // 从swap_position获取期初价格、保证金率等信息 @@ -235,7 +235,7 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator if (swapPosition != null) { // 期初全价和净价(转换为百分比格式) - dic["参考标的期初全价"] = ((double)swapPosition.PosiGrossPrice).ToString("N4"); + dic["参考标的期初全价"] = ((double)swapPosition.PosiGrossPrice).ToString("0.000"); dic["参考标的期初净价"] = ((double)(swapPosition.PosiNetNoFeePrice ?? 0m)).ToString("N4"); dic["参考标的期初全价%"] = ((double)swapPosition.PosiGrossPrice * 100).ToString("N4"); @@ -257,40 +257,43 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator dic["维持保障金率"] = ((double)clientMarginRate.maintain_rate * 100).ToString("N4"); // 期初预付比例和金额 - dic["期初预付比例"] = ((double)clientMarginRate.init_rate * 100).ToString("N4"); + dic["期初预付比例"] = ((double)clientMarginRate.init_rate * 100).ToString("0.##"); dic["期初预付金额"] = ((trade.OriginalStockEqvNotional ?? 0) * (double)clientMarginRate.init_rate).ToString("N2"); } else { dic["初始保障金率"] = "0.0000"; dic["维持保障金率"] = "0.0000"; - dic["期初预付比例"] = "0.0000"; + dic["期初预付比例"] = "0"; dic["期初预付金额"] = "0.00"; } bool posiLong = IsCustomerLong(swapPosition); // 计算平仓线、预警线、档位值(利率债TRS,固定4档) - var maintainRatePercent = (double)(clientMarginRate?.maintain_rate ?? 0) * 100; // 维持保证金率 A(%) + // A = 维持保证金率(%),B = 初始保证金率(%),diff = B - A + var maintainRatePercent = (double)(clientMarginRate?.maintain_rate ?? 0) * 100; // A(%) + var initRatePercent = (double)(clientMarginRate?.init_rate ?? 0) * 100; // B(%) + var diff = initRatePercent - maintainRatePercent; // B - A if (maintainRatePercent > 0) { if (posiLong) { - // 客户看多:档位n = 100 - n * A - dic["平仓线"] = (100 - 4 * maintainRatePercent).ToString("0.##"); - dic["预警线"] = (100 - 3 * maintainRatePercent).ToString("0.##"); - dic["档位1"] = (100 - 1 * maintainRatePercent).ToString("0.##"); - dic["档位2"] = (100 - 2 * maintainRatePercent).ToString("0.##"); - dic["档位3"] = (100 - 3 * maintainRatePercent).ToString("0.##"); - dic["档位4"] = (100 - 4 * maintainRatePercent).ToString("0.##"); + // 客户看多:档位n = 100 - n * (B-A) + dic["档位1"] = (100 - 1 * diff).ToString("0.##"); + dic["档位2"] = (100 - 2 * diff).ToString("0.##"); + dic["档位3"] = (100 - 3 * diff).ToString("0.##"); + dic["档位4"] = (100 - 4 * diff).ToString("0.##"); + dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位 + dic["平仓线"] = (100 - 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 - (B-A) } else { - // 客户看空:档位n = 100 + n * A - dic["平仓线"] = (100 + 4 * maintainRatePercent).ToString("0.##"); - dic["预警线"] = (100 + 3 * maintainRatePercent).ToString("0.##"); - dic["档位1"] = (100 + 1 * maintainRatePercent).ToString("0.##"); - dic["档位2"] = (100 + 2 * maintainRatePercent).ToString("0.##"); - dic["档位3"] = (100 + 3 * maintainRatePercent).ToString("0.##"); - dic["档位4"] = (100 + 4 * maintainRatePercent).ToString("0.##"); + // 客户看空:档位n = 100 + n * (B-A) + dic["档位1"] = (100 + 1 * diff).ToString("0.##"); + dic["档位2"] = (100 + 2 * diff).ToString("0.##"); + dic["档位3"] = (100 + 3 * diff).ToString("0.##"); + dic["档位4"] = (100 + 4 * diff).ToString("0.##"); + dic["预警线"] = dic["档位4"]; // 预警线 = 最高档位 + dic["平仓线"] = (100 + 5 * diff).ToString("0.##"); // 平仓线 = 最高档位 + (B-A) } } else @@ -303,6 +306,96 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator dic["档位4"] = ""; } + // 追保事件/返还追保事件 价格区间和金额比例 + if (maintainRatePercent > 0) + { + if (posiLong) + { + // === 客户看多 追保事件 === + // 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,追保0% + dic["追保区间0下限"] = (100 - 1 * diff).ToString("0.##"); + dic["追保金额比例0"] = "0"; + // 区间1:{100-2A}% ≤ 当前净价 < {100-A}%,追保{B-A}% + dic["追保区间1下限"] = (100 - 2 * maintainRatePercent).ToString("0.##"); + dic["追保区间1上限"] = (100 - 1 * maintainRatePercent).ToString("0.##"); + dic["追保金额比例1"] = (1 * diff).ToString("0.##"); + // 区间2:{100-3A}% ≤ 当前净价 < {100-2A}%,追保{2(B-A)}% + dic["追保区间2下限"] = (100 - 3 * maintainRatePercent).ToString("0.##"); + dic["追保区间2上限"] = (100 - 2 * maintainRatePercent).ToString("0.##"); + dic["追保金额比例2"] = (2 * diff).ToString("0.##"); + // 区间3:{100-4A}% ≤ 当前净价 < {100-3A}%,追保{3(B-A)}% + dic["追保区间3下限"] = (100 - 4 * maintainRatePercent).ToString("0.##"); + dic["追保区间3上限"] = (100 - 3 * maintainRatePercent).ToString("0.##"); + dic["追保金额比例3"] = (3 * diff).ToString("0.##"); + + // === 客户看多 返还追保事件 === + // 区间0:当前净价 ≥ {100-(B-A)}% × 期初净价,返还0% + dic["返还区间0下限"] = (100 - 1 * diff).ToString("0.##"); + dic["返还金额比例0"] = "0"; + // 区间1:{100-2(B-A)}% ≤ 当前净价 < {100-(B-A)}%,返还{B-A}% + dic["返还区间1下限"] = (100 - 2 * diff).ToString("0.##"); + dic["返还区间1上限"] = (100 - 1 * diff).ToString("0.##"); + dic["返还金额比例1"] = (1 * diff).ToString("0.##"); + // 区间2:{100-3(B-A)}% ≤ 当前净价 < {100-2(B-A)}%,返还{2(B-A)}% + dic["返还区间2下限"] = (100 - 3 * diff).ToString("0.##"); + dic["返还区间2上限"] = (100 - 2 * diff).ToString("0.##"); + dic["返还金额比例2"] = (2 * diff).ToString("0.##"); + // 区间3:{100-4(B-A)}% ≤ 当前净价 < {100-3(B-A)}%,返还{3(B-A)}% + dic["返还区间3下限"] = (100 - 4 * diff).ToString("0.##"); + dic["返还区间3上限"] = (100 - 3 * diff).ToString("0.##"); + dic["返还金额比例3"] = (3 * diff).ToString("0.##"); + } + else + { + // === 客户看空 追保事件 === + // 区间0:当前净价 ≤ {100+(B-A)}% × 期初净价,追保0% + dic["追保区间0上限"] = (100 + 1 * diff).ToString("0.##"); + dic["追保金额比例0"] = "0"; + // 区间1:{100+(B-A)}% < 当前净价 ≤ {100+2(B-A)}%,追保{B-A}% + dic["追保区间1下限"] = (100 + 1 * diff).ToString("0.##"); + dic["追保区间1上限"] = (100 + 2 * diff).ToString("0.##"); + dic["追保金额比例1"] = (1 * diff).ToString("0.##"); + // 区间2:{100+2(B-A)}% < 当前净价 ≤ {100+3(B-A)}%,追保{2(B-A)}% + dic["追保区间2下限"] = (100 + 2 * diff).ToString("0.##"); + dic["追保区间2上限"] = (100 + 3 * diff).ToString("0.##"); + dic["追保金额比例2"] = (2 * diff).ToString("0.##"); + // 区间3:{100+3(B-A)}% < 当前净价 ≤ {100+4(B-A)}%,追保{3(B-A)}% + dic["追保区间3下限"] = (100 + 3 * diff).ToString("0.##"); + dic["追保区间3上限"] = (100 + 4 * diff).ToString("0.##"); + dic["追保金额比例3"] = (3 * diff).ToString("0.##"); + + // === 客户看空 返还追保事件 === + // 区间0:当前净价 ≤ {100+(B-A)}% × 期初净价,返还0% + dic["返还区间0上限"] = (100 + 1 * diff).ToString("0.##"); + dic["返还金额比例0"] = "0"; + // 区间1:{100+(B-A)}% < 当前净价 ≤ {100+2(B-A)}%,返还{B-A}% + dic["返还区间1下限"] = (100 + 1 * diff).ToString("0.##"); + dic["返还区间1上限"] = (100 + 2 * diff).ToString("0.##"); + dic["返还金额比例1"] = (1 * diff).ToString("0.##"); + // 区间2:{100+2(B-A)}% < 当前净价 ≤ {100+3(B-A)}%,返还{2(B-A)}% + dic["返还区间2下限"] = (100 + 2 * diff).ToString("0.##"); + dic["返还区间2上限"] = (100 + 3 * diff).ToString("0.##"); + dic["返还金额比例2"] = (2 * diff).ToString("0.##"); + // 区间3:{100+3(B-A)}% < 当前净价 ≤ {100+4(B-A)}%,返还{3(B-A)}% + dic["返还区间3下限"] = (100 + 3 * diff).ToString("0.##"); + dic["返还区间3上限"] = (100 + 4 * diff).ToString("0.##"); + dic["返还金额比例3"] = (3 * diff).ToString("0.##"); + } + } + else + { + // 清空追保/返还相关字典值 + for (int i = 0; i <= 3; i++) + { + dic[$"追保区间{i}下限"] = ""; + dic[$"追保区间{i}上限"] = ""; + dic[$"追保金额比例{i}"] = ""; + dic[$"返还区间{i}下限"] = ""; + dic[$"返还区间{i}上限"] = ""; + dic[$"返还金额比例{i}"] = ""; + } + } + // 期初预付金利率(InterestMode == 初始预付金) var initialMarginPosition = swapPositions .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金) @@ -326,8 +419,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator dic["IsFixed"] = "□"; dic["IsFloat"] = "□"; - dic["期初观察日"] = trade.StartDate?.ToString("yyyy年M月d日") ?? ""; - dic["期末观察日"] = trade.ExerciseDate?.ToString("yyyy年M月d日") ?? ""; + dic["期初观察日"] = trade.StartDate?.ToString("【yyyy】年【M】月【d】日") ?? ""; + dic["期末观察日"] = trade.ExerciseDate?.ToString("【yyyy】年【M】月【d】日") ?? ""; if (interestMargin != null) { if (string.IsNullOrEmpty(interestMargin.FloatRateUnderlyingCode)) @@ -349,8 +442,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator { var firstInterval = interestMargin.SwapIntervalList.OrderBy(o=>o.Date).First(); var lastInterval = interestMargin.SwapIntervalList.OrderBy(o => o.Date).Last(); - dic["期初观察日"] = firstInterval.Date.ToString("yyyy年M月d日") ?? ""; - dic["期末观察日"] = lastInterval.Date.ToString("yyyy年M月d日") ?? ""; + dic["期初观察日"] = firstInterval.Date.ToString("【yyyy】年【M】月【d】日") ?? ""; + dic["期末观察日"] = lastInterval.Date.ToString("【yyyy】年【M】月【d】日") ?? ""; } } else @@ -364,7 +457,7 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator var notional = trade.OriginalStockEqvNotional ?? 0; var tradingFee = (double)swapPosition.PosiTradingFeePending; var basicFeeRate = notional == 0 ? 0 : tradingFee / notional * 100; - dic["基本费率"] = basicFeeRate.ToString("N4"); + dic["基本费率"] = basicFeeRate.ToString("0.##"); // 期初现金交换比例和金额(使用初始预付金数据) dic["期初现金交换比例"] = initialMarginPosition != null @@ -376,31 +469,31 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator } else { - dic["参考标的期初全价"] = "0.0000"; + dic["参考标的期初全价"] = "0.000"; dic["参考标的期初净价"] = "0.0000"; dic["初始保障金率"] = "0.0000"; dic["维持保障金率"] = "0.0000"; - dic["期初预付比例"] = "0.0000"; + dic["期初预付比例"] = "0"; dic["期初预付金额"] = "0.00"; dic["期初预付金利率"] = "0.0000"; dic["利率类型"] = "固定利率"; dic["固定利率"] = "0.0000"; dic["利差"] = ""; - dic["基本费率"] = "0.0000"; + dic["基本费率"] = "0"; dic["期初现金交换比例"] = "0.0000"; dic["期初现金交换金额"] = "0.00"; } // 参考标的券面总额(名义本金) - dic["参考标的券面总额"] = trade.OriginalStockEqvNotional?.ToString("N2") ?? "0.00"; + dic["参考标的券面总额"] = swapPosition != null ? ((double)swapPosition.PosiQuantity).ToString("N2") : "0"; // 参考标的证券全称和参考标的名义份额(复用上方已声明的bond) dic["参考标的证券全称"] = underlying != null ? (JsonHelper.Deserialize(underlying.ExJson)?.UnderlyingFullName ?? underlying.UnderlyingName) : ""; dic["参考标的名义份额"] = swapPosition != null - ? ((double)swapPosition.PosiQuantity).ToString("N2") - : "0.00"; + ? ((double)swapPosition.PosiQuantity).ToString("0.##") + : "0"; dic["参考标的基金管理人"] = ""; var contractTypeId = (Context.GetContractTypes().FirstOrDefault(O => O.ContactType == "交易确认书接收")?.id) ?? 0; // 乙方联系人信息 @@ -417,8 +510,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator ["标的代码"] = underlying?.UnderlyingCode ?? "", ["标的名称"] = underlying?.UnderlyingName ?? "", ["名义本金"] = trade.OriginalStockEqvNotional?.ToString("N2") ?? "0.00", - ["成交日期"] = trade.TradeDate?.ToString("yyyy年M月d日"), - ["到期日期"] = trade.ExerciseDate?.ToString("yyyy年M月d日") + ["成交日期"] = trade.TradeDate?.ToString("【yyyy】年【M】月【d】日"), + ["到期日期"] = trade.ExerciseDate?.ToString("【yyyy】年【M】月【d】日") }; table1.Add(row); dic["table1"] = table1; diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs deleted file mode 100644 index 2c748aca..00000000 --- a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest.cs +++ /dev/null @@ -1,1411 +0,0 @@ -using Newtonsoft.Json; -using YLErp.DBModels; -using YLErp.DBModels.Enums; -using YLErp.Models; - -namespace YLErp.Modules.SwapModule -{ - /// - /// 互换利息计算单元测试 - /// ================================================================ - /// 测试口径说明: - /// "11" = 算头算尾(含起息日和到期日) - /// "10" = 算头不算尾(含起息日,不含到期日) - /// "01" = 不算头算尾(不含起息日,含到期日) - /// "00" = 不算头不算尾(不含起息日也不含到期日) - /// 不算头不算尾暂时测试不通过 - /// 统一测试数据: - /// - Principal=1000, FixedRate=1.00%, AnnualDays=365 - /// - ResetPeriod=3天, InterestRule=-1(前一营业日), InterestRule=0(当前营业日) - /// - FR007@2026-04-27=0.10%, FR007@2026-04-30=0.20% - /// - StartDate=2026-04-28, TradeDate=2026-04-27 - /// ================================================================ - /// - [TestClass] - public class GetInterestsUnitTest - { - #region 内部类:浮动利率模拟服务 - - /// - /// StubSwapDealService - 模拟浮动利率获取 - /// 用于单元测试中预置FR007价格,避免依赖外部数据源 - /// - private sealed class StubSwapDealService : SwapDealService - { - private readonly IReadOnlyDictionary _floatRates; - - public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) - { - _floatRates = floatRates; - } - - protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) - { - if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) - { - rate = 0; - return false; - } - - if (_floatRates.TryGetValue(valueDate.Date, out rate)) - { - return true; - } - - rate = 0; - return false; - } - } - - #endregion - - #region 测试常量与共享变量 - - private const decimal Principal = 1000m; // 本金:1000 - private const decimal FixedRate = 0.01m; // 固定利率:1.00% - private const int AnnualDays = 365; // 年化天数 - private const int ResetPeriod = 3; // 重置周期:3天 - private const int InterestRule_Pre = -1; // 前一营业日规则 - private const int InterestRule_Cur = 0; // 当前营业日规则 - - private static readonly DateTime TradeDate = new(2026, 4, 27); // 成交日 - private static readonly DateTime StartDate = new(2026, 4, 28); // 起息日(开始计息日) - private static readonly DateTime ExerciseDate = new(2027, 4, 27); // 到期日 - - private SwapDealService _service; - private IReadOnlyDictionary _floatRates; - - [TestInitialize] - public void Init() - { - // 预置FR007价格数据 - _floatRates = new Dictionary - { - [new DateTime(2026, 4, 27)] = 0.001, // FR007@2026-04-27 = 0.10% - [new DateTime(2026, 4, 28)] = 0.001, // FR007@2026-04-28 = 0.10% (新增) - [new DateTime(2026, 4, 29)] = 0.001, // FR007@2026-04-29 = 0.10% - [new DateTime(2026, 4, 30)] = 0.002, // FR007@2026-04-30 = 0.20% - [new DateTime(2026, 5, 6)] = 0.002, // FR007@2026-05-06 = 0.20% - // 到期日测试用例需要的利率数据(2027年) - [new DateTime(2027, 4, 23)] = 0.001, // FR007@2027-04-23 = 0.10%(2027-04-26的前一工作日) - [new DateTime(2027, 4, 24)] = 0.001, // FR007@2027-04-24 = 0.10%(周末) - [new DateTime(2027, 4, 25)] = 0.001, // FR007@2027-04-25 = 0.10%(周末) - [new DateTime(2027, 4, 26)] = 0.001, // FR007@2027-04-26 = 0.10% - [new DateTime(2027, 4, 27)] = 0.001 // FR007@2027-04-27 = 0.10%(到期日) - }; - - _service = new StubSwapDealService( - new OptUserInfo(0, nameof(GetInterestsUnitTest), OptUserFrom.UnitTest), - _floatRates); - } - - #endregion - - #region 测试数据构建器 - - /// - /// 创建测试用交易对象 - /// - /// 计息口径:"11"/"10"/"01"/"00" - /// 取率规则:-1=前一营业日,0=当前营业日 - private static trade CreateTrade(string interestCalcMode, int interestRule = InterestRule_Pre) - { - var extend = new trade_extend - { - TradeId = 1, - ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson - { - AnnualDays = AnnualDays, - InterestCalcMode = interestCalcMode, - SettlementRules = interestRule - }) - }; - - return new trade - { - id = 1, - TradeNumber = "UT-SWAP-INT-001", - ClientId = 999998, - TradeType = "收益互换", - TradeDate = TradeDate, - StartDate = StartDate, - ExerciseDate = ExerciseDate, - TradeStatus = "确认成交", - ValidState = "Valid", - trade_extend = extend - }; - } - - /// - /// 创建测试用持仓对象 - /// - /// 计息口径 - /// 取率规则 - private static swap_position CreateInterestPosition(string interestCalcMode, int interestRule = InterestRule_Pre) - { - var intervalModels = new List - { - new IntervalModel - { - Date = ExerciseDate, - Rate = FixedRate, - Settlement = 0 - } - }; - - return new swap_position - { - id = 1001, - SwapTradeId = 1, - PositionType = (int)PositionTypeFlag.Unknown, - InterestDirection = (int)SwapDirectionEnum.收取, - InterestMode = (int)InterestModeEnum.标的期初全价, - InterestRateDefault = FixedRate, - InterestPrincipalFix = Principal, - PosiStartDate = StartDate, - PosiMatuirityDate = ExerciseDate, - IsInitial = true, - Invalid = false, - InterestType = (int)InterestTypeEnum.单利, - IsAnnualized = true, - interest_rest_days = ResetPeriod, - interest_rule = interestRule, - FloatRateUnderlyingCode = "FR007", - InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) - }; - } - - /// - /// 创建日终持仓记录(EOD归档数据) - /// - private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) - { - return new eod_swap_position - { - id = 1, - SwapTradeId = 1, - PositionId = 1001, - ValueDate = valueDate, - ClientId = 999998, - FloatRate = floatRate, - TdInterestPrincipal = tdPrincipal, - PosiNotionalValue = tdPrincipal, - InterestProfitSum = interestSum - }; - } - - /// - /// 计算期望利息金额 - /// 公式:本金 × (固定利率 + 浮动利率) × 计息天数 ÷ 年化天数 - /// - private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) - { - var yearlyRate = fixedRate + floatRate; - var interest = principal * yearlyRate * days / AnnualDays; - return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - } - - #endregion - - #region 通用的GetInterests调用方法 - - /// - /// 通用平仓计算(不含eodPositions) - /// - private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, - decimal closePercent, int interestRule = InterestRule_Pre) - { - return CalcUnwind(interestCalcMode, valueDate, unwindDate, closePercent, - new List(), interestRule); - } - - /// - /// 通用平仓计算(含eodPositions) - /// - private swap_flow_event CalcUnwind(string interestCalcMode, DateTime valueDate, DateTime unwindDate, - decimal closePercent, List eodPositions, int interestRule = InterestRule_Pre) - { - var td = CreateTrade(interestCalcMode, interestRule); - var position = CreateInterestPosition(interestCalcMode, interestRule); - - var interests = _service.GetInterests( - td, td.trade_extend, - valueDate, unwindDate, - eodPositions, - new List { position }, - Principal, 0, 0, - Principal, closePercent, - (int)SwapEventTypeEnum.平仓, - false, false, 0, Principal, - false, - false); - - Assert.AreEqual(1, interests.Count); - return interests[0]; - } - - /// - /// 通用收盘计算 - /// settment=true 表示收盘场景 - /// - private swap_flow_event CalcEod(string interestCalcMode, DateTime valueDate, - List eodPositions, int interestRule = InterestRule_Pre) - { - var td = CreateTrade(interestCalcMode, interestRule); - var position = CreateInterestPosition(interestCalcMode, interestRule); - - var interests = _service.GetInterests( - td, td.trade_extend, - valueDate, valueDate, - eodPositions, - new List { position }, - Principal, 0, 0, - Principal, 1m, - (int)SwapEventTypeEnum.平仓, - false, false, 0, Principal, - false, - true); // settment=true 表示收盘 - - Assert.AreEqual(1, interests.Count); - return interests[0]; - } - - /// - /// 通用自动互换计算 - /// 使用SwapEventTypeEnum.自动互换事件类型 - /// - private swap_flow_event CalcAutoSwap(string interestCalcMode, DateTime valueDate, - List eodPositions, decimal closePercent = 1m, int interestRule = InterestRule_Pre) - { - var td = CreateTrade(interestCalcMode, interestRule); - var position = CreateInterestPosition(interestCalcMode, interestRule); - - var interests = _service.GetInterests( - td, td.trade_extend, - valueDate, valueDate, - eodPositions, - new List { position }, - Principal, 0, 0, - Principal, closePercent, - (int)SwapEventTypeEnum.自动互换, - false, false, 0, Principal, - false, - false); - - Assert.AreEqual(1, interests.Count); - return interests[0]; - } - - #endregion - - #region 场景1:算头算尾 (InterestCalcMode="11") - #region 计息区间说明: - /// 11_001: 首日(StartDate=4/28)平仓 → S=4/28, E=4/28 → 1天 - /// 11_002: 次日(4/29)平仓 → S=4/28, E=4/29 → 2天 - /// 11_003: 次日(4/29)平仓50% → S=4/28, E=4/29 → 2天×50% - /// 11_004: 跨周期(5/6)平仓 → S=4/28, E=5/6 → 8天(分段取率) - /// 11_EOD_001: 首日(4/28)收盘 → 1天 - /// 11_EOD_002: 4/28已收盘 → 4/29平仓 → S=4/29, E=4/29 → 1天 - #endregion - /// ================================================================ */ - - /// - /// [11_001] 算头算尾 - 首日起息日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日StartDate)盘中执行全平 - /// 前置:无上一日EOD持仓(首次操作) - /// 操作:valueDate=2026-04-28,执行"全平"(closePercent=100%) - /// 口径:算头算尾,计息区间 S=4/28, E=4/28 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_PRE_001() - { - var interest = CalcUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [11_002] 算头算尾 - 次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 - /// 前置:无上一日EOD持仓 - /// 操作:valueDate=2026-04-29,执行"全平" - /// 口径:算头算尾,计息区间 S=4/28, E=4/29 - /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_PRE_002() - { - var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [11_003] 算头算尾 - 次日平仓50% - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓50% - /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) - /// 口径:算头算尾,计息区间 S=4/28, E=4/29 - /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_PRE_003() - { - var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [11_004] 算头算尾 - 跨重置周期全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 - /// 背景:ResetPeriod=3天,4/28→4/30为第一周期,5/1→5/6为第二周期 - /// 操作:valueDate=2026-05-06,执行"全平" - /// 取率:跨周期分段取率 - /// - 第一段(4/28-4/30): 3天×FR007@4/27(0.10%) - /// - 第二段(5/1-5/6): 6天×FR007@4/30(0.20%) - /// 口径:算头算尾,计息区间 S=4/28, E=5/6 - /// 期望:分段计算利息 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_PRE_004() - { - var interest = CalcUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); - // 预期分段计算:3天@0.10% + 6天@0.20% - var expected = Math.Round( - ExpectedInterest(3, FixedRate, 0.001m, Principal) + - ExpectedInterest(6, FixedRate, 0.002m, Principal), - ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [11_EOD_001] 算头算尾 - 首日收盘归档 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日)执行收盘EOD归档 - /// 前置:无上一日EOD持仓(首次收盘) - /// 操作:执行 2026-04-28 收盘归档 - /// 口径:算头算尾,计息区间 S=4/28, E=4/28 - /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_EOD_001() - { - var interest = CalcEod("11", new DateTime(2026, 4, 28), new List()); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [11_EOD_002] 算头算尾 - 前日已收盘,次日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 - /// 前置:存在4/28的EOD持仓记录(待实现利息=1天利息) - /// 操作:valueDate=2026-04-29,执行"全平" - /// 口径:算头算尾 - /// 期望:总利息=历史待实现利息+当期利息=1天(4/28)+1天(4/29)=2天 - /// 利息=2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_EOD_002() - { - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - var interest = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); - // 平仓利息 = 历史待实现利息(4/28=1天) + 当期利息(4/29=1天) = 2天 - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #region 场景2:算头不算尾 (InterestCalcMode="10") - 当前测试重点 - #region 计息区间说明: - /// 10_001: 首日(4/28)平仓 → S=4/28, E=4/27 → 0天 - /// 10_002: 次日(4/29)全平 → S=4/28, E=4/28 → 1天 - /// 10_003: 次日(4/29)半平 → 1天×50% - /// 10_004: 次日(4/29)全平后收盘 → 全平利息+收盘待实现=0 - /// 10_005: 第3日(4/30)全平 → S=4/28, E=4/29 → 2天 - /// 10_006: 第3日(4/30)半平 → 2天×50% - /// 10_007: 次日(4/29)半平 + 第3日(4/30)收盘 → 剩余50%×1天 - /// 10_008: 第3日(4/30)直接收盘 → 持仓×1天 - /// 10_009: 次日(4/29)自动互换 → 1天 - /// 10_010: 自动互换后次日(4/30)平仓 → 0天 - /// 10_011: 跨周期(5/6)全平 → 分段计息 - /// 10_EOD_001: 首日(4/28)收盘 → 0天(首次) - /// 10_EOD_002: 4/28收盘 → 4/29全平 → 1天 - /// 10_EOD_003: 4/28收盘 → 4/29半平 → 0.5天 - /// 10_EOD_004: 4/28→4/29连续收盘 - /// 10_EOD_005: 4/28收盘 → 4/30收盘 - #endregion - /// ================================================================ */ - - #region 2.1 盘中平仓场景 - - /// - /// [10_001] 算头不算尾 - 首日起息日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日StartDate)盘中执行全平 - /// 前置:无上一日EOD持仓 - /// 操作:valueDate=2026-04-28,执行"全平" - /// 口径:算头不算尾 - /// - 算头:计息开始日 S=4/28(起息日) - /// - 不算尾:计息结束日 E=4/27(前一日) - /// - 计息天数 = E - S = 4/27 - 4/28 = -1 → 0天 - /// 期望:计息天数=0天,利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_001() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [10_002] 算头不算尾 - 次日全平(基准场景) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 - /// 前置:无上一日EOD持仓 - /// 操作:valueDate=2026-04-29,执行"全平" - /// 取率:前一营业日规则 → 取2026-04-27的FR007=0.10% - /// 口径:算头不算尾 - /// - 算头:S=4/28(起息日) - /// - 不算尾:E=4/28(操作日前一日) - /// - 计息天数 = 4/28 - 4/28 = 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_002() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_003] 算头不算尾 - 次日平仓50% - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 - /// 操作:valueDate=2026-04-29,执行"平仓50%"(closePercent=50%) - /// 取率:前一营业日规则 → FR007@2026-04-27=0.10% - /// 口径:算头不算尾,计息天数=1天 - /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_003() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_004] 算头不算尾 - 次日全平后收盘 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中全平;2026-04-29 收盘 - /// 操作: - /// 1. 2026-04-29 盘中执行"全平" → 计息1天 - /// 2. 2026-04-29 执行收盘归档 → 待实现利息=0 - /// 期望: - /// - 全平应计利息=1天 - /// - 收盘待实现利息=0(因持仓已不存在) - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_004() - { - // 第一步:全平计息 - var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); - - // 第二步:收盘(持仓已不存在,利息=0) - Console.WriteLine("全平后收盘,待实现利息=0(持仓已不存在)"); - } - - /// - /// [10_005] 算头不算尾 - 第3日全平(跨周末) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-30(第3个工作日)盘中全平 - /// 背景:4/28(周二)→4/29(周三)→4/30(周四),跨2个自然日 - /// 操作:valueDate=2026-04-30,执行"全平" - /// 取率:按"前一营业日"规则,沿用首个周期取率日 2026-04-27 - /// 口径:算头不算尾 - /// - 算头:S=4/28(起息日) - /// - 不算尾:E=4/30(操作日前一日) - /// - 计息天数 = 4/30 - 4/28 = 2天 - /// 实际计算:持仓期间为4/28~4/29(算头不算尾)=2天 - /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_005() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_006] 算头不算尾 - 第3日平仓50%(跨周末) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中平仓一半 - /// 操作:valueDate=2026-04-30,执行"平仓50%" - /// 取率:FR007@2026-04-27=0.10% - /// 口径:算头不算尾,计息天数=2天 - /// 期望:计息天数=2天,利息=0.5*2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_006() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 0.5m); - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_007] 算头不算尾 - 次日半平 + 第3日收盘 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中平仓一半;2026-04-30 收盘 - /// 操作: - /// 1. 2026-04-29 盘中"平仓50%" → 剩余50%持仓 - /// 2. 2026-04-30 执行收盘归档 → 剩余50%持仓计息 - /// 取率:FR007@2026-04-27=0.10% - /// 口径:算头不算尾 - /// 期望: - /// - 4/29全平利息=0.5*1*(1.00%+0.10%)*1000/365 - /// - 4/30收盘利息=0.5*1*(1.00%+0.10%)*1000/365(剩余50%计1天) - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_007() - { - // 第一步:4月29日平仓50% - var unwindInterest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); - var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expectedUnwind, unwindInterest.InterestAmount); - - // 第二步:4月30日收盘(剩余50%持仓计息1天) - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m)) - }; - var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); - var expectedEod = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expectedEod, eodInterest.InterestAmount); - } - - /// - /// [10_008] 算头不算尾 - 第3日直接收盘(未平仓) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 已收盘归档;2026-04-30 收盘 - /// 背景:持仓期间4/28→4/29已完成收盘归档 - /// 操作:直接执行 2026-04-30 收盘归档 - /// 取率:FR007@2026-04-27=0.10% - /// 口径:算头不算尾 - /// 期望:2026-04-30 收盘待实现利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_008() - { - // 4月29日收盘归档后,4月30日收盘 - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - var eodInterest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); - var expectedEod = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expectedEod, eodInterest.InterestAmount); - } - - /// - /// [10_009] 算头不算尾 - 次日自动互换 - /// --------------------------------------------------------------- - /// 场景:2026-04-29 执行"自动互换" - /// 背景:自动互换是互换交易的一种定期重置操作 - /// 操作:2026-04-29 执行"自动互换" - /// 取率:FR007@2026-04-27=0.10% - /// 口径:算头不算尾 - /// 期望: - /// - 计息天数=1天 - /// - 利息=1*(1.00%+0.10%)*1000/365 - /// - 当日收盘待实现利息=0(持仓已互换) - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_009() - { - var interest = CalcAutoSwap("10", new DateTime(2026, 4, 29), new List()); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - Console.WriteLine("自动互换后,当日收盘待实现利息=0"); - } - - /// - /// [10_010] 算头不算尾 - 自动互换后次日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-29 已发生自动互换;2026-04-30 执行"全平/收益结算" - /// 背景:自动互换已将持仓重置,累计利息清零 - /// 操作:valueDate=2026-04-30,执行"全平" - /// 口径:算头不算尾 - /// 期望:计息天数=0天,利息=0(持仓已互换) - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_010() - { - // 4月29日自动互换后的eodPosition(自动互换后累计利息清零) - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, 0m) - }; - // 4月30日平仓(持仓已互换,计息天数=0) - var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, eodPositions); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [10_011] 算头不算尾 - 跨重置周期全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 未平仓;2026-05-06 跨重置周期全平 - /// 背景: - /// - ResetPeriod=3天 - /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% - /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% - /// 操作: - /// 1. 2026-04-29 收盘归档 - /// 2. 2026-05-06 全平(跨周期) - /// 口径:算头不算尾 - /// 取率:分段取率 - /// - 4/29收盘利息=1天@0.10% - /// - 4/30持仓利息=1天@0.10%(第一周期最后一天) - /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) - /// 期望:利息 = 4/29收盘 + 4/30持仓 + 5/1~5/5持仓 = oneDay*2 + secondPeriod - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_011() - { - // 4月29日收盘归档 - var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal); - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, oneDay) - }; - - // 5月6日全平(跨周期) - var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); - // 4/30: 1天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) - var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); - // 累计利息 = 4/29收盘利息 + 4/30持仓利息(同第一周期) + 5/1~5/5利息 - var expected = Math.Round(oneDay * 2 + secondPeriod, - ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_012] 算头不算尾 - 跨重置周期全平(中间无收盘) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 起息;5/6 全平(中间4/29未收盘) - /// 背景: - /// - ResetPeriod=3天 - /// - 第一周期:4/28→4/30,取FR007@4/27=0.10% - /// - 第二周期:5/1→5/6,取FR007@4/30=0.20% - /// 操作:4/28起息后,4/29未收盘,直接5/6全平 - /// 口径:算头不算尾 - /// 取率:分段取率 - /// - 4/28~4/30持仓利息=3天@0.10%(第一周期,4/28算头) - /// - 5/1~5/5持仓利息=5天@0.20%(第二周期) - /// 期望:利息 = 3天@0.10% + 5天@0.20% - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_012() - { - // 4/28起息,无EOD持仓(4/29未收盘) - var eodPositions = new List(); - - // 5月6日全平(跨周期,4/29未收盘) - var interest = CalcUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); - // 4/28~4/30: 3天@0.10%(第一周期),5/1~5/5: 5天@0.20%(第二周期) - var firstPeriod = ExpectedInterest(3, FixedRate, 0.001m, Principal); - var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); - var expected = Math.Round(firstPeriod + secondPeriod, - ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #region 2.2 收盘归档场景(文档4.2节 - 组B) - - /// - /// [10_PRE_EOD_001] 算头不算尾 - 首日收盘归档(文档4.2节) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 收盘 - /// 操作:执行 2026-04-28 EOD - /// 取率日:2026-04-27(FR007=0.10%) - /// 口径:算头不算尾 - /// 说明:首日收盘,当日计息1天 - /// 期望:当日收盘利息(待实现)=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_EOD_001() - { - var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_PRE_EOD_002] 算头不算尾 - 首日收盘,次日全平(文档4.2节) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘;2026-04-29 盘中全平或收益结算 - /// 操作:valueDate=2026-04-29 执行"全平/收益结算" - /// 取率日:2026-04-27(FR007=0.10%) - /// 口径:算头不算尾 - /// - 持仓区间:4/28~4/29 - /// - 计息区间:4/29-4/28=1天 - /// 期望:计息天数=1;利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_EOD_002() - { - // 4/28收盘,利息=1天 - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_PRE_EOD_003] 算头不算尾 - 首日收盘,次日平仓50%(文档4.2节) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘;2026-04-29 盘中平仓一半 - /// 操作:valueDate=2026-04-29 执行"平仓50%" - /// 取率日:2026-04-27(FR007=0.10%) - /// 口径:算头不算尾 - /// 期望:计息天数=1;利息=0.5*1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_PRE_EOD_003() - { - // 4/28收盘,利息=1天 - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #region 2.3 代码额外补充的收盘场景 - - /// - /// [10_EOD_001] 算头不算尾 - 首日收盘归档(代码实现版) - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日)执行收盘EOD归档 - /// 前置:无上一日EOD持仓(首次收盘) - /// 操作:执行 2026-04-28 收盘归档 - /// 口径:算头不算尾 - /// 说明:算头,4/28起息日算利息;不算尾指到期日不算 - /// - 算头:S=4/28 - /// - 不算尾:E=4/27(到期日4/28不算) - /// 期望:当日收盘利息=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_001() - { - var interest = CalcEod("10", new DateTime(2026, 4, 28), new List()); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_EOD_002] 算头不算尾 - 首日收盘,次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 - /// 前置:存在4/28的EOD持仓记录(待实现利息=1天) - /// 操作:valueDate=2026-04-29,执行"全平" - /// 口径:算头不算尾 - /// - 算头:4/28起息日算利息 - /// - 不算尾:4/29到期日不算利息 - /// - 历史待实现:4/28=1天 - /// - 当期利息:4/29=0天(不算尾) - /// 期望:总利息=1天+0天=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_002() - { - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); - // 平仓利息 = 历史待实现(1天) + 当期(0天) = 1天 - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_EOD_003] 算头不算尾 - 首日收盘,次日平仓50% - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行平仓一半 - /// 操作:valueDate=2026-04-29,执行"平仓50%" - /// 口径:算头不算尾,计息天数=1天 - /// 期望:总利息=(历史1天+当期0天)*50%=0.5天,利息=0.5*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_003() - { - // 4/28收盘(算头=1天利息),4/29平仓50% - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, - ExpectedInterest(1, FixedRate, 0.001m, Principal)) - }; - - // 平仓50%:总利息=(历史1天+当期0天)*50%=0.5天 - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29) - /// --------------------------------------------------------------- - /// 场景:2026-04-28 和 2026-04-29 连续两个工作日收盘归档 - /// 操作: - /// 1. 执行 2026-04-28 收盘归档 - /// 2. 执行 2026-04-29 收盘归档 - /// 口径:算头不算尾 - /// 期望: - /// - 4/28收盘利息=1天(算头,首日计息) - /// - 4/29收盘利息=1天 + 4/28累计利息 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_004() - { - // 4月28日收盘(利息=1天,算头) - var eod1 = CalcEod("10", new DateTime(2026, 4, 28), new List()); - var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected1, eod1.InterestAmount); - - // 4月29日收盘(利息=1天 + 4/28累计利息) - var eod2 = CalcEod("10", new DateTime(2026, 4, 29), new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1) - }); - // 4/29收盘利息 = 4/28累计利息 - var expected2 = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected2, eod2.InterestAmount); - } - - /// - /// [10_EOD_005] 算头不算尾 - 首日收盘后第3日收盘 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-30 执行收盘归档 - /// 背景:4/29(周三)未执行收盘归档 - /// 操作:执行 2026-04-30 收盘归档 - /// 口径:算头不算尾 - /// 期望:4/29收盘利息=1天 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_005() - { - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) - }; - - var interest = CalcEod("10", new DateTime(2026, 4, 30), eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_EOD_006] 算头不算尾 - 到期日收盘不算尾 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) - /// 操作:2027-04-27 执行收盘归档 - /// 口径:算头不算尾("10") - /// - 算头:首日4/28计息 - /// - 不算尾:到期日4/27不计息 - /// 期望:到期日收盘利息=0(到期日不算尾) - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_EOD_006() - { - // 2027-04-26 收盘归档产生的 EOD 持仓 - // 假设累计利息为 InterestProfitSum=10 - var eodPositions = new List - { - CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) - }; - - // 到期日 2027-04-27 收盘(不算尾,利息=0) - var interest = CalcEod("10", new DateTime(2027, 4, 27), eodPositions); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [11_EOD_006] 算头算尾 - 到期日收盘算尾 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 起息,2027-04-27 到期(ExerciseDate) - /// 操作:2027-04-27 执行收盘归档 - /// 口径:算头算尾("11") - /// - 算头:首日4/28计息 - /// - 算尾:到期日4/27计息 - /// 期望:到期日收盘利息=1天 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_11_EOD_006() - { - // 2027-04-26 收盘归档产生的 EOD 持仓 - // 假设累计利息为 InterestProfitSum=10 - var eodPositions = new List - { - CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) - }; - - // 到期日 2027-04-27 收盘(算尾,利息=1天) - var interest = CalcEod("11", new DateTime(2027, 4, 27), eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #region 2.3 当前营业日规则(interest_rule=0) - - /// - /// [10_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平 - /// --------------------------------------------------------------- - /// 场景:算头不算尾("10");interest_rule=0(当前营业日) - /// 操作:2026-04-28 未收盘;2026-04-29 盘中全平 - /// 前置:提供 FR007@2026-04-29 数据 - /// 取率:当前营业日规则 → 取当日 FR007@2026-04-29=0.10% - /// 口径:算头不算尾,计息天数=1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_CUR_001() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, InterestRule_Cur); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [10_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平 - /// --------------------------------------------------------------- - /// 场景:算头不算尾("10");interest_rule=0(当前营业日) - /// 操作:2026-04-28 未收盘;2026-04-30 盘中全平 - /// 前置:提供 FR007@2026-04-30 数据 - /// 取率:ResetPeriod=3天,从4/28到4/30=2天<3天(重置周期内) - /// 应取起息日利率 FR007@2026-04-28=0.10% - /// 口径:算头不算尾,计息天数=2天 - /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_10_CUR_002() - { - var interest = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, InterestRule_Cur); - var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #endregion - - #region 场景3:不算头算尾 (InterestCalcMode="01") - #region 计息区间说明: - /// 01_001: 首日(4/28)平仓 → S=4/29, E=4/28 → 0天 - /// 01_002: 次日(4/29)全平 → S=4/29, E=4/29 → 1天 - /// 01_003: 次日(4/29)半平 → 1天×50% - /// 01_004: 第3日(4/30)全平 → S=4/29, E=4/30 → 1天 - /// 01_005: 跨周期(5/6)全平 → 0天 - /// 01_EOD_001: 首日(4/28)收盘 → 0天 - /// 01_EOD_002: 4/28收盘 → 4/29全平 → 1天 - #endregion - /// ================================================================ */ - - /// - /// [01_001] 不算头算尾 - 首日起息日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日StartDate)盘中执行全平 - /// 口径:不算头算尾 - /// - 不算头:S=4/29(起息日次日) - /// - 算尾:E=4/28(操作日) - /// - 计息天数 = 4/28 - 4/29 = -1 → 0天 - /// 期望:计息天数=0天,利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_PRE_001() - { - var interest = CalcUnwind("01", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [01_002] 不算头算尾 - 次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 - /// 口径:不算头算尾 - /// - 不算头:S=4/29(下一日起息) - /// - 算尾:E=4/29(操作日) - /// - 计息天数 = 4/29 - 4/29 = 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_PRE_002() - { - var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [01_003] 不算头算尾 - 次日平仓50% - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行平仓一半 - /// 口径:不算头算尾,计息天数=1天 - /// 期望:计息天数=1天,利息=0.5*1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_PRE_003() - { - var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [01_004] 不算头算尾 - 第3日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 - /// 口径:不算头算尾 - /// - 不算头:S=4/29(下一日起息) - /// - 算尾:E=4/30(操作日) - /// - 计息天数 = 4/30 - 4/29 = 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_PRE_004() - { - var interest = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [01_EOD_001] 不算头算尾 - 首日收盘归档 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日)执行收盘EOD归档 - /// 口径:不算头算尾 - /// - 不算头:S=4/29 - /// - 算尾:E=4/28 → 计息天数=0 - /// 期望:当日收盘利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_EOD_001() - { - var interest = CalcEod("01", new DateTime(2026, 4, 28), new List()); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [01_EOD_002] 不算头算尾 - 前日已收盘,次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 - /// 口径:不算头算尾 - /// - 不算头:S=4/29 - /// - 算尾:E=4/29 - /// - 计息天数=1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_EOD_002() - { - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) - }; - var interest = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [01_005] 不算头算尾 - 跨周期全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 - /// 口径:不算头算尾 - /// - 不算头:S=5/7(下一周期起息日) - /// - 算尾:E=5/6 - /// - 计息天数 = 5/6 - 5/7 = -1 → 0天 - /// 期望:计息天数=0天,利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_01_PRE_005() - { - var interest = CalcUnwind("01", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); - Assert.AreEqual(0m, interest.InterestAmount); - } - - #endregion - - #region 场景4:不算头不算尾 (InterestCalcMode="00") - #region 计息区间说明: - /// 00_001: 首日(4/28)平仓 → S=4/29, E=4/27 → 0天 - /// 00_002: 次日(4/29)全平 → S=4/29, E=4/28 → 0天 - /// 00_003: 第3日(4/30)全平 → S=4/29, E=4/29 → 0天 - /// 00_004: 跨周期(5/6)全平 → 0天 - /// 00_EOD_001: 首日(4/28)收盘 → 0天 - /// 00_EOD_002: 4/28收盘 → 4/29全平 → 0天 - #endregion - /// ================================================================ */ - - /// - /// [00_001] 不算头不算尾 - 首日起息日平仓 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日StartDate)盘中执行全平 - /// 口径:不算头不算尾 - /// - 不算头:S=4/29 - /// - 不算尾:E=4/27 - /// - 计息天数 = 4/27 - 4/29 = -2 → 0天 - /// 期望:计息天数=0天,利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_PRE_001() - { - var interest = CalcUnwind("00", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [00_002] 不算头不算尾 - 次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 - /// 口径:不算头不算尾 - /// - 不算头:利息从4/29开始(跨到下一周期) - /// - 不算尾:E=4/28 - /// - 计息区间:4/29-5/1 → 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_PRE_002() - { - var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [00_003] 不算头不算尾 - 第3日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中全平 - /// 口径:不算头不算尾 - /// - 不算头:利息从4/30开始(跨到下一周期) - /// - 不算尾:E=4/29(减1天) - /// - 计息区间:4/30-5/1 → 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_PRE_003() - { - var interest = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - /// - /// [00_004] 不算头不算尾 - 跨周期全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 未平仓;2026-05-06 跨周期全平 - /// 口径:不算头不算尾 - /// - 不算头:S=5/7 - /// - 不算尾:E=5/5 - /// - 计息天数 = 5/5 - 5/7 = -2 → 0天 - /// 期望:计息天数=0天,利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_PRE_004() - { - var interest = CalcUnwind("00", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [00_EOD_001] 不算头不算尾 - 首日收盘归档 - /// --------------------------------------------------------------- - /// 场景:2026-04-28(起息日)执行收盘EOD归档 - /// 口径:不算头不算尾 - /// - 不算头:S=4/29 - /// - 不算尾:E=4/27 → 计息天数=0 - /// 期望:当日收盘利息=0 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_EOD_001() - { - var interest = CalcEod("00", new DateTime(2026, 4, 28), new List()); - Assert.AreEqual(0m, interest.InterestAmount); - } - - /// - /// [00_EOD_002] 不算头不算尾 - 前日已收盘,次日全平 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 已收盘归档;2026-04-29 盘中执行全平 - /// 口径:不算头不算尾 - /// - 不算头:利息从4/29开始(跨到下一周期) - /// - 不算尾:E=4/28(减1天) - /// - 计息区间:4/29-5/1 → 1天 - /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_00_EOD_002() - { - var eodPositions = new List - { - CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) - }; - var interest = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); - var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); - Assert.AreEqual(expected, interest.InterestAmount); - } - - #endregion - - #region 场景5:口径对比验证 - #region 对比测试说明: - /// COMPARE_001: 同一日(4/29)全平,4种口径对比 - /// COMPARE_002: 同一日(4/30)全平,4种口径对比 - #endregion - /// ================================================================ */ - - /// - /// [COMPARE_001] 口径对比 - 同一日(4/29)全平,4种口径对比验证 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-29 盘中执行全平 - /// 操作:对同一操作日(4/29)分别用4种计息口径执行"全平" - /// 对比结果: - /// - "11"算头算尾: S=4/28, E=4/29 → 2天 - /// - "10"算头不算尾: S=4/28, E=4/28 → 1天 - /// - "01"不算头算尾: S=4/29, E=4/29 → 1天 - /// - "00"不算头不算尾: S=4/29, E=5/1 → 1天(中间日期跨周期) - /// 期望:验证4种口径的差异符合预期 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_COMPARE_001() - { - // "11"算头算尾: S=4/28, E=4/29 => 2天 - var interest11 = CalcUnwind("11", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest11.InterestAmount); - - // "10"算头不算尾: S=4/28, E=4/28 => 1天 - var interest10 = CalcUnwind("10", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest10.InterestAmount); - - // "01"不算头算尾: S=4/29, E=4/29 => 1天 - var interest01 = CalcUnwind("01", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); - - // "00"不算头不算尾: S=4/29, E=5/1 => 1天(中间日期跨周期) - var interest00 = CalcUnwind("00", new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); - Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); - } - - /// - /// [COMPARE_002] 口径对比 - 同一日(4/30)全平,4种口径对比验证 - /// --------------------------------------------------------------- - /// 场景:2026-04-28 盘中未平仓;2026-04-30 盘中执行全平(跨周末) - /// 操作:对同一操作日(4/30)分别用4种计息口径执行"全平" - /// 对比结果: - /// - "11"算头算尾: S=4/28, E=4/30 → 3天 - /// - "10"算头不算尾: S=4/28, E=4/29 → 2天 - /// - "01"不算头算尾: S=4/29, E=4/30 → 1天 - /// - "00"不算头不算尾: S=4/30, E=5/1 → 1天(中间日期跨周期) - /// 期望:验证4种口径的差异符合预期 - /// --------------------------------------------------------------- - /// - [TestMethod] - public void UT_SWAP_INT_COMPARE_002() - { - // "11"算头算尾: S=4/28, E=4/30 => 3天 - var interest11 = CalcUnwind("11", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - Assert.AreEqual(ExpectedInterest(3, FixedRate, 0.001m, Principal), interest11.InterestAmount); - - // "10"算头不算尾: S=4/28, E=4/29 => 2天 - var interest10 = CalcUnwind("10", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - Assert.AreEqual(ExpectedInterest(2, FixedRate, 0.001m, Principal), interest10.InterestAmount); - - // "01"不算头算尾: S=4/29, E=4/30 => 1天 - var interest01 = CalcUnwind("01", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest01.InterestAmount); - - // "00"不算头不算尾: S=4/30, E=5/1 => 1天(中间日期跨周期) - var interest00 = CalcUnwind("00", new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); - Assert.AreEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), interest00.InterestAmount); - } - - #endregion - - } -} diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs new file mode 100644 index 00000000..90067ce5 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T0.cs @@ -0,0 +1,1078 @@ +using Newtonsoft.Json; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换利息计算单元测试 - T+0场景 + /// ================================================================ + /// T+0定义:起息日(StartDate) = 成交日(TradeDate),不额外加1天 + /// TradeDate=2026-04-27, StartDate=2026-04-27 + /// ---------------------------------------------------------------- + /// 测试口径: + /// "10" = 算头不算尾(含起息日,不含操作日) + /// "11" = 算头算尾(含起息日和操作日) + /// ---------------------------------------------------------------- + /// 与T+1的关键差异: + /// T+1: StartDate=4/28, 4/29平仓(算头不算尾)→1天 + /// T+0: StartDate=4/27, 4/28平仓(算头不算尾)→1天 (所有天数+1) + /// ---------------------------------------------------------------- + /// Excel覆盖的T+0算头不算尾场景: + /// 固定利率:T+0固定正利率、T+0固定负利率 + /// 浮动利率:T+0浮动加点(当前营业日/前一营业日/单利) + /// 每个场景 × 4业务场景(浮动×3) + /// ================================================================ + /// + [TestClass] + public class GetInterestsUnitTest_T0 + { + #region 内部类:浮动利率模拟服务 + + private sealed class StubSwapDealService : SwapDealService + { + private readonly IReadOnlyDictionary _floatRates; + + public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) + { + _floatRates = floatRates; + } + + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) + { + rate = 0; + return false; + } + if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true; + rate = 0; + return false; + } + } + + #endregion + + #region 测试常量 + + private const decimal Principal = 1000m; + private const decimal FixedRate = 0.01m; + private const decimal FixedRatePositive = 0.0075m; + private const decimal FixedRateNegative = -0.0105m; + private const decimal FloatMinusRate = -0.021m; + private const decimal FloatPlusRate = 0.0025m; + private const int AnnualDays = 365; + private const int ResetPeriod = 3; + private const int ResetPeriodFixed = 1; + private const int InterestRule_Pre = -1; + private const int InterestRule_Cur = 0; + + // T+0: StartDate = TradeDate(不额外加1天) + private static readonly DateTime TradeDate = new(2026, 4, 27); + private static readonly DateTime StartDate = new(2026, 4, 27); // = TradeDate + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + + private SwapDealService _service; + private IReadOnlyDictionary _floatRates; + + [TestInitialize] + public void Init() + { + _floatRates = new Dictionary + { + [new DateTime(2026, 4, 24)] = 0.001, // InterestRule_Pre: GetNonHolidayDefore(4/26日)→4/24 + [new DateTime(2026, 4, 26)] = 0.001, // 新增:T+0前一营业日场景需要 + [new DateTime(2026, 4, 27)] = 0.001, + [new DateTime(2026, 4, 28)] = 0.001, + [new DateTime(2026, 4, 29)] = 0.001, + [new DateTime(2026, 4, 30)] = 0.002, + [new DateTime(2026, 5, 1)] = 0.002, // 复利从头算需要完整日期范围 + [new DateTime(2026, 5, 3)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 5)] = 0.002, // InterestRule_Pre取率日 + [new DateTime(2026, 5, 6)] = 0.002, + [new DateTime(2027, 4, 23)] = 0.001, + [new DateTime(2027, 4, 24)] = 0.001, + [new DateTime(2027, 4, 25)] = 0.001, + [new DateTime(2027, 4, 26)] = 0.001, + [new DateTime(2027, 4, 27)] = 0.001 + }; + _service = new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsUnitTest_T0), OptUserFrom.UnitTest), + _floatRates); + } + + #endregion + + #region 测试数据构建器 + + private static trade CreateTrade(string interestCalcMode, int interestRule = InterestRule_Pre) + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = interestCalcMode, + SettlementRules = interestRule + }) + }; + return new trade + { + id = 1, TradeNumber = "UT-SWAP-INT-T0-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + trade_extend = extend + }; + } + + private static swap_position CreateFloatInterestPosition( + int interestRule = InterestRule_Cur, InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal fixedRate = 0.01m, SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + var intervalModels = new List + { + new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 } + }; + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)direction, InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)interestType, + IsAnnualized = true, interest_rest_days = ResetPeriod, + interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + private static swap_position CreateFixedInterestPosition( + decimal fixedRate = 0.0075m, int interestRule = InterestRule_Cur, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + var intervalModels = new List + { + new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 } + }; + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)direction, InterestMode = (int)InterestModeEnum.合约名义本金规模, + InterestRateDefault = fixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = ResetPeriodFixed, + interest_rule = interestRule, FloatRateUnderlyingCode = null, + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) + { + return new eod_swap_position + { + id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = valueDate, + ClientId = 999998, FloatRate = floatRate, TdInterestPrincipal = tdPrincipal, + PosiNotionalValue = tdPrincipal, InterestProfitSum = interestSum + }; + } + + /// + /// 计算含预EOD利息的总期望利息(匹配生产代码中间舍入行为) + /// --------------------------------------------------------------- + /// 生产代码先取 preEod.InterestProfitSum(已舍入到11位的DB值), + /// 再加上新期间日度原始利息,最后再舍入一次 + /// + private static decimal ExpectedInterestWithPreEod( + int newDays, decimal fixedRate, decimal floatRate, decimal principal, + decimal preEodInterestSum, decimal closePercent) + { + var yearlyRate = fixedRate + floatRate; + var newRawInterest = principal * yearlyRate * newDays / AnnualDays; + return Math.Round(preEodInterestSum * closePercent + newRawInterest * closePercent, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + /// + /// 容忍末位差异的利息比较(允许相差2位) + /// + private static void AssertInterestEqual(decimal expected, decimal actual) + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + string.Format("Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, expected - actual)); + } + + private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) + { + var yearlyRate = fixedRate + floatRate; + var interest = principal * yearlyRate * days / AnnualDays; + return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + #endregion + + #region 通用调用方法 + + // --- 浮动利率 --- + private swap_flow_event CalcFloatUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, InterestTypeEnum interestType, + List eodPositions = null, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, + eodPositions, new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFloatEod(string calcMode, DateTime valueDate, + int interestRule, decimal fixedRate, InterestTypeEnum interestType, + List eodPositions = null, List closeList = null) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, + eodPositions, new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + // --- 固定利率 --- + private swap_flow_event CalcFixedUnwind(string calcMode, DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + List eodPositions = null, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, unwindDate, + eodPositions, new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFixedEod(string calcMode, DateTime valueDate, + int interestRule, decimal fixedRate, List eodPositions = null, + List closeList = null) + { + eodPositions ??= new List(); + var td = CreateTrade(calcMode, interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + var interests = _service.GetInterests(td, td.trade_extend, valueDate, valueDate, + eodPositions, new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + // ================================================================ + // T+0场景:StartDate = TradeDate = 2026-04-27 + // 算头不算尾("10"):4/27平仓→0天, 4/28平仓→1天, 4/29平仓→2天 + // 算头算尾("11"):4/27平仓→1天, 4/28平仓→2天, 4/29平仓→3天 + // ================================================================ + + #region 1. T+0固定正利率 算头不算尾("10") - interest_rule=0 + + /// + /// [UT_T0_FIX_POS_001] T+0固定正利率算头不算尾-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→S=4/27,E=4/27→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_001() + { + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_002] T+0固定正利率算头不算尾-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→1+1=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_003] T+0固定正利率算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_004] T+0固定正利率算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天×50%; 5/6全平剩余→EOD=4/28, newCalcLast=true + /// 算头不算尾,newCalcLast强制算尾: 4/29~5/6=8天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_004() + { + // 4/28部分平仓50%(算头不算尾→S=4/27,E=4/27→1天) + var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); + + // 5/6全平剩余50%(EOD=4/28, newCalcLast=true强制算尾: 4/29~5/6=8天) + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); + var expected = ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 2. T+0固定负利率 算头不算尾("10") - interest_rule=0 + + /// + /// [UT_T0_FIX_NEG_001] T+0固定负利率算头不算尾-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→1天, 负利率-1.05% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_001() + { + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRateNegative); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_002] T+0固定负利率算头不算尾-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, 负利率-1.05% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRateNegative, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_003] T+0固定负利率算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var interest = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRateNegative, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal)*0.5m, interest.InterestAmount); + } + + /// + /// [UT_T0_FIX_NEG_004] T+0固定负利率算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天×50%; 5/6全平剩余→newCalcLast=true, 8天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_NEG_004() + { + var u1 = CalcFixedUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRateNegative); + AssertInterestEqual(ExpectedInterest(1, FixedRateNegative, 0m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRateNegative, eod, Principal * 0.5m, newCalcLast: true); + var expected = ExpectedInterestWithPreEod(8, FixedRateNegative, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 3. T+0浮动加点(当前营业日,复利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_CUR_002] T+0浮动加点(当前营业日,复利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, FR007+0.25% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_003] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_004] T+0浮动加点(当前营业日,复利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平50%→1天; 5/6全平剩余→复利从头算9天[27-29][30-2][3-5] + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List(); + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:9天, 每3天重置, [27-29]@0.35%, [30-2,3-5]@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expected = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 4. T+0浮动加点(前一营业日,复利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_PRE_002] T+0浮动加点(前一营业日,复利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_003] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_004] T+0浮动加点(前一营业日,复利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→1天@0.10%; 5/6全平→复利从头算9天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:9天, 每3天重置, [27-29]@0.001, [30-2]@0.001, [3-5]@0.002 → 6@0.35%+3@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest2 = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest2; + interest2 += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; + } + var expected = Math.Round(interest2, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 5. T+0浮动加点(单利)算头不算尾("10") + + /// + /// [UT_T0_FLT_PLUS_SI_002] T+0浮动加点(单利)算头不算尾-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天, 单利 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_003] T+0浮动加点(单利)算头不算尾-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头不算尾→1天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal*0.5m), interest.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_004] T+0浮动加点(单利)算头不算尾-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→1天; 5/6全平→9天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_004() + { + var u1 = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("10", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); + // 单利: 9天, [27,28,29]@0.001 +[30,1,2]@0.001 + [3,4,5]@0.002 + var raw = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + + Principal * 0.5m * (FloatPlusRate + 0.002m) * 3 / AnnualDays; + var expected = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, u2.InterestAmount); + } + + #endregion + + #region 6. T+0固定正利率 算头算尾("11") - interest_rule=0 + + /// + /// [UT_T0_FIX_POS_11_001] 算头算尾 T+0固定正利率-未收盘平仓 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头算尾→S=4/27,E=4/28→2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_001() + { + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_002] 算头算尾 T+0固定正利率-收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→1+1=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_003] 算头算尾 T+0固定正利率-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var i = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_POS_11_004] 算头算尾 T+0固定正利率-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天×50%; 5/6全平→EOD=4/28, 算头算尾(newCalcLast=true无影响) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_POS_11_004() + { + var u1 = CalcFixedUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var u2 = CalcFixedUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FixedRatePositive, eod, Principal * 0.5m, newCalcLast: true); + AssertInterestEqual(ExpectedInterestWithPreEod(8, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m), u2.InterestAmount); + } + + #endregion + + #region 7. T+0浮动加点(当前营业日)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_CUR_11_002] 算头算尾 T+0浮动加点(当前营业日,复利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_11_003] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_CUR_11_004] 算头算尾 T+0浮动加点(当前营业日,复利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天; 5/6全平→复利从头算10天 [27-29] [30-2] [3-5] [6] + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_CUR_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Cur, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m, newCalcLast: false); + // 复利从头算:10天, 每3天重置, [27-29]@0.35%, [30-2,3-5,6]@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 10; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 8. T+0浮动加点(前一营业日)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_PRE_11_002] 算头算尾 T+0浮动加点(前一营业日,复利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_11_003] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_PRE_11_004] 算头算尾 T+0浮动加点(前一营业日,复利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天@0.10%; 5/6全平→复利从头算10天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_PRE_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.复利, eod, Principal * 0.5m); + // 复利从头算:10天, 每3天重置, [27-29,30-2]@0.001, [3-5,6]@0.002 → 6@0.35%+4@0.45% + var principal = Principal * 0.5m; + var rate1 = FloatPlusRate + 0.001m; + var rate2 = FloatPlusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 10; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 6 ? rate1 : rate2) / AnnualDays; + } + var expected1 = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 9. T+0浮动加点(单利)算头算尾("11") + + /// + /// [UT_T0_FLT_PLUS_SI_11_002] 算头算尾 T+0浮动加点(单利)-收盘后次日全平 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头算尾→2天, 单利 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_002() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FloatPlusRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_11_003] 算头算尾 T+0浮动加点(单利)-部分平仓 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28半平50%, 算头算尾→2天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_003() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var i = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FloatPlusRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_PLUS_SI_11_004] 算头算尾 T+0浮动加点(单利)-部分平仓后全平 + /// --------------------------------------------------------------- + /// 4/28半平→2天; 5/6全平→算头算尾 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_PLUS_SI_11_004() + { + var u1 = CalcFloatUnwind("11", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 0.5m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m), u1.InterestAmount); + + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatPlusRate, 0.001m, Principal * 0.5m)) + }; + var u2 = CalcFloatUnwind("11", new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + InterestRule_Pre, FloatPlusRate, InterestTypeEnum.单利, eod, Principal * 0.5m); + // 单利: 10天, [27,28,29]@0.001 + [30,1,2]@0.001 + [3,4,5,6]@0.002 → 3@0.35% + 3@0.35% + 3@0.45% + var raw1 = Principal * 0.5m * (FloatPlusRate + 0.001m) * 6 / AnnualDays + + Principal * 0.5m * (FloatPlusRate + 0.002m) * 4 / AnnualDays; + var expected1 = Math.Round(raw1, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected1, u2.InterestAmount); + } + + #endregion + + #region 10. T+0固定利率 - 收盘归档 + + /// + /// [UT_T0_FIX_EOD_001] T+0固定利率算头不算尾-首日收盘归档 + /// --------------------------------------------------------------- + /// 4/27(起息日)执行收盘, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_EOD_001() + { + var i = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); + AssertInterestEqual(ExpectedInterest(1, FixedRatePositive, 0m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FIX_EOD_002] T+0固定利率算头不算尾-连续收盘 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28收盘, 算头不算尾→各1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FIX_EOD_002() + { + var e1 = CalcFixedEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRatePositive); + var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1, e1.InterestAmount); + + var e2 = CalcFixedEod("10", new DateTime(2026, 4, 28), InterestRule_Cur, FixedRatePositive, + new List { CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0m, expected1) }); + AssertInterestEqual(expected1*2, e2.InterestAmount); + } + + #endregion + + #region 11. T+0浮动利率 - 基础场景 + + /// + /// [UT_T0_FLT_BASE_001] T+0浮动利率基础-起息日平仓→0天 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/27平仓, 算头不算尾→0天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_001() + { + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 27), new DateTime(2026, 4, 27), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(0m, i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_002] T+0浮动利率基础-第2天全平→1天 + /// --------------------------------------------------------------- + /// StartDate=4/27, 4/28平仓, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_002() + { + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_003] T+0浮动利率基础-首日收盘→1天 + /// --------------------------------------------------------------- + /// 4/27执行收盘, 算头不算尾→1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_003() + { + var i = CalcFloatEod("10", new DateTime(2026, 4, 27), InterestRule_Cur, FixedRate, InterestTypeEnum.单利); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + /// + /// [UT_T0_FLT_BASE_004] T+0浮动利率基础-首日收盘+次日全平→2天 + /// --------------------------------------------------------------- + /// 4/27收盘+4/28全平, 算头不算尾→2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_T0_FLT_BASE_004() + { + var eod = new List + { + CreateEodPosition(new DateTime(2026, 4, 27), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var i = CalcFloatUnwind("10", new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m, + InterestRule_Cur, FixedRate, InterestTypeEnum.单利, eod); + AssertInterestEqual(ExpectedInterest(1, FixedRate, 0.001m, Principal), i.InterestAmount); + } + + #endregion + } +} diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs new file mode 100644 index 00000000..d1a94453 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsUnitTest_T1.cs @@ -0,0 +1,1760 @@ +using Newtonsoft.Json; +using System.Security.Principal; +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换利息计算单元测试 - T+1场景 + /// ================================================================ + /// T+1定义:起息日(StartDate) = 成交日(TradeDate) + 1天 + /// TradeDate=2026-04-27, StartDate=2026-04-28 + /// ---------------------------------------------------------------- + /// 测试口径: + /// "10" = 算头不算尾(含起息日,不含操作日) + /// "11" = 算头算尾(含起息日和操作日) + /// ---------------------------------------------------------------- + /// 计息逻辑说明(算头不算尾): + /// calcFirst=true(算头), calcLast=false(不算尾) + /// 计息区间:从StartDate到valueDate-1天 + /// 例如:StartDate=4/28, valueDate=4/29 → 计息区间=4/28(1天) + /// ---------------------------------------------------------------- + /// 统一测试数据: + /// - Principal=1000, AnnualDays=365 + /// - ResetPeriod=3天(浮动利率)/ 1天(固定利率) + /// - InterestRule=-1(前一营业日), 0(当前营业日) + /// - FR007@2026-04-27=0.10%, FR007@2026-04-30=0.20% + /// ---------------------------------------------------------------- + /// Excel覆盖的T+1场景: + /// 固定利率:T+1固定正利率、T+1固定负利率 + /// 浮动利率:T+1浮动减点(前一/当前营业日,单/复利) + /// 每个场景 × 4业务场景(浮动×3) + /// ================================================================ + /// + [TestClass] + public class GetInterestsUnitTest_T1 + { + #region 内部类:浮动利率模拟服务 + + /// + /// StubSwapDealService - 模拟浮动利率获取 + /// 用于单元测试中预置FR007价格,避免依赖外部数据源 + /// + private sealed class StubSwapDealService : SwapDealService + { + private readonly IReadOnlyDictionary _floatRates; + + public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) + { + _floatRates = floatRates; + } + + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) + { + rate = 0; + return false; + } + + if (_floatRates.TryGetValue(valueDate.Date, out rate)) + { + return true; + } + + rate = 0; + return false; + } + } + + #endregion + + #region 测试常量与共享变量 + + private const decimal Principal = 1000m; // 本金:1000 + private const decimal FixedRate = 0.01m; // 固定利率:1.00% + private const decimal FixedRatePositive = 0.0075m; // 固定正利率:0.75%(Excel场景) + private const decimal FixedRateNegative = -0.0105m; // 固定负利率:-1.05%(Excel场景) + private const int AnnualDays = 365; // 年化天数 + private const int ResetPeriod = 3; // 重置周期:3天(浮动利率) + private const int ResetPeriodFixed = 1; // 重置周期:1天(固定利率) + private const int InterestRule_Pre = -1; // 前一营业日规则 + private const int InterestRule_Cur = 0; // 当前营业日规则 + + private static readonly DateTime TradeDate = new(2026, 4, 27); // 成交日 + private static readonly DateTime StartDate = new(2026, 4, 28); // 起息日(开始计息日) + private static readonly DateTime ExerciseDate = new(2027, 4, 27); // 到期日 + + private SwapDealService _service; + private IReadOnlyDictionary _floatRates; + + [TestInitialize] + public void Init() + { + // 预置FR007价格数据 + _floatRates = new Dictionary + { + [new DateTime(2026, 4, 27)] = 0.001, // FR007@2026-04-27 = 0.10% + [new DateTime(2026, 4, 28)] = 0.001, // FR007@2026-04-28 = 0.10% + [new DateTime(2026, 4, 29)] = 0.001, // FR007@2026-04-29 = 0.10% + [new DateTime(2026, 4, 30)] = 0.002, // FR007@2026-04-30 = 0.20% + [new DateTime(2026, 5, 1)] = 0.002, // 复利从头算需要完整日期范围 + [new DateTime(2026, 5, 3)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 4)] = 0.002, // 复利重置日取FR007 + [new DateTime(2026, 5, 6)] = 0.002, // FR007@2026-05-06 = 0.20% + // 到期日测试用例需要的利率数据(2027年) + [new DateTime(2027, 4, 23)] = 0.001, + [new DateTime(2027, 4, 24)] = 0.001, + [new DateTime(2027, 4, 25)] = 0.001, + [new DateTime(2027, 4, 26)] = 0.001, + [new DateTime(2027, 4, 27)] = 0.001 + }; + + _service = new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsUnitTest_T1), OptUserFrom.UnitTest), + _floatRates); + } + + #endregion + + #region 测试数据构建器 + + /// + /// 创建测试用交易对象 + /// + /// 计息口径:"10"=算头不算尾 + /// 取率规则:-1=前一营业日,0=当前营业日 + private static trade CreateTrade(string interestCalcMode = "10", int interestRule = InterestRule_Pre) + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = interestCalcMode, + SettlementRules = interestRule + }) + }; + + return new trade + { + id = 1, + TradeNumber = "UT-SWAP-INT-001", + ClientId = 999998, + TradeType = "收益互换", + TradeDate = TradeDate, + StartDate = StartDate, + ExerciseDate = ExerciseDate, + TradeStatus = "确认成交", + ValidState = "Valid", + trade_extend = extend + }; + } + + /// + /// 创建浮动利率测试用持仓对象(Excel场景:FR007+固定利率) + /// + private static swap_position CreateFloatInterestPosition( + int interestRule = InterestRule_Pre, + InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal fixedRate = 0.01m, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + var intervalModels = new List + { + new IntervalModel + { + Date = ExerciseDate, + Rate = fixedRate, + Settlement = 0 + } + }; + + return new swap_position + { + id = 1001, + SwapTradeId = 1, + PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)direction, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestRateDefault = fixedRate, + InterestPrincipalFix = Principal, + PosiStartDate = StartDate, + PosiMatuirityDate = ExerciseDate, + IsInitial = true, + Invalid = false, + InterestType = (int)interestType, + IsAnnualized = true, + interest_rest_days = ResetPeriod, + interest_rule = interestRule, + FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + /// + /// 创建固定利率测试用持仓对象(无浮动利率标的,纯固定利率) + /// 对应Excel中的固定利率场景 + /// + private static swap_position CreateFixedInterestPosition( + decimal fixedRate = 0.0075m, + int interestRule = InterestRule_Pre, + SwapDirectionEnum direction = SwapDirectionEnum.收取) + { + var intervalModels = new List + { + new IntervalModel + { + Date = ExerciseDate, + Rate = fixedRate, + Settlement = 0 + } + }; + + return new swap_position + { + id = 1001, + SwapTradeId = 1, + PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)direction, + InterestMode = (int)InterestModeEnum.合约名义本金规模, + InterestRateDefault = fixedRate, + InterestPrincipalFix = Principal, + PosiStartDate = StartDate, + PosiMatuirityDate = ExerciseDate, + IsInitial = true, + Invalid = false, + InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, + interest_rest_days = ResetPeriodFixed, + interest_rule = interestRule, + FloatRateUnderlyingCode = null, // 无浮动利率标的 + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + /// + /// 创建日终持仓记录(EOD归档数据) + /// + private static eod_swap_position CreateEodPosition(DateTime valueDate, decimal tdPrincipal, decimal floatRate, decimal interestSum) + { + return new eod_swap_position + { + id = 1, + SwapTradeId = 1, + PositionId = 1001, + ValueDate = valueDate, + ClientId = 999998, + FloatRate = floatRate, + TdInterestPrincipal = tdPrincipal, + PosiNotionalValue = tdPrincipal, + InterestProfitSum = interestSum + }; + } + + /// + /// 计算期望利息金额(先累加原始值,最后一次性舍入,比较时比生产少2位容错) + /// + /// + /// 容忍末位差异的利息比较(允许相差2位) + /// + private static void AssertInterestEqual(decimal expected, decimal actual) + { + var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2); + Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, + string.Format("Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, expected - actual)); + } + + private static decimal ExpectedInterest(int days, decimal fixedRate, decimal floatRate, decimal principal) + { + var yearlyRate = fixedRate + floatRate; + var interest = principal * yearlyRate * days / AnnualDays; + return Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + /// + /// 计算含预EOD利息的总期望利息(比生产少2位精度容错) + /// + private static decimal ExpectedInterestWithPreEod( + int newDays, decimal fixedRate, decimal floatRate, decimal principal, + decimal preEodInterestSum, decimal closePercent) + { + var yearlyRate = fixedRate + floatRate; + var newRawInterest = principal * yearlyRate * newDays / AnnualDays; + return Math.Round(preEodInterestSum * closePercent + newRawInterest * closePercent, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + } + + #endregion + + #region 通用的GetInterests调用方法 + + /// + /// 通用浮动利率平仓计算(不含eodPositions) + /// + private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null, bool newCalcLast = false) + { + return CalcFloatUnwind(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, interestType, closeList: closeList, newCalcLast: newCalcLast); + } + + /// + /// 通用浮动利率平仓计算(含eodPositions) + /// + private swap_flow_event CalcFloatUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m, + InterestTypeEnum interestType = InterestTypeEnum.单利, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用浮动利率收盘计算 + /// + private swap_flow_event CalcFloatEod(DateTime valueDate, + List eodPositions, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.01m, InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用浮动利率自动互换计算 + /// + private swap_flow_event CalcFloatAutoSwap(DateTime valueDate, + List eodPositions, decimal closePercent = 1m, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.01m, + InterestTypeEnum interestType = InterestTypeEnum.单利, + List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, closePercent, + (int)SwapEventTypeEnum.自动互换, + false, false, 0, Principal, false, settment: false, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用固定利率平仓计算 + /// + private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m, + List closeList = null, bool newCalcLast = false) + { + return CalcFixedUnwind(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, closeList: closeList, newCalcLast: newCalcLast); + } + + /// + /// 通用固定利率平仓计算(含eodPositions) + /// + private swap_flow_event CalcFixedUnwind(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule = InterestRule_Pre, decimal fixedRate = 0.0075m, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("10", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + /// + /// 通用固定利率收盘计算 + /// + private swap_flow_event CalcFixedEod(DateTime valueDate, + List eodPositions, int interestRule = InterestRule_Pre, + decimal fixedRate = 0.0075m, List closeList = null) + { + var td = CreateTrade("10", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, valueDate, + eodPositions, + new List { position }, + Principal, Principal, Principal, Principal, 1m, + (int)SwapEventTypeEnum.平仓, + false, false, 0, Principal, false, settment: true, newCalcLast: false, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + // ================================================================ + // 所有测试均使用 InterestCalcMode="10"(算头不算尾) + // ================================================================ + + #region 场景1:浮动利率算头不算尾 - 盘中平仓场景 + + /// + /// [FLOAT_UNWIND_001] 算头不算尾 - 首日起息日平仓 + /// --------------------------------------------------------------- + /// 场景:StartDate=4/28盘中执行全平 + /// 口径:算头不算尾 + /// - 算头:S=4/28 + /// - 不算尾:E=4/27(操作日前一日) + /// - 计息天数 = 0天 + /// 期望:利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_001() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 28), new DateTime(2026, 4, 28), 1m); + AssertInterestEqual(0m, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_002] 算头不算尾 - 次日全平(基准场景) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中全平 + /// 口径:算头不算尾 → 计息区间:4/28(1天) + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_002() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_003] 算头不算尾 - 次日平仓50% + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中平仓一半 + /// 口径:算头不算尾,计息天数=1天 + /// 期望:利息=0.5*1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_003() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expected = ExpectedInterestWithPreEod(0, FixedRate, 0.001m, Principal, ExpectedInterest(1, FixedRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_004] 算头不算尾 - 第3日全平(跨周末) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/30盘中全平 + /// 口径:算头不算尾 → 计息区间:4/28~4/29(2天) + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_004() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_005] 算头不算尾 - 第3日平仓50%(跨周末) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/30盘中平仓一半 + /// 口径:算头不算尾,计息天数=2天 + /// 期望:利息=0.5*2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_005() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 0.5m); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_006] 算头不算尾 - 次日半平 + 第3日收盘 + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29盘中平仓一半;4/30收盘 + /// 期望: + /// - 4/29平仓利息=0.5*1*(1.00%+0.10%)*1000/365 + /// - 4/30收盘利息=剩余50%*1天利息 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_006() + { + // 第一步:4月29日平仓50% + var unwindInterest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m); + var expectedUnwind = ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind, unwindInterest.InterestAmount); + + // 第二步:4月30日收盘(剩余50%持仓计息1天) + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal * 0.5m)) + }; + var eodInterest = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(2, FixedRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_007] 算头不算尾 - 第3日直接收盘(已有前日EOD) + /// --------------------------------------------------------------- + /// 场景:4/28未平仓;4/29已收盘归档;4/30收盘 + /// 期望:4/30收盘待实现利息=3天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_007() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, + ExpectedInterest(2, FixedRate, 0.001m, Principal)) + }; + var eodInterest = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expectedEod = ExpectedInterest(3, FixedRate, 0.001m, Principal * 1m); + AssertInterestEqual(expectedEod, eodInterest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_008] 算头不算尾 - 次日自动互换 + /// --------------------------------------------------------------- + /// 场景:4/29执行"自动互换" + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_008() + { + var interest = CalcFloatAutoSwap(new DateTime(2026, 4, 29), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_009] 算头不算尾 - 自动互换后次日平仓 + /// --------------------------------------------------------------- + /// 场景:4/29已自动互换;4/30执行"全平" + /// 期望:计息天数=0天,利息=0 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_009() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, 0m) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, eodPositions); + AssertInterestEqual(0m, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_010] 算头不算尾 - 跨重置周期全平(中间有收盘) + /// --------------------------------------------------------------- + /// 场景:4/29收盘归档;5/6全平(跨周期) + /// 期望:分段计息,累计利息=4/29收盘+4/30持仓+5/1~5/5持仓 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_010() + { + var oneDay = ExpectedInterest(1, FixedRate, 0.001m, Principal); + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal, 0.001m, oneDay) + }; + + var interest = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); + var expected = Math.Round(oneDay * 2 + secondPeriod, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_UNWIND_010A] 已有前次结算时,浮动利率重置起点应按当前计息段起点计算 + /// --------------------------------------------------------------- + /// 场景:5/3已有EOD;5/6全平;重置周期=3天,取率规则=当前营业日 + /// 期望:应按 5/3~5/6 这一段判断重置,取到 5/6 的 0.20% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_010A() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 5, 3), Principal, 0.001m, 0m) + }; + + var interest = CalcFloatUnwind( + new DateTime(2026, 5, 6), + new DateTime(2026, 5, 6), + 1m, + eodPositions, + InterestRule_Cur); + + Assert.AreEqual(0.002m, interest.FloatRate); + } + + /// + /// [FLOAT_UNWIND_011] 算头不算尾 - 跨重置周期全平(中间无收盘) + /// --------------------------------------------------------------- + /// 场景:4/28起息;5/6全平(4/29未收盘) + /// 期望:3天@0.10% + 5天@0.20% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_UNWIND_011() + { + var eodPositions = new List(); + var interest = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, eodPositions); + var firstPeriod = ExpectedInterest(3, FixedRate, 0.001m, Principal); + var secondPeriod = ExpectedInterest(5, FixedRate, 0.002m, Principal); + var expected = Math.Round(firstPeriod + secondPeriod, + ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expected, interest.InterestAmount); + } + + #endregion + + #region 场景2:浮动利率算头不算尾 - 收盘归档场景 + + /// + /// [FLOAT_EOD_001] 算头不算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:4/28执行收盘EOD归档(首次收盘) + /// 期望:当日收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_001() + { + var interest = CalcFloatEod(new DateTime(2026, 4, 28), new List()); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_002] 算头不算尾 - 首日收盘,次日全平 + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/29盘中全平 + /// 期望:总利息=历史1天+当期0天=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_003] 算头不算尾 - 首日收盘,次日平仓50% + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/29盘中平仓一半 + /// 期望:总利息=(历史1天+当期0天)*50%=0.5天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FixedRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, eodPositions); + var expected = ExpectedInterestWithPreEod(0, FixedRate, 0.001m, Principal, ExpectedInterest(1, FixedRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_004] 算头不算尾 - 连续收盘(4/28、4/29) + /// --------------------------------------------------------------- + /// 场景:4/28和4/29连续两个工作日收盘归档 + /// 期望:4/29收盘累计利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_004() + { + var eod1 = CalcFloatEod(new DateTime(2026, 4, 28), new List()); + var expected1 = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected1, eod1.InterestAmount); + + var eod2 = CalcFloatEod(new DateTime(2026, 4, 29), new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, expected1) + }); + var expected2 = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected2, eod2.InterestAmount); + } + + /// + /// [FLOAT_EOD_005] 算头不算尾 - 首日收盘后第3日收盘 + /// --------------------------------------------------------------- + /// 场景:4/28已收盘;4/30执行收盘(4/29未收盘) + /// 期望:4/29收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_005() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, 0m) + }; + var interest = CalcFloatEod(new DateTime(2026, 4, 30), eodPositions); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_EOD_006] 算头不算尾 - 到期日收盘不算尾 + /// --------------------------------------------------------------- + /// 场景:4/28起息,2027-04-27到期 + /// 操作:2027-04-27执行收盘归档 + /// 期望:到期日收盘利息=0(不算尾) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_EOD_006() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0.001m, 10m) + }; + var interest = CalcFloatEod(new DateTime(2027, 4, 27), eodPositions); + AssertInterestEqual(0m, interest.InterestAmount); + } + + #endregion + + #region 场景3:浮动利率算头不算尾 - 当前营业日规则(interest_rule=0) + + /// + /// [FLOAT_CUR_001] 算头不算尾 + 当前营业日规则 - 次日全平 + /// --------------------------------------------------------------- + /// 场景:interest_rule=0(当前营业日),4/29盘中全平 + /// 期望:计息天数=1天,利息=1*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_CUR_001() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, InterestRule_Cur); + var expected = ExpectedInterest(1, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLOAT_CUR_002] 算头不算尾 + 当前营业日规则 - 第3日全平 + /// --------------------------------------------------------------- + /// 场景:interest_rule=0(当前营业日),4/30盘中全平 + /// 期望:计息天数=2天,利息=2*(1.00%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLOAT_CUR_002() + { + var interest = CalcFloatUnwind(new DateTime(2026, 4, 30), new DateTime(2026, 4, 30), 1m, InterestRule_Cur); + var expected = ExpectedInterest(2, FixedRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:固定利率算头不算尾 + // 维度:T+1/T+0 × 正利率/负利率 × 4业务场景 + // ================================================================ + + #region 场景4:固定利率算头不算尾 - T+1固定正利率(前一营业日,正利率0.75%) + + /// + /// [FIX_POS_T1_001] T+1固定正利率算头不算尾 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 业务场景1:固定利率未收盘平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28起息,4/29盘中全平 + /// 期望:计息天数=1天,利息=1*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_001() + { + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_002] T+1固定正利率算头不算尾 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=历史1天+当期0天=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_003] T+1固定正利率算头不算尾 - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterestWithPreEod(0, FixedRatePositive, 0m, Principal, ExpectedInterest(1, FixedRatePositive, 0m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_004] T+1固定正利率算头不算尾 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=0.75% + /// 操作:4/29部分平仓50%;经过4/29收盘、4/30收盘;5/6全部平仓剩余50% + /// 期望:4/29平仓=0.5天+5/6平仓=剩余×累计天数 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRatePositive); + var expectedUnwind1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%(经过4/29收盘和4/30收盘) + // newCalcLast=true: 4/30~5/6=7天 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive, Principal * 0.5m, newCalcLast: true); + // newCalcLast=true强制算尾: 4/30~5/6=7天 + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景6:固定利率算头不算尾 - T+1固定负利率(前一营业日,负利率-1.05%) + + /// + /// [FIX_NEG_T1_001] T+1固定负利率算头不算尾 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 业务场景1:固定利率未收盘平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28起息,4/29盘中全平 + /// 期望:计息天数=1天,利息=1*(-1.05%)*1000/365(负利息) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_001() + { + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterest(1, FixedRateNegative, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_002] T+1固定负利率算头不算尾 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(负利息) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterest(1, FixedRateNegative, 0m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_003] T+1固定负利率算头不算尾 - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*(-1.05%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRateNegative, 0m, Principal)) + }; + var interest = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRateNegative); + var expected = ExpectedInterestWithPreEod(0, FixedRateNegative, 0m, Principal, ExpectedInterest(1, FixedRateNegative, 0m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_NEG_T1_004] T+1固定负利率算头不算尾 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=-1.05% + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_NEG_T1_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFixedUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRateNegative); + var expectedUnwind1 = ExpectedInterest(1, FixedRateNegative, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%,newCalcLast=true + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRateNegative, Principal * 0.5m, newCalcLast: true); + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRateNegative, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRateNegative, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:浮动利率算头不算尾 - 扩展维度 + // 维度:T+1/T+0 × 加减点 × 前一/当前营业日 × 单/复利 + // ================================================================ + + #region 场景8:浮动利率算头不算尾 - T+1浮动减点(当前营业日,复利) + + private const decimal FloatMinusRate = -0.021m; // 浮动减点固定端-2.10%(Excel场景) + private const decimal FloatPlusRate = 0.0025m; // 浮动加点固定端+0.25%(Excel场景) + + /// + /// [FLT_MINUS_T1_CUR_002] T+1浮动减点算头不算尾(当前营业日) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=0, FixedRate=-2.10%, InterestType=复利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(固定-2.10%+浮动0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_003] T+1浮动减点算头不算尾(当前营业日) - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_004] T+1浮动减点算头不算尾(当前营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_004() + { + // 第一步:4/29部分平仓50% + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 第二步:5/6全平剩余50%,复利从头算 + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:8天, 每3天重置, [28-30]@-2.0%, [1-5]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 8; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景10:浮动利率算头不算尾 - T+1浮动减点(前一营业日,复利) + + /// + /// [FLT_MINUS_T1_PRE_002] T+1浮动减点算头不算尾(前一营业日) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=复利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(-2.10%+0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_003] T+1浮动减点算头不算尾(前一营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_004] T+1浮动减点算头不算尾(前一营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 复利从头算:8天 [28-30]@-2.0% + [1-5]@-1.9% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_004() + { + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:8天, 每3天重置, [28-30]@-2.0%, [1-5]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 8; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景12:浮动利率算头不算尾 - T+1浮动减点(前一营业日,单利) + + /// + /// [FLT_MINUS_T1_PRE_SI_002] T+1浮动减点算头不算尾(前一营业日,单利) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 业务场景2:收盘后次日全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:总利息=1天(-2.10%+0.10%=-2.00%) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 1m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_003] T+1浮动减点算头不算尾(前一营业日,单利) - 部分平仓 + /// --------------------------------------------------------------- + /// 业务场景3:部分平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*1*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_004] T+1浮动减点算头不算尾(前一营业日,单利) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 业务场景4:部分平仓一次后,经过数日再全部平仓 + /// 参数:interest_rule=-1, FixedRate=-2.10%, InterestType=单利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_004() + { + var unwind1 = CalcFloatUnwind(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expectedUnwind1 = ExpectedInterest(1, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利, Principal * 0.5m); + // 单利: 6天(EOD后), [30]@0.001 + [1-5]@0.002 → 1@-2.0% + 5@-1.9% + var raw = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m) + + Principal * 0.5m * (FloatMinusRate + 0.001m) * 1 / AnnualDays + + Principal * 0.5m * (FloatMinusRate + 0.002m) * 5 / AnnualDays; + var expectedTotal = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景14:固定利率算头不算尾 - 收盘归档场景 + + /// + /// [FIX_EOD_001] 固定利率算头不算尾 - 首日收盘归档 + /// --------------------------------------------------------------- + /// 场景:4/28执行收盘EOD归档(首次收盘) + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 期望:当日收盘利息=1天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_001() + { + var interest = CalcFixedEod(new DateTime(2026, 4, 28), new List(), + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_EOD_002] 固定利率算头不算尾 - 连续收盘 + /// --------------------------------------------------------------- + /// 场景:4/28和4/29连续两个工作日收盘归档 + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 期望:4/28和4/29收盘利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_002() + { + var eod1 = CalcFixedEod(new DateTime(2026, 4, 28), new List(), + InterestRule_Pre, FixedRatePositive); + var expected1 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1, eod1.InterestAmount); + + var eod2 = CalcFixedEod(new DateTime(2026, 4, 29), new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, expected1) + }, InterestRule_Pre, FixedRatePositive); + var expected2 = ExpectedInterest(1, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected1+expected2, eod2.InterestAmount); + } + + /// + /// [FIX_EOD_003] 固定利率算头不算尾 - 到期日收盘不算尾 + /// --------------------------------------------------------------- + /// 场景:4/28起息,2027-04-27到期 + /// 参数:FixedRate=0.75%, interest_rule=-1 + /// 操作:2027-04-27执行收盘归档 + /// 期望:到期日收盘利息=0(不算尾) + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_EOD_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2027, 4, 26), Principal, 0m, 10m) + }; + var interest = CalcFixedEod(new DateTime(2027, 4, 27), eodPositions, + InterestRule_Pre, FixedRatePositive); + AssertInterestEqual(0m, interest.InterestAmount); + } + + #endregion + + // ================================================================ + // Excel测试文件场景:算头算尾(InterestCalcMode="11") + // 这些场景在Excel中标记为"通过",同样需要单元测试覆盖 + // 口径说明:"11"=算头算尾(含起息日和到期日/操作日) + // 与算头不算尾("10")的关键区别: + // - "10":计息区间 S=startDate, E=valueDate-1 → days天 + // - "11":计息区间 S=startDate, E=valueDate → days+1天 + // ================================================================ + + #region 场景A:固定利率算头算尾 - T+1固定正利率(前一营业日,正利率0.75%) + + /// + /// [FIX_POS_T1_11_001] 算头算尾 T+1固定正利率 - 未收盘平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28起息,4/29盘中全平 + /// 算头算尾:S=4/28, E=4/29 → 2天 + /// 期望:利息=2*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_001() + { + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_002] 算头算尾 T+1固定正利率 - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中全平 + /// 算头算尾:历史1天+当期1天=2天 + /// 期望:总利息=2天 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m, + ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_003] 算头算尾 T+1固定正利率 - 部分平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 算头算尾:历史1天+当期1天=2天×50% + /// 期望:利息=0.5*2*0.75%*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0m,ExpectedInterest(1, FixedRatePositive, 0m, Principal)) + }; + var interest = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FixedRatePositive); + var expected = ExpectedInterest(2, FixedRatePositive, 0m, Principal*0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FIX_POS_T1_11_004] 算头算尾 T+1固定正利率 - 部分平仓后经过数日再全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=0.75% + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// 算头算尾:4/29半平=2天×50%;5/6全平剩余=8天×50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FIX_POS_T1_11_004() + { + // 4/29部分平仓50%(算头算尾→2天) + var unwind1 = CalcFixedUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FixedRatePositive); + var expectedUnwind1 = ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + // 5/6全平剩余50%(EOD=4/29, 算头算尾→4/29~5/6=8天) + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m)) + }; + var unwind2 = CalcFixedUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FixedRatePositive, Principal * 0.5m, newCalcLast: true); + // 算头算尾: 4/29~5/6(算尾)=8天(newCalcLast=true无影响) + var expectedTotal = ExpectedInterestWithPreEod(7, FixedRatePositive, 0m, Principal * 0.5m, + ExpectedInterest(2, FixedRatePositive, 0m, Principal * 0.5m), 1m); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景C:浮动利率算头算尾 - T+1浮动减点(当前营业日,复利) + + /// + /// [FLT_MINUS_T1_CUR_11_002] 算头算尾 T+1浮动减点(当前营业日) - 收盘后次日全部平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_11_003] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_CUR_11_004] 算头算尾 T+1浮动减点(当前营业日) - 部分平仓后全平, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_CUR_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Cur, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:9天, 每3天重置, [28-30]@-2.0%, [1-3,4-6]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景E:浮动利率算头算尾 - T+1浮动减点(前一营业日,复利) + + /// + /// [FLT_MINUS_T1_PRE_11_002] 算头算尾 T+1浮动减点(前一营业日) - 收盘后次日全部平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_11_003] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓, 复利从头算 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 0.5m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_11_004] 算头算尾 T+1浮动减点(前一营业日) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 复利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.复利, Principal * 0.5m, newCalcLast: false); + // 复利从头算:9天, 每3天重置, [28-30]@-2.0%, [1-3,4-6]@-1.9% + var principal = Principal * 0.5m; + var rate1 = FloatMinusRate + 0.001m; + var rate2 = FloatMinusRate + 0.002m; + decimal interest = 0m, dynomic = principal; + for (int d = 0; d < 9; d++) + { + if (d % 3 == 0) dynomic = principal + interest; + interest += dynomic * (d < 3 ? rate1 : rate2) / AnnualDays; + } + var expectedTotal = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + #region 场景G:浮动利率算头算尾 - T+1浮动减点(单利) + + /// + /// [FLT_MINUS_T1_PRE_SI_11_002] 算头算尾 T+1浮动减点(单利) - 收盘后次日全部平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/28收盘归档;4/29盘中全平 + /// 期望:利息=2*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_002() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterest(1, FloatMinusRate, 0.001m, Principal)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_11_003] 算头算尾 T+1浮动减点(单利) - 部分平仓 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/28收盘归档;4/29盘中平仓50% + /// 期望:利息=0.5*2*(-2.10%+0.10%)*1000/365 + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_003() + { + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 28), Principal, 0.001m, + ExpectedInterestWithPreEod(0, FloatMinusRate, 0.001m, Principal, ExpectedInterest(1, FloatMinusRate, 0.001m, Principal), 1m)) + }; + var interest = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expected = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal*0.5m); + AssertInterestEqual(expected, interest.InterestAmount); + } + + /// + /// [FLT_MINUS_T1_PRE_SI_11_004] 算头算尾 T+1浮动减点(单利) - 部分平仓后全平 + /// --------------------------------------------------------------- + /// 参数:InterestCalcMode="11", interest_rule=-1, FixedRate=-2.10%, 单利 + /// 操作:4/29部分平仓50%;经过4/29收盘;5/6全部平仓剩余50% + /// --------------------------------------------------------------- + /// + [TestMethod] + public void UT_SWAP_INT_FLT_MINUS_T1_PRE_SI_11_004() + { + var unwind1 = CalcFloatUnwind11(new DateTime(2026, 4, 29), new DateTime(2026, 4, 29), 0.5m, + InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利); + var expectedUnwind1 = ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m); + AssertInterestEqual(expectedUnwind1, unwind1.InterestAmount); + + var eodPositions = new List + { + CreateEodPosition(new DateTime(2026, 4, 29), Principal * 0.5m, 0.001m, + ExpectedInterest(2, FloatMinusRate, 0.001m, Principal * 0.5m)) + }; + var unwind2 = CalcFloatUnwind11(new DateTime(2026, 5, 6), new DateTime(2026, 5, 6), 1m, + eodPositions, InterestRule_Pre, FloatMinusRate, InterestTypeEnum.单利, Principal * 0.5m); + // 单利: 9天, [28,29,30]@0.001 + [1-6]@0.002 → 1@-2.0% + 6@-1.9% + var raw = Principal * 0.5m * (FloatMinusRate + 0.001m) * 3 / AnnualDays + + Principal * 0.5m * (FloatMinusRate + 0.002m) * 6 / AnnualDays; + var expectedTotal = Math.Round(raw, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + AssertInterestEqual(expectedTotal, unwind2.InterestAmount); + } + + #endregion + + // ================================================================ + // 算头算尾("11")通用调用方法 + // ================================================================ + + #region 算头算尾("11")辅助方法 + + private swap_flow_event CalcFixedUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + return CalcFixedUnwind11(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, posiNotional, closeList, newCalcLast); + } + + private swap_flow_event CalcFixedUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule, decimal fixedRate, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("11", interestRule); + var position = CreateFixedInterestPosition(fixedRate, interestRule); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + private swap_flow_event CalcFloatUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, int interestRule, decimal fixedRate, + InterestTypeEnum interestType, decimal posiNotional = Principal, + List closeList = null, bool newCalcLast = false) + { + return CalcFloatUnwind11(valueDate, unwindDate, closePercent, + new List(), interestRule, fixedRate, interestType, posiNotional, closeList: closeList, newCalcLast: newCalcLast); + } + + private swap_flow_event CalcFloatUnwind11(DateTime valueDate, DateTime unwindDate, + decimal closePercent, List eodPositions, + int interestRule, decimal fixedRate, InterestTypeEnum interestType, + decimal posiNotional = Principal, List closeList = null, + bool newCalcLast = false) + { + var td = CreateTrade("11", interestRule); + var position = CreateFloatInterestPosition(interestRule, interestType, fixedRate); + + var interests = _service.GetInterests( + td, td.trade_extend, + valueDate, unwindDate, + eodPositions, + new List { position }, + posiNotional, posiNotional, posiNotional, posiNotional, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, posiNotional, false, settment: false, newCalcLast: newCalcLast, closeList: closeList); + + AssertInterestEqual(1, interests.Count); + return interests[0]; + } + + #endregion + + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 8772ad0c..fe89dd14 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -333,7 +333,7 @@ namespace YLErp.Modules.SwapModule var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice; var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成).ToList(); bool tdClose = closeList.Count > 0; - interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false, closeList); + interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false,false, closeList); return interests; } /// @@ -372,6 +372,7 @@ namespace YLErp.Modules.SwapModule decimal orginPv, bool add = false, bool settment = true, + bool newCalcLast= false, List closeList = null) { List interests = new List(); @@ -409,21 +410,35 @@ namespace YLErp.Modules.SwapModule else { // 盘中互换场景,使用 CalcUnwindInterest - interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast)); + interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast||newCalcLast)); } } - //当日有平仓或互换记录时,需要把平仓或互换已经结算的利息从计算结果中扣除,避免重复计算 + //当日有平仓或互换记录时,避免重复结算 if (closeList != null && closeList.Count > 0) { foreach (var item in interests) { var closeEvent = closeList.Where(x => x.PositionId == item.PositionId); - var closePnl = closeEvent.Sum(s => s.InterestClosePnL); - var closeAmount = closeEvent.Sum(s => s.InterestAmount); - var closeTdAmount = closeEvent.Sum(s => s.TdInterestAmount); - item.InterestAmount = item.InterestAmount - closeAmount; - item.TdInterestAmount = item.TdInterestAmount - closeTdAmount; - item.InterestClosePnL = item.InterestClosePnL - closePnl; + if (eventType == (int)SwapEventTypeEnum.互换 || eventType == (int)SwapEventTypeEnum.自动互换) + { + // 互换:该仓位当天已有完成事件,直接归0 + if (closeEvent.Any()) + { + item.InterestAmount = 0; + item.TdInterestAmount = 0; + item.InterestClosePnL = 0; + } + } + else if (!calcLast && !newCalcLast) + { + // 平仓不算尾:扣除已结算的利息(算尾时利息已包含关闭日,无重叠) + var closePnl = closeEvent.Sum(s => s.InterestClosePnL); + var closeAmount = closeEvent.Sum(s => s.InterestAmount); + var closeTdAmount = closeEvent.Sum(s => s.TdInterestAmount); + item.InterestAmount = item.InterestAmount - closeAmount; + item.TdInterestAmount = item.TdInterestAmount - closeTdAmount; + item.InterestClosePnL = item.InterestClosePnL - closePnl; + } } } return interests; @@ -656,11 +671,11 @@ namespace YLErp.Modules.SwapModule var floateRate = preEodPosition.FloatRate; if (position.InterestType == (int)InterestTypeEnum.复利) { - CalcDailyCompoundInterest(preEodPosition, endDate, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); + CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); } else { - CalcDailySimpleInterest(preEodPosition, endDate, position, closePosiNotionalValue, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); + CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); } interest.InterestAmount = Math.Round(InterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); @@ -684,26 +699,73 @@ namespace YLErp.Modules.SwapModule /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailyCompoundInterest( DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) { - // 复利:利息并入本金 - CalcDailyInterest(preEodPosition, endDate, position, principal, posiPrincipal, flowEvent, annualDays, needPrice, floateRate, closePercent, orginPv, compoundInterest: true, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); + var startDate = position.PosiStartDate; + decimal interestProfitSum = 0; + decimal TdInterestPrincipal = 0; + decimal interest = interestProfitSum ; + decimal tdinterest = interestProfitSum ; + int interestPeriod = position.interest_rest_days ?? 1; + // 复利:只能用要平仓的名义本金从头开始算 + decimal dynomicPrincipal = principal; + decimal tdDynomicPrincipal = dynomicPrincipal; + var calcDays = (endDate - startDate).Days; + double floatRate = Convert.ToDouble(floateRate); + for (int i = 0; i <= calcDays; i++) + { + var accrueDate = startDate.AddDays(i); + if (!calcFirst && accrueDate == startDate) continue; // 首日不算头 + if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾 + if (accrueDate >= startDate) + { + if (i % interestPeriod == 0) + { + // 复利时:利息并入本金 + dynomicPrincipal = principal + interest; + tdDynomicPrincipal = principal + interest; + // 获取新的浮动利率 + if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) + { + var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0)); + if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) + { + if (floatRate1 != 0) floatRate = floatRate1; + } + else + { + throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格"); + } + } + flowEvent.InterestPrincipal = tdDynomicPrincipal; + TdInterestPrincipal = tdDynomicPrincipal; + } + else + { + // 复利非重置日:利息不并入本金,不用closePercent缩放(principal已反映平仓比例) + flowEvent.InterestPrincipal = tdDynomicPrincipal; + TdInterestPrincipal = tdDynomicPrincipal; + } + flowEvent.FloatRate = Convert.ToDecimal(floatRate); + var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); + var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); + if (position.IsAnnualized) + { + interest1 /= annualDays; + tdinterest1 /= annualDays; + } + interest += interest1; + tdinterest += tdinterest1; + } + } + InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } /// /// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率) /// - public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) - { - // 单利:利息不并入本金 - CalcDailyInterest(preEodPosition, endDate, position, principal, posiPrincipal, flowEvent, annualDays, needPrice, floateRate, closePercent, orginPv, compoundInterest: false,calcFirst,calcLast, ref InterestAmount, ref TdInterestAmount); - } - - /// - /// 通用日度利息计算方法(单利/复利共用) - /// - /// 是否复利:true=利息并入本金,false=单利 - private void CalcDailyInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool compoundInterest, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) { var startDate = position.PosiStartDate; decimal interestProfitSum = preEodPosition.InterestProfitSum; @@ -711,35 +773,27 @@ namespace YLErp.Modules.SwapModule decimal interest = interestProfitSum * closePercent; decimal tdinterest = interestProfitSum * closePercent; int interestPeriod = position.interest_rest_days ?? 1; - decimal dynomicPrincipal = principal; - decimal tdDynomicPrincipal = posiPrincipal; + // 单利:可用上一日计息基数 + decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; + decimal tdDynomicPrincipal = dynomicPrincipal; var calcDays = (endDate - startDate).Days; double floatRate = Convert.ToDouble(floateRate); for (int i = 0; i <= calcDays; i++) { var accrueDate = startDate.AddDays(i); - if (!calcFirst && accrueDate == startDate) continue; // 首日不算头 - if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾 + if (!calcFirst && accrueDate == startDate) continue; // 首日不算头 + if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾 if (accrueDate > preEodPosition.ValueDate) { if (i % interestPeriod == 0) { - // 复利时:利息并入本金 - if (compoundInterest) - { - dynomicPrincipal = dynomicPrincipal + interest; - tdDynomicPrincipal = tdDynomicPrincipal + interest; - } // 获取新的浮动利率 if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) { var fr007RateDate = QdpCalendarHelper.GetNonHolidayDefore(accrueDate.AddDays(position.interest_rule ?? 0)); if (TryGetFloatRate(fr007RateDate, position.FloatRateUnderlyingCode, out double floatRate1)) { - if (floatRate1 != 0) - { - floatRate = floatRate1; - } + if (floatRate1 != 0) floatRate = floatRate1; } else { @@ -751,9 +805,9 @@ namespace YLErp.Modules.SwapModule } else { - flowEvent.InterestPrincipal = (preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv) * closePercent; + flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent; tdDynomicPrincipal = flowEvent.InterestPrincipal; - TdInterestPrincipal = (preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv); + TdInterestPrincipal = tdDynomicPrincipal; } flowEvent.FloatRate = Convert.ToDecimal(floatRate); var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 05114ea3..888e067f 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -62,7 +62,7 @@ namespace YLErp.Modules.SwapModule public void SwapPositionCompose(DateTime settleDate, DateTime preSettleDate, IEnumerable ClientIds) { var dateStr = settleDate.ToString("yyyy-MM-dd"); - Log.Info("SwapPositionCompose:"+"settleDate:" + settleDate+ " preSettleDate:"+ preSettleDate+ " ClientIds:"+JsonHelper.Serialize(ClientIds)); + Log.Info("SwapPositionCompose:" + "settleDate:" + settleDate + " preSettleDate:" + preSettleDate + " ClientIds:" + JsonHelper.Serialize(ClientIds)); var tradePredicate = PredicateBuilder.Create(n => n.ValidState != ConsGlobal.InValid && n.TradeType == "收益互换" && n.TradeDate <= settleDate @@ -221,32 +221,32 @@ namespace YLErp.Modules.SwapModule decimal orginPv) { Log.Info("===================处理利息腿归档===================="); - + // 添加详细的参数验证日志 Log.Info($"[DealInterests] 参数验证 - settleDate: {settleDate:yyyy-MM-dd}, td.id: {td?.id}, td.TradeNumber: {td?.TradeNumber}"); Log.Info($"[DealInterests] 参数验证 - interestList.Count: {interestList?.Count ?? 0}, eodPositions.Count: {eodPositions?.Count ?? 0}, todyEodPositions.Count: {todyEodPositions?.Count ?? 0}"); Log.Info($"[DealInterests] 参数验证 - flowEvents.Count: {flowEvents?.Count ?? 0}, autoInterests.Count: {autoInterests?.Count ?? 0}"); Log.Info($"[DealInterests] 参数验证 - posiLongNational: {posiLongNational}, posiShortNational: {posiShortNational}, closeNational: {closeNational}, grossPrice: {grossPrice}, orginPv: {orginPv}"); - + // 验证关键参数 if (td == null) { Log.Info("[DealInterests] 参数验证: td (trade) 为 null"); throw new ArgumentNullException(nameof(td), "交易对象不能为null"); } - + if (interestList == null) { Log.Info($"[DealInterests] 参数验证: interestList 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(interestList), "利息腿列表不能为null"); } - + if (flowEvents == null) { Log.Info($"[DealInterests] 参数验证: flowEvents 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(flowEvents), "流水事件列表不能为null"); } - + if (autoInterests == null) { Log.Info($"[DealInterests] 参数验证: autoInterests 为 null, td.id: {td.id}"); @@ -390,14 +390,14 @@ namespace YLErp.Modules.SwapModule } unwindData.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? 0); unwindData.PosiNotionalValue = StockEqvNotional; - + // 预付金腿类型列表:初始预付金、追加预付金 - var premiumModes = new List() {(int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; - + var premiumModes = new List() { (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 }; + // 分别计算预付金腿和利息腿的金额 var premiumInterests = autoInterests.Where(x => premiumModes.Contains(x.InterestMode)).ToList(); var interestLegs = autoInterests.Where(x => !premiumModes.Contains(x.InterestMode)).ToList(); - + // 预付金腿金额 decimal premiumTotal = 0; premiumInterests.ForEach(x => @@ -406,7 +406,7 @@ namespace YLErp.Modules.SwapModule premiumTotal += x.InterestClosePnL * ratio; }); unwindData.SwapMarginRebatePnl = premiumTotal; // 预付金腿金额 - + // 利息腿金额(总金额减去预付金腿金额) decimal interestTotal = 0; interestLegs.ForEach(x => @@ -415,10 +415,10 @@ namespace YLErp.Modules.SwapModule interestTotal += x.InterestClosePnL * ratio; }); unwindData.SwapCloseAmount = interestTotal; // 利息腿金额 - + // 总实现盈亏 unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount + unwindData.SwapMarginRebatePnl; - + SaveAutoSwapDeal(td, autoInterests, unwindData, interval); } /// @@ -432,7 +432,7 @@ namespace YLErp.Modules.SwapModule //td.UnWindDate = unwindData.ValueDate; //优先使用 interval.SettlementDate 作为资金记录发生日期,如果没有则使用 ValueDate var cashHappenDate = interval?.SettlementDate ?? unwindData.ValueDate; - + int clientCashId = 0; // 利息腿:插入资金记录(使用系统操作_互换) if (unwindData.SwapCloseAmount != 0) @@ -445,7 +445,7 @@ namespace YLErp.Modules.SwapModule { AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate); } - + string data = JsonConvert.SerializeObject(unwindData); var swapEvent = new SwapEventService(this).AddSwapEventDate(unwindData.ValueDate, unwindData.SwapTradeId, (int)SwapEventTypeEnum.自动互换, data, clientCashId, true, "系统操作-自动互换");//将互换总额存入事件 flowEvents.ForEach(x => @@ -731,7 +731,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 - newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum; + newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum; //累计已实现 newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; @@ -759,41 +759,41 @@ namespace YLErp.Modules.SwapModule private List SaveAutoEodInterestPosition(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, swap_position position, trade td, DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap, decimal posiLongNotional, decimal posiShortNational, decimal grossPrice, decimal orginPv) { Log.Info($"[SaveAutoEodInterestPosition] 开始执行 - valueDate: {valueDate:yyyy-MM-dd}, td.id: {td?.id}, position.id: {position?.id}"); - + // 详细的参数验证 if (td == null) { Log.Info("[SaveAutoEodInterestPosition] 参数验证: td (trade) 为 null"); throw new ArgumentNullException(nameof(td), "交易对象不能为null"); } - + if (position == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: position 为 null, td.id: {td.id}"); throw new ArgumentNullException(nameof(position), "持仓对象不能为null"); } - + if (interval == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: interval 为 null, td.id: {td.id}, position.id: {position.id}"); throw new ArgumentNullException(nameof(interval), "观察日信息不能为null"); } - + if (td.trade_extend == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend 为 null, td.id: {td.id}, td.TradeNumber: {td.TradeNumber}"); throw new ArgumentNullException("td.trade_extend", "交易扩展信息不能为null"); } - + Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); - + // 验证 ExtendObj if (td.trade_extend.ExtendObj == null) { Log.Info($"[SaveAutoEodInterestPosition] 参数验证: td.trade_extend.ExtendObj 为 null, td.id: {td.id}"); throw new ArgumentNullException("td.trade_extend.ExtendObj", "交易扩展对象不能为null"); } - + var tradeExtend = td.trade_extend.ExtendObj; decimal oriPosiNotionalValue = posiLongNotional + posiShortNational; decimal posiNotionalValue = oriPosiNotionalValue; @@ -902,20 +902,18 @@ namespace YLErp.Modules.SwapModule Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); var tradeExtend = td.trade_extend.ExtendObj; decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational; - decimal posiNotionalValue = oriPosiNotionalValue; + decimal posiNotionalValue = posiLongNotional + posiShortNational; decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负 if (marginTypes.Contains(position.InterestMode)) { ratio = -ratio; } - if (eodPayPosition == null) - { - eodPayPosition = new eod_swap_position(); - eodPayPosition.ClientId = td.ClientId; - eodPayPosition.SwapTradeId = td.id; - eodPayPosition.PosiStartDate = td.StartDate.Value; - eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; - } + var lastInterestIncomeSum = eodPayPosition.InterestIncomeSum; + eodPayPosition = new eod_swap_position(); + eodPayPosition.ClientId = td.ClientId; + eodPayPosition.SwapTradeId = td.id; + eodPayPosition.PosiStartDate = td.StartDate.Value; + eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value; if (newEodPayPosition == null) { newEodPayPosition = eodPayPosition.Clone(); @@ -926,6 +924,10 @@ namespace YLErp.Modules.SwapModule { orginPv = eodPayPosition.InterestPrincipalFix; } + else + { + orginPv = posiNotionalValue; + } decimal closePercent = oriPosiNotionalValue == 0 ? 0 : closeNational / oriPosiNotionalValue; var eventType = autoSwap ? (int)SwapEventTypeEnum.自动互换 : (int)SwapEventTypeEnum.平仓; bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString(); @@ -941,9 +943,9 @@ namespace YLErp.Modules.SwapModule positions.Add(position); List preEodPositions = new List(); preEodPositions.Add(eodPayPosition); - var interests = new SwapDealService(this).GetInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true); + var interests = new SwapDealService(this).GetInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: true); decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount); - + decimal InterestAmount = interests.Sum(x => x.InterestAmount); newEodPayPosition.ValueDate = valueDate; newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); @@ -964,7 +966,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.interest_rest_days = position.interest_rest_days; newEodPayPosition.interest_rule = position.interest_rule; //利息端估值用信息 - newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiLongNotional + posiShortNational; + newEodPayPosition.TdInterestPrincipal = interests.Count > 0 ? interests.First().InterestPrincipal : 0; if (interval != null) { newEodPayPosition.TdInterestRate = interval.Rate; @@ -977,8 +979,13 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; newEodPayPosition.TdCloseInterest = flowEvents.Sum(s => s.InterestClosePnL); - newEodPayPosition.TdInterestIncome = TdInterestAmount * (1 - closePercent); - Log.Info($"InterestIncomeSum is {eodPayPosition.InterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" + + var intersetAcmount = newEodPayPosition.TdInterestPrincipal*(newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); + if (position.IsAnnualized) + { + intersetAcmount /= tradeExtend.AnnualDays; + } + newEodPayPosition.TdInterestIncome = intersetAcmount; + Log.Info($"InterestIncomeSum is {lastInterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" + $",TdCloseInterest is {newEodPayPosition.TdCloseInterest}"); Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); @@ -988,7 +995,7 @@ namespace YLErp.Modules.SwapModule } else { - newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome - newEodPayPosition.TdCloseInterest * ratio; + newEodPayPosition.InterestIncomeSum = InterestAmount; } //持仓内容-利息腿-损益统计(本方视角) newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; @@ -1001,9 +1008,9 @@ namespace YLErp.Modules.SwapModule Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio ; + newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; - newEodPayPosition.RealizedPnl = newEodPayPosition.RealizedInterest + newEodPayPosition.RealizedInterestFee;; + newEodPayPosition.RealizedPnl = newEodPayPosition.RealizedInterest + newEodPayPosition.RealizedInterestFee; ; var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate , seekPreday: true, currencyRateType: position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell); newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); @@ -1040,7 +1047,7 @@ namespace YLErp.Modules.SwapModule eodPayPosition.ClientId = td.ClientId; eodPayPosition.SwapTradeId = td.id; //eodPayPosition.PositionId = position.id; 为了算利息时找不到给日期重新赋值 - + eodPayPosition.InterestMode = position.InterestMode; eodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; eodPayPosition.InterestRateDefault = position.InterestRateDefault; @@ -1123,7 +1130,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee; newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 - newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum; + newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum; //累计已实现 newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; @@ -2002,8 +2009,10 @@ namespace YLErp.Modules.SwapModule { var predicate = PredicateBuilder.Create(n => !n.Invalid && n.PosiQuantity > 0); var interestPredicate = PredicateBuilder.Create(n => !n.Invalid && n.InterestDirection > 0); - var tradePredicate = PredicateBuilder.Create(n => n.StructureType == req.StructureType - && n.ValidState != "InValid"); + var tradePredicate = PredicateBuilder.Create(n => n.ValidState != "InValid"); + // TODO 这里暂时忽略前端传的值 暂时使用临时方案:同时查普通债券类收益互换和普通收益互换 + tradePredicate = tradePredicate.And(n => n.StructureType == "普通债券类收益互换" || n.StructureType == "普通收益互换"); + if (req.ClientId > 0) { predicate = predicate.And(x => x.ClientId == req.ClientId); @@ -2055,7 +2064,7 @@ namespace YLErp.Modules.SwapModule { eventDate = QdpCalendarHelper.GetNonHoliday(eventDate.AddDays(tradeExtend.ExtendObj.SettlementRules)); } - item.DayCount = Math.Max(0,(eventDate - item.position.PosiStartDate).Days + 1); + item.DayCount = Math.Max(0, (eventDate - item.position.PosiStartDate).Days + 1); //item.position.PosiProfitSum += item.position.VTradingFee-item.position.PosiFeePending; SetClientEodPosition(item.position); //item.position.PosiProfitSum += item.TradingFee; diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs index 24ed1178..089a79fc 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs @@ -35,7 +35,7 @@ namespace YLErp.Modules.SwapModule public eod_commodity_future_price SearchTodayFRData(DateTime dateTime) { - var data = DbContext.eod_commodity_future_price.Where(a => a.ValueDate == dateTime).FirstOrDefault(); + var data = DbContext.eod_commodity_future_price.Where(a => a.ValueDate == dateTime && a.UnderlyingCode == "FR007").FirstOrDefault(); if (data == null) { data = new eod_commodity_future_price(); @@ -80,7 +80,7 @@ namespace YLErp.Modules.SwapModule public bool AddOrUpdateFRdata(Double price, DateTime dateTime) { string beforedate = ""; - var frdata = DbContext.eod_commodity_future_price.Where(a => a.ValueDate == dateTime).FirstOrDefault(); + var frdata = DbContext.eod_commodity_future_price.Where(a => a.ValueDate == dateTime && a.UnderlyingCode == "FR007").FirstOrDefault(); if (frdata == null) { frdata = new eod_commodity_future_price(); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs index af98e155..0e354bf0 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs @@ -1,4 +1,5 @@ -using YLErp.BLL; +using BaseOUDAL; +using YLErp.BLL; using YLErp.BLL.Eod; using YLErp.Model; using YLErp.Model.Enum; @@ -143,6 +144,7 @@ namespace YLErp.Modules.TradeModule.DealModule var count = tradeProessQuery.Count(); if (count == 0 || td.ProcessOrderId == ProcessTradeLog.审批通过)//投资规模校验已经将数据设置为已通过 { + req.comments = FormatCommentsWithRole(td.ProcessOrderId, req.comments); result.ChangeConfirmPath = SetTradeOpen(req, td); return result; } @@ -166,7 +168,8 @@ namespace YLErp.Modules.TradeModule.DealModule //} if (nextOrder==null) { - + + req.comments = FormatCommentsWithRole(td.ProcessOrderId, req.comments); result.ChangeConfirmPath = SetTradeOpen(req, td); return result; } @@ -175,6 +178,8 @@ namespace YLErp.Modules.TradeModule.DealModule // 继续审批流转 //----------------------------------------------- + var oldOrderId = td.ProcessOrderId; + var formattedComments = FormatCommentsWithRole(oldOrderId, req.comments); td.ProcessOrderId = nextOrder.order; //client.ProcessStatus = "审批中"; td.ProcessOptDate = OptDate; @@ -194,7 +199,12 @@ namespace YLErp.Modules.TradeModule.DealModule if (!req.notNeedOperationHistory) { - AddTradeOperationHistoryAndSetParentTradeInfo(false, td, req.ignoreMoneyCheck ? "交易特批-交易审批同意" : "交易审批同意", req.comments); + AddTradeOperationHistoryAndSetParentTradeInfo(false, td, req.ignoreMoneyCheck ? "交易特批-交易审批同意" : "交易审批同意", formattedComments); + } + + if (td.TradeType == "收益互换") + { + new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批通过, "", 0, true, formattedComments); } DbContext.SaveChanges(); @@ -278,6 +288,8 @@ namespace YLErp.Modules.TradeModule.DealModule /// private TradeOpenResult TradeReject(TradeOpenReqModel req, trade td) { + var oldOrderId = td.ProcessOrderId; + req.comments = FormatCommentsWithRole(oldOrderId, req.comments); td.ProcessOrderId = Convert.ToInt32(PStatusEnum.reject); td.ProcessOrderBranch = 0; //td.IsApproval = false; @@ -299,7 +311,7 @@ namespace YLErp.Modules.TradeModule.DealModule var swapFlowEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == td.id && x.EventType == swapTradeEventType && x.EventDate == swapEvent.ValueDate); DbContext.swap_flow_event.RemoveRange(swapFlowEvents); } - new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批拒绝, "", 0, true, "提前终止拒绝"); + new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批拒绝, "", 0, true, req.comments); } } else @@ -311,7 +323,7 @@ namespace YLErp.Modules.TradeModule.DealModule } if (td.TradeType == "收益互换") { - new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批拒绝, "", 0, true, "交易审批拒绝"); + new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批拒绝, "", 0, true, req.comments); } } td.ProcessStatus = ProcessTradeStatus.已拒绝.ToString(); @@ -515,7 +527,7 @@ namespace YLErp.Modules.TradeModule.DealModule if (td.TradeType=="收益互换") { SwapTradeService swapTradeService = new SwapTradeService(this); - new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批通过, "", 0, true); + new SwapEventService(UserInfo).AddSwapEventDate(DateTime.Now.Date, td.id, (int)SwapEventTypeEnum.审批通过, "", 0, true, req.comments); new TradeConfirmService(UserInfo).SwapTradeConfirm(td, "确认交易",false, optlog: "手工操作"); } //组合交易(非互换) @@ -727,6 +739,27 @@ namespace YLErp.Modules.TradeModule.DealModule throw; } } + + private string GetRoleNameByOrderId(int orderId) + { + var node = DbContext.approvalprocess + .FirstOrDefault(x => x.processType == "TradeProcess" && x.order == orderId); + if (node == null) + return null; + + return new ErpBaseContext().Roles + .Where(r => r.Id == node.roleId) + .Select(r => r.Name) + .FirstOrDefault(); + } + + private string FormatCommentsWithRole(int orderId, string comments) + { + var roleName = GetRoleNameByOrderId(orderId); + if (string.IsNullOrEmpty(roleName)) + return comments ?? string.Empty; + return $"【角色:{roleName}】{comments}"; + } } /// diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs index 18e0312b..fb20c420 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs @@ -8,8 +8,10 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule /// /// 国贸交易确认书编号生成器 /// 规则: - /// - 对客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{标的代码}-{当日第N笔对客交易(数字递增)},从001开始 - /// - 非对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{标的代码}-{当日第N笔非对客交易(字母递增)},从A开始 + /// - 对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{当日第N笔对客交易(数字递增)}-{标的代码},从01开始 + /// 例:GLMS-IS-20260424-01-250210IB + /// - 非对客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-{当日第N笔非对客交易使用字母递增}-{标的代码},从A开始 + /// 例:GLMS-IS-20260424-A-250210IB /// public class GuolianContractNoGenerator { @@ -27,15 +29,16 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule trade trade, string clientCode) { - // 根据trade的OpponentRole判断是否为对客交易 - var isClientTrade = trade.OpponentRole == "甲方"; - var underlyingCode = trade.UnderlyingCode ?? ""; + // 根据trade的OpponentRole判断是否为对客交易(乙方=对客) + var isClientTrade = trade.OpponentRole == "乙方"; + var underlyingCode = (trade.UnderlyingCode ?? "").Replace(".", ""); // 去掉标的代码中的点号 var tradeDate = trade.TradeDate ?? DateTime.MinValue; var tradeDateStr = tradeDate.ToString("yyyyMMdd"); - var prefix = $"GLMS-{clientCode}-{tradeDateStr}-{underlyingCode}-"; + var prefix = $"GLMS-{clientCode}-{tradeDateStr}-"; lock (_syncLock) { + // 幂等:如果已生成过确认书编号,直接返回 var existingForTrade = dbContext.trade_contract_r .Where(r => r.TradeId == trade.id && r.Type == ContractTypeEnum.Trade && r.IsValid) .Select(r => r.ContractCode) @@ -46,111 +49,27 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule return existingForTrade; } - var existingCodes = dbContext.trade_contract_r - .Where(d => d.ContractCode.StartsWith(prefix) && d.Type == ContractTypeEnum.Trade && d.IsValid) - .Select(d => d.ContractCode) - .ToList(); - - var existingCodeSet = new HashSet(); - for (var i = 0; i < existingCodes.Count; i++) - { - var code = existingCodes[i]; - if (!string.IsNullOrWhiteSpace(code)) - { - existingCodeSet.Add(code); - } - } + // 通过当天同客户同类交易的 id 排序确定序号(幂等),每个客户独立递增 + var tradeDateStart = tradeDate.Date; + var sameDayTradesCount = dbContext.trade + .Count(t => t.TradeDate >= tradeDateStart + && t.TradeDate < tradeDateStart.AddDays(1) + && t.ClientId == trade.ClientId + && t.OpponentRole == trade.OpponentRole + && t.id < trade.id); + var sequenceNo = sameDayTradesCount + 1; if (isClientTrade) { - var maxNo = 0; - var prefixLen = prefix.Length; - for (var i = 0; i < existingCodes.Count; i++) - { - var code = existingCodes[i]; - if (string.IsNullOrEmpty(code) || code.Length <= prefixLen) - { - continue; - } - - var suffix = code.Substring(prefixLen); - if (int.TryParse(suffix, out var n) && n > maxNo) - { - maxNo = n; - } - } - - var nextNo = maxNo + 1; - var contractNo = $"{prefix}{nextNo:D3}"; - while (existingCodeSet.Contains(contractNo)) - { - nextNo++; - contractNo = $"{prefix}{nextNo:D3}"; - } - return contractNo; + return $"{prefix}{sequenceNo:D2}-{underlyingCode}"; } else { - var maxNo = 0; - var prefixLen = prefix.Length; - for (var i = 0; i < existingCodes.Count; i++) - { - var code = existingCodes[i]; - if (string.IsNullOrEmpty(code) || code.Length <= prefixLen) - { - continue; - } - - var suffix = code.Substring(prefixLen); - if (!TryParseLettersToNumber(suffix, out var n)) - { - continue; - } - - if (n > maxNo) - { - maxNo = n; - } - } - - var nextNo = maxNo + 1; - var contractNo = $"{prefix}{NumberToLetter(nextNo)}"; - while (existingCodeSet.Contains(contractNo)) - { - nextNo++; - contractNo = $"{prefix}{NumberToLetter(nextNo)}"; - } - return contractNo; + return $"{prefix}{NumberToLetter(sequenceNo)}-{underlyingCode}"; } } } - private static bool TryParseLettersToNumber(string letters, out int number) - { - number = 0; - if (string.IsNullOrEmpty(letters)) - { - return false; - } - - for (var i = 0; i < letters.Length; i++) - { - var c = letters[i]; - if (c >= 'a' && c <= 'z') - { - c = (char)(c - 32); - } - else if (c < 'A' || c > 'Z') - { - number = 0; - return false; - } - - number = (number * 26) + (c - 'A' + 1); - } - return true; - } - /// /// 将数字转换为字母序列(1=A, 2=B, 26=Z, 27=AA, 28=AB...) /// diff --git a/YLErpDAL/Modules/UnderlyingModule/UnderlyingHelper.cs b/YLErpDAL/Modules/UnderlyingModule/UnderlyingHelper.cs index be030982..52c55e39 100644 --- a/YLErpDAL/Modules/UnderlyingModule/UnderlyingHelper.cs +++ b/YLErpDAL/Modules/UnderlyingModule/UnderlyingHelper.cs @@ -194,6 +194,27 @@ namespace YLErp.Modules.UnderlyingModule } } + /// + /// 清除指定标的的保证金率缓存 + /// + /// 标的代码,为空时清除全部 + public static void ClearMarginRateCache(string underlyingCode = null) + { + try + { + if (ylCache == null) return; + + var cachePattern = string.IsNullOrEmpty(underlyingCode) + ? "ClientMarginRate:*" + : $"ClientMarginRate:*:{underlyingCode}:*"; + ylCache.BatchDelete(cachePattern); + } + catch (Exception ex) + { + LogFactory.GetLogger("UnderlyingHelper").Error($"清除保证金率缓存时发生异常: {ex.Message}", ex); + } + } + /// /// 获取篮子标的品种对象 /// diff --git a/YLErpWeb/Controllers/tradeController.cs b/YLErpWeb/Controllers/tradeController.cs index 0624d5ad..428a4c66 100644 --- a/YLErpWeb/Controllers/tradeController.cs +++ b/YLErpWeb/Controllers/tradeController.cs @@ -2108,16 +2108,16 @@ namespace YLErp.Web.Controllers OptName = n.OptName, OptDate = n.OptDate }).ToArray(); - datas = datas.GroupBy(O => new { O.OptDate, O.OptName, O.OptType }).Select(O => O.First()).OrderByDescending(n => n.id).ToArray(); + datas = datas.OrderByDescending(n => n.id).ToArray(); foreach (var item in datas) { - if (IsExchange) + if (IsExchange && item.OptType == "批量调整-标的交易") { - item.Changes = item.OptType == "批量调整-标的交易" ? new DatatraceQueryService(CurUser).ReplaceDataChanges(new trade(), item.Changes) : ""; + item.Changes = new DatatraceQueryService(CurUser).ReplaceDataChanges(new trade(), item.Changes); } - else + else if (!IsExchange && item.OptType == "修改交易") { - item.Changes = item.OptType == "修改交易" ? new DatatraceQueryService(CurUser).ReplaceDataChanges(new trade(), item.Changes) : ""; + item.Changes = new DatatraceQueryService(CurUser).ReplaceDataChanges(new trade(), item.Changes); } if (!string.IsNullOrEmpty(item.Changes)) { diff --git a/YLErpWeb/Controllers/underlying_managerController.cs b/YLErpWeb/Controllers/underlying_managerController.cs index 3c3585bc..85e08a03 100644 --- a/YLErpWeb/Controllers/underlying_managerController.cs +++ b/YLErpWeb/Controllers/underlying_managerController.cs @@ -477,6 +477,7 @@ namespace YLErp.Web.Controllers model.ExJson = JsonHelper.Serialize(model.Bond); } var r = new UnderlyingDalService(CurUser).SaveUnderlyingData(model); + YLErp.Modules.UnderlyingModule.UnderlyingHelper.ClearMarginRateCache(model.UnderlyingCode); return JsonSuccess("更新成功", r); } diff --git a/YLErpWeb/Views/clientbalance/TradeMarketDescSet.cshtml b/YLErpWeb/Views/clientbalance/TradeMarketDescSet.cshtml index 48f33151..9bda37b7 100644 --- a/YLErpWeb/Views/clientbalance/TradeMarketDescSet.cshtml +++ b/YLErpWeb/Views/clientbalance/TradeMarketDescSet.cshtml @@ -100,17 +100,17 @@
@Html.MyAceDropdownInput2("setTemplate", "模板名:  ", YLErp.Web.Controllers.clientbalanceController.GetAllClientBalanceTemplate(), false, "", true, new { onchange = "changeTemplate()" })
-

发件人:

- + @*

发件人:

*@ + @* *@ @if (!PS.Config.Is申万) {