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 posiLongNotionalValue, decimal posiShortNotionalValue, decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice, decimal grossPrice, 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, posiLongNotionalValue, posiShortNotionalValue, closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice, grossPrice, 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, 0m, closeNotional, 1m, (int)SwapEventTypeEnum.平仓, false, false, 0m, 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 } }