using System; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using YLErp; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule; namespace UnitTestProject.Modules.SwapModule { /// /// 回归保护测试:业务场景1(固定利率收盘平仓)+ 业务场景2(浮动利率第一重置期内平仓) /// -------------------------------------------------------------------------- /// 数据来源:缺陷测试-利息20260807晚.xlsx /// - 业务场景1-浮动(8 用例):收盘到 4/2 利息=0,Excel 标记"无关",此处验证返回 0 /// - 业务场景1-固定(12 用例):固定利率单利,平仓日 5/28 或 4/6,Excel 全标"通过" /// - 业务场景2(12 用例):浮动利率第一重置期内平仓(4/27),Excel 全标"通过" /// /// 目的:确保场景3/4 的 Bug 修复不回归破坏已通过的用例。 /// 断言容差 0.01(匹配 Excel 2 位小数精度)。 /// [TestClass] public class SwapInterestScenario1And2Test { #region 内部 Stub(与 Scenario3And4 相同结构) 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; } public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m; } private sealed class StubEodPositionService : TestableSwapEodPositionService { private readonly IReadOnlyDictionary _floatRates; public StubEodPositionService(IReadOnlyDictionary floatRates) : base(nameof(SwapInterestScenario1And2Test)) { _floatRates = floatRates; } protected override List CalcSwapInterests( trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate, List eodPositions, List positions, decimal posiNotionalValue, decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false, List closeList = null) { var svc = new StubSwapDealService( new OptUserInfo(0, nameof(SwapInterestScenario1And2Test), OptUserFrom.UnitTest), _floatRates); return svc.GetInterests(td, tradeExtend, valueDate, unwindDate, eodPositions, positions, posiNotionalValue, closePosiNotionalValue, closePrecent, eventType, tdClose, orginPv, add, settment, newCalcLast, closeList); } public eod_swap_position ExecuteClose(trade td, swap_position position, DateTime valueDate, decimal posiLongNotional, decimal posiShortNotional, List flowEvents, decimal closeNotional, eod_swap_position prevEod) { SaveAutoEodWithCloseInterestPosition(prevEod, null, position, td, valueDate, null, posiLongNotional + posiShortNotional, flowEvents, closeNotional, false, 1m, posiLongNotional + posiShortNotional); return PersistedPositions.LastOrDefault(); } } #endregion #region 常量 private const int AnnualDays = 365; private const int ResetPeriod = 7; private const decimal Notional = 303139117.8m; private const decimal FixedNotional = 10012350m; private static void AssertStrict(decimal expected, decimal actual, string tag) { var diff = Math.Abs(expected - actual); Assert.IsTrue(diff <= 0.01m, $"{tag}: Expected={expected}, Actual={actual}, Diff={expected - actual}"); } private StubEodPositionService _eod; private IReadOnlyDictionary _floatRates; [TestInitialize] public void Init() { _floatRates = new Dictionary { [new DateTime(2026, 4, 1)] = 0.0142, [new DateTime(2026, 4, 2)] = 0.014, [new DateTime(2026, 4, 3)] = 0.0135, [new DateTime(2026, 4, 4)] = 0.0135, [new DateTime(2026, 4, 6)] = 0.0135, [new DateTime(2026, 4, 7)] = 0.0134, [new DateTime(2026, 4, 8)] = 0.0133, [new DateTime(2026, 4, 9)] = 0.0133, [new DateTime(2026, 4, 10)] = 0.0134, [new DateTime(2026, 4, 13)] = 0.0136, [new DateTime(2026, 4, 14)] = 0.0137, [new DateTime(2026, 4, 15)] = 0.0136, [new DateTime(2026, 4, 16)] = 0.0133, [new DateTime(2026, 4, 17)] = 0.0131, [new DateTime(2026, 4, 20)] = 0.0132, [new DateTime(2026, 4, 21)] = 0.0132, [new DateTime(2026, 4, 22)] = 0.0132, [new DateTime(2026, 4, 23)] = 0.0132, [new DateTime(2026, 4, 24)] = 0.0131, [new DateTime(2026, 4, 27)] = 0.013502, [new DateTime(2026, 4, 28)] = 0.0136, [new DateTime(2026, 4, 29)] = 0.0138, [new DateTime(2026, 4, 30)] = 0.0139, [new DateTime(2026, 5, 4)] = 0.0139, [new DateTime(2026, 5, 5)] = 0.0139, [new DateTime(2026, 5, 6)] = 0.0136, [new DateTime(2026, 5, 7)] = 0.0136, [new DateTime(2026, 5, 8)] = 0.0135, [new DateTime(2026, 5, 9)] = 0.0131, [new DateTime(2026, 5, 11)] = 0.0134, [new DateTime(2026, 5, 12)] = 0.013, [new DateTime(2026, 5, 13)] = 0.0129, [new DateTime(2026, 5, 14)] = 0.013, [new DateTime(2026, 5, 15)] = 0.013, [new DateTime(2026, 5, 18)] = 0.0132, [new DateTime(2026, 5, 19)] = 0.0131, [new DateTime(2026, 5, 20)] = 0.0132, [new DateTime(2026, 5, 21)] = 0.013131, [new DateTime(2026, 5, 22)] = 0.0135, [new DateTime(2026, 5, 25)] = 0.0139, [new DateTime(2026, 5, 26)] = 0.013727, [new DateTime(2026, 5, 27)] = 0.013639, [new DateTime(2026, 5, 28)] = 0.0135, }; _eod = new StubEodPositionService(_floatRates); } #endregion #region 构造器 private static trade CreateTrade(string interestCalcMode, int interestRule, DateTime startDate, DateTime maturity, string tradeNo = "UT-SCEN-1-2") { var extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = interestCalcMode, SettlementRules = interestRule }) }; return new trade { id = 1, TradeNumber = tradeNo, ClientId = 999998, TradeType = "收益互换", TradeDate = startDate, StartDate = startDate, ExerciseDate = maturity, TradeStatus = "确认成交", ValidState = "Valid", trade_extend = extend }; } private static swap_position CreatePosition(decimal spread, int interestRule, InterestTypeEnum interestType, DateTime startDate, DateTime maturity, int interestMode, decimal notional, bool isFixed = false) { var intervalModels = new List { new IntervalModel { Date = maturity, Rate = spread, Settlement = 0 } }; return new swap_position { id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = interestMode, InterestRateDefault = spread, InterestPrincipalFix = notional, PosiStartDate = startDate, PosiMatuirityDate = maturity, IsInitial = true, Invalid = false, InterestType = (int)interestType, IsAnnualized = true, interest_rest_days = isFixed ? 1 : ResetPeriod, interest_rule = interestRule, FloatRateUnderlyingCode = isFixed ? null : "FR007", InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } private swap_flow_event CalcCloseFlow(trade td, swap_position position, DateTime valueDate, List prevEod, decimal closeNotional) { var svc = new StubSwapDealService( new OptUserInfo(0, nameof(SwapInterestScenario1And2Test), OptUserFrom.UnitTest), _floatRates); var isMaturity = valueDate == td.ExerciseDate; var interests = svc.GetInterests( td, td.trade_extend, valueDate, valueDate, prevEod, new List { position }, closeNotional, closeNotional, 1m, (int)SwapEventTypeEnum.平仓, false, closeNotional, false, settment: false, newCalcLast: isMaturity); Assert.AreEqual(1, interests.Count); return interests[0]; } #endregion #region 业务场景1-浮动:收盘到4/2,利息=0(8用例,Excel标"无关") // 浮动利率交易,收盘到 4/2 经过 0 天,全部平仓返还利息 = 0 [DataTestMethod] [DataRow("T+1浮动减点算头算尾(当前营业日)", 0, 2, "-0.021")] [DataRow("T+0浮动加点算头算尾(当前营业日)", 0, 2, "0.0025")] [DataRow("T+1浮动减点算头不算尾(当前营业日)", 0, 9, "-0.021")] [DataRow("T+0浮动加点算头不算尾(当前营业日)", 0, 2, "0.0025")] [DataRow("T+1浮动减点算头算尾", -1, 9, "-0.021")] [DataRow("T+0浮动加点算头算尾", -1, 2, "0.0025")] [DataRow("T+1浮动减点算头不算尾", -1, 9, "-0.021")] [DataRow("T+0浮动加点算头不算尾", -1, 9, "0.0025")] public void 场景1_浮动_收盘到4月2日_利息应为0(string note, int rule, int interestMode, string spreadStr) { var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture); var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22); var td = CreateTrade("11", rule, startDate, new DateTime(2026, 5, 19)); var position = CreatePosition(spread, rule, InterestTypeEnum.复利, startDate, new DateTime(2026, 5, 19), interestMode, Notional); var flow = CalcCloseFlow(td, position, new DateTime(2026, 4, 2), new List(), Notional); var eod = _eod.ExecuteClose(td, position, new DateTime(2026, 4, 2), 0m, 0m, new List { flow }, Notional, null); // Excel 标记"无关",利息应为 0(T+1 起算日 4/22 > 4/2,0 天计息) AssertStrict(0m, eod.TdCloseInterest, "场景1-浮动 " + note); } #endregion #region 业务场景1-固定:固定利率单利,平仓日5/28(12用例,Excel全标"通过") // 固定利率单利,Notional=10012350,到期日6/12 // 利率准则列(利率准则)为空时固定利率不使用FR007, interest_rule不影响固定利率 // interestMode: 2=合约名义本金规模, 9=标的期初全价 // 平仓日 = 5/28 (53天 for T+1, 53天 for T+0), 4/6 平仓日变体 = 1天/2天 [DataTestMethod] // 平仓日 4/6(T+1=closeDate4/6=1天, T+0=closeDate4/7=2天) // T+1: tradeDate=4/3, startDate=4/6, closeDate=4/6 // T+0: tradeDate=4/6, startDate=4/6, closeDate=4/7 [DataRow("T+1固定正利率算头算尾_4月6日", true, true, 2, "0.0075", "205.73", "0406")] [DataRow("T+0固定正利率算头算尾_4月6日", true, true, 2, "0.0075", "411.47", "0406")] [DataRow("T+1固定负利率算头不算尾_4月6日", true, false, 9, "-0.0075", "0", "0406")] [DataRow("T+1固定正利率算头不算尾_4月6日", true, false, 9, "0.0075", "0", "0406")] [DataRow("T+0固定负利率算头不算尾_4月6日", true, false, 2, "-0.0075", "-205.73", "0406")] [DataRow("T+0固定正利率算头不算尾_4月6日", true, false, 2, "0.0075", "205.73", "0406")] // 平仓日 5/28(T+1=53天, T+0=53天) [DataRow("T+1固定正利率算头算尾_5月28日", true, true, 2, "0.0075", "10903.86", "0528")] [DataRow("T+0固定正利率算头算尾_5月28日", true, true, 2, "0.0075", "10903.86", "0528")] [DataRow("T+1固定负利率算头不算尾_5月28日", true, false, 9, "-0.0075", "-10698.13", "0528")] [DataRow("T+1固定正利率算头不算尾_5月28日", true, false, 9, "0.0075", "10698.13", "0528")] [DataRow("T+0固定负利率算头不算尾_5月28日", true, false, 2, "-0.0075", "-10698.13", "0528")] [DataRow("T+0固定正利率算头不算尾_5月28日", true, false, 2, "0.0075", "10698.13", "0528")] public void 场景1_固定利率单利平仓(string note, bool calcFirst, bool calcLast, int interestMode, string spreadStr, string oracleStr, string dateGroup) { var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture); var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture); var mode = (calcFirst && calcLast) ? "11" : "10"; // 固定利率交易:起算日4/6, 到期日6/12 var startDate = new DateTime(2026, 4, 6); var maturity = new DateTime(2026, 6, 12); var td = CreateTrade(mode, 0, startDate, maturity, "UT-SCEN-1-FIX"); var position = CreatePosition(spread, 0, InterestTypeEnum.单利, startDate, maturity, interestMode, FixedNotional, isFixed: true); // T+1: closeDate=4/6 (同起算日); T+0: closeDate=4/7 (起算日+1) // 5/28变体: closeDate=5/28 DateTime closeDate; if (dateGroup == "0528") closeDate = new DateTime(2026, 5, 28); else // 0406 closeDate = note.StartsWith("T+1") ? new DateTime(2026, 4, 6) : new DateTime(2026, 4, 7); var flow = CalcCloseFlow(td, position, closeDate, new List(), FixedNotional); var eod = _eod.ExecuteClose(td, position, closeDate, 0m, 0m, new List { flow }, FixedNotional, null); AssertStrict(oracle, eod.TdCloseInterest, "场景1-固定 " + note); } #endregion #region 业务场景2:浮动利率第一重置期内平仓(4/27)(12用例,Excel全标"通过") // 浮动利率,平仓日 4/27(第一重置期内),Notional=303139117.8 [DataTestMethod] [DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-38868.25")] [DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "91273.94")] [DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-32390.21")] [DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "78234.81")] [DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-38868.25")] [DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "91273.94")] [DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-32390.21")] [DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "78234.81")] [DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-38868.25")] [DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "91273.94")] [DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-32390.21")] [DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "78234.81")] public void 场景2_第一重置期内平仓(string note, bool compound, bool calcFirst, bool calcLast, int rule, int interestMode, string spreadStr, string oracleStr) { var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture); var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture); var mode = (calcFirst && calcLast) ? "11" : "10"; var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利; var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22); var td = CreateTrade(mode, rule, startDate, new DateTime(2026, 5, 19)); var position = CreatePosition(spread, rule, type, startDate, new DateTime(2026, 5, 19), interestMode, Notional); // 平仓日 4/27(第一重置期内,非到期日) var closeDate = new DateTime(2026, 4, 27); var flow = CalcCloseFlow(td, position, closeDate, new List(), Notional); var eod = _eod.ExecuteClose(td, position, closeDate, 0m, 0m, new List { flow }, Notional, null); AssertStrict(oracle, eod.TdCloseInterest, "场景2 " + note); } #endregion } }