using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// SwapEodPositionService.NormalizeInterestSignForReport 的回归测试。 /// ----------------------------------------------------------------- /// 守卫提交 f4ffe710 中报表分支的符号归一化逻辑(原内联于 SearchEodSwapList)。 /// /// 业务口径: /// - 仅当 InterestDirection > 0 时执行(兼容历史 0 方向脏数据) /// - 普通利息腿:收取为正、支付为负(interestRatio = Direction==收取 ? 1 : -1) /// - 预付金腿(初始预付金/追加预付金):利息方向与保证金本金方向相反,interestRatio 取反 /// - TdCloseInterest / RealizedInterest 统一按 Math.Abs × interestRatio 重写 /// - RealizedPnl 重算为 RealizedInterest + RealizedInterestFee(兼容历史未同步落库) /// /// 抽为 public static 纯函数以支持无库单测。 /// [TestClass] public class SwapReportInterestSignNormalizeTest { // ================================================================ // 场景1:普通利息腿收取方向 → 利息维持正号 // TdCloseInterest=-1000(历史脏数据符号错) → 归一化为 +1000 // RealizedInterest=-2000 → +2000 // RealizedPnl = 2000 + 100 = 2100 // ================================================================ [TestMethod] public void 普通利息腿_收取方向_利息归一化为正() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.收取, tdCloseInterest: -1000m, realizedInterest: -2000m, realizedInterestFee: 100m); SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(1000m, pos.TdCloseInterest, 0.0001m, "TdCloseInterest 应归一化为 +1000"); Assert.AreEqual(2000m, pos.RealizedInterest, 0.0001m, "RealizedInterest 应归一化为 +2000"); Assert.AreEqual(2100m, pos.RealizedPnl, 0.0001m, "RealizedPnl = 2000 + 100 = 2100"); } // ================================================================ // 场景2:普通利息腿支付方向 → 利息归一化为负 // TdCloseInterest=1000(历史脏数据符号错) → 归一化为 -1000 // RealizedInterest=2000 → -2000 // RealizedPnl = -2000 + 100 = -1900 // ================================================================ [TestMethod] public void 普通利息腿_支付方向_利息归一化为负() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.支付, tdCloseInterest: 1000m, realizedInterest: 2000m, realizedInterestFee: 100m); SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(-1000m, pos.TdCloseInterest, 0.0001m, "TdCloseInterest 应归一化为 -1000"); Assert.AreEqual(-2000m, pos.RealizedInterest, 0.0001m, "RealizedInterest 应归一化为 -2000"); Assert.AreEqual(-1900m, pos.RealizedPnl, 0.0001m, "RealizedPnl = -2000 + 100 = -1900"); } // ================================================================ // 场景3:预付金腿收取方向 → 利息方向反向为负 // 原因:预付金腿的利息方向与保证金本金方向相反 // TdCloseInterest=1000 → -1000 // RealizedInterest=2000 → -2000 // ================================================================ [TestMethod] public void 预付金腿_收取方向_利息反向为负() { var pos = NewPosition( interestMode: (int)InterestModeEnum.初始预付金, interestDirection: (int)SwapDirectionEnum.收取, tdCloseInterest: 1000m, realizedInterest: 2000m, realizedInterestFee: 0m); SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(-1000m, pos.TdCloseInterest, 0.0001m, "预付金腿收取方向:利息反向为负"); Assert.AreEqual(-2000m, pos.RealizedInterest, 0.0001m, "预付金腿收取方向:累计利息反向为负"); Assert.AreEqual(-2000m, pos.RealizedPnl, 0.0001m, "RealizedPnl = -2000 + 0 = -2000"); } // ================================================================ // 场景4:预付金腿支付方向 → 利息方向反向为正 // ================================================================ [TestMethod] public void 预付金腿_支付方向_利息反向为正() { var pos = NewPosition( interestMode: (int)InterestModeEnum.追加预付金, interestDirection: (int)SwapDirectionEnum.支付, tdCloseInterest: -1000m, realizedInterest: -2000m, realizedInterestFee: 50m); SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(1000m, pos.TdCloseInterest, 0.0001m, "预付金腿支付方向:利息反向为正"); Assert.AreEqual(2000m, pos.RealizedInterest, 0.0001m, "预付金腿支付方向:累计利息反向为正"); Assert.AreEqual(2050m, pos.RealizedPnl, 0.0001m, "RealizedPnl = 2000 + 50 = 2050"); } // ================================================================ // 场景5:InterestDirection=0 → 不处理(兼容历史 0 方向脏数据) // 所有字段保持原值不变 // ================================================================ [TestMethod] public void 方向为零_不处理_字段保持原值() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: 0, tdCloseInterest: -999m, realizedInterest: -888m, realizedInterestFee: 77m); pos.RealizedPnl = 555m; // 预置旧值 SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(-999m, pos.TdCloseInterest, 0.0001m, "方向=0:TdCloseInterest 不变"); Assert.AreEqual(-888m, pos.RealizedInterest, 0.0001m, "方向=0:RealizedInterest 不变"); Assert.AreEqual(555m, pos.RealizedPnl, 0.0001m, "方向=0:RealizedPnl 不重算"); } // ================================================================ // 场景6:InterestDirection 为负 → 不处理(防御性,对应原 "> 0" 判断) // ================================================================ [TestMethod] public void 方向为负_不处理_字段保持原值() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: -1, tdCloseInterest: -999m, realizedInterest: -888m, realizedInterestFee: 77m); pos.RealizedPnl = 555m; SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(-999m, pos.TdCloseInterest, 0.0001m, "方向<0:TdCloseInterest 不变"); Assert.AreEqual(555m, pos.RealizedPnl, 0.0001m, "方向<0:RealizedPnl 不重算"); } // ================================================================ // 场景7:利息为零 → Math.Abs(0)=0,归一化后仍为 0,RealizedPnl=费用 // ================================================================ [TestMethod] public void 利息为零_归一化后仍为零_盈亏等于费用() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.收取, tdCloseInterest: 0m, realizedInterest: 0m, realizedInterestFee: 300m); SwapEodPositionService.NormalizeInterestSignForReport(pos); Assert.AreEqual(0m, pos.TdCloseInterest, 0.0001m); Assert.AreEqual(0m, pos.RealizedInterest, 0.0001m); Assert.AreEqual(300m, pos.RealizedPnl, 0.0001m, "RealizedPnl = 0 + 300 = 300"); } // ================================================================ // 场景8:null 入参 —— 抛 NullReferenceException(现状锚点) // 生产代码未加 null 检查,InterestDirection 解引用即 NRE。 // 若未来改为 ArgumentNullException,此处需同步更新。 // ================================================================ [TestMethod] public void Null入参_抛NullReferenceException() { Assert.ThrowsException(() => SwapEodPositionService.NormalizeInterestSignForReport(null!)); } // ================================================================ // Helper:构造 eod_swap_position // ================================================================ private static eod_swap_position NewPosition( int interestMode, int interestDirection, decimal tdCloseInterest, decimal realizedInterest, decimal realizedInterestFee) { return new eod_swap_position { InterestMode = interestMode, InterestDirection = interestDirection, TdCloseInterest = tdCloseInterest, RealizedInterest = realizedInterest, RealizedInterestFee = realizedInterestFee }; } } }