using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// SwapEodPositionService.CalculateSwapRealizedPnl 的回归测试。 /// --------------------------------------------------------------- /// 守卫张名锐提交 6676b625 "fix(swap): 修正掉期产品保证金利息计算逻辑"。 /// /// 旧 bug:eod_swap.RealizedPnL 直接 Sum(s.RealizedPnl),未对保证金腿利息做方向反向, /// 导致"收取对手方保证金"产生的利息被错误计入我方收益(实际是我方支付给对手方的成本), /// 框架合约已实现收益虚高。 /// /// 修复:新增 CalculateSwapRealizedPnl —— /// 非保证金腿:interestRatio = Direction==收取 ? 1 : -1(维持数据库方向) /// 保证金腿(初始预付金 5 / 追加预付金 6):interestRatio 反向 /// 最终:RealizedInterest × interestRatio + 其他 4 字段 /// /// 抽为 public static 纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。 /// 本测试直接锁定方向反向契约,防止后续误改回归。 /// [TestClass] public class SwapEodRealizedPnlCalcTest { // ================================================================ // 场景1:非保证金腿收取方向 → RealizedInterest × +1(维持原向) // ================================================================ [TestMethod] public void 非保证金腿_收取方向_利息维持原向系数为1() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.收取, realizedInterest: 1000m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); Assert.AreEqual(1000m, result, 0.0001m, "非保证金腿收取方向:利息 ×(+1)=1000"); } // ================================================================ // 场景2:非保证金腿支付方向 → RealizedInterest × -1(维持原向) // ================================================================ [TestMethod] public void 非保证金腿_支付方向_利息维持原向系数为负1() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.支付, realizedInterest: 1000m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); Assert.AreEqual(-1000m, result, 0.0001m, "非保证金腿支付方向:利息 ×(-1)=-1000"); } // ================================================================ // 场景3:保证金腿(初始预付金)收取方向 → 利息反向,系数 -1 // 这是 6676b625 修复的核心场景:收取对手方保证金产生的利息是我方支付成本 // ================================================================ [TestMethod] public void 保证金腿_初始预付金_收取方向_利息反向系数为负1() { var pos = NewPosition( interestMode: (int)InterestModeEnum.初始预付金, interestDirection: (int)SwapDirectionEnum.收取, realizedInterest: 1000m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); Assert.AreEqual(-1000m, result, 0.0001m, "保证金腿收取方向:利息应反向 ×(-1)=-1000(修复前会错误得 +1000)"); } // ================================================================ // 场景4:保证金腿(初始预付金)支付方向 → 利息反向,系数 +1 // ================================================================ [TestMethod] public void 保证金腿_初始预付金_支付方向_利息反向系数为1() { var pos = NewPosition( interestMode: (int)InterestModeEnum.初始预付金, interestDirection: (int)SwapDirectionEnum.支付, realizedInterest: 1000m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); Assert.AreEqual(1000m, result, 0.0001m, "保证金腿支付方向:利息应反向 ×(+1)=1000"); } // ================================================================ // 场景5:追加预付金同初始预付金,同样走反向逻辑 // ================================================================ [TestMethod] public void 保证金腿_追加预付金_收取方向_利息反向() { var pos = NewPosition( interestMode: (int)InterestModeEnum.追加预付金, interestDirection: (int)SwapDirectionEnum.收取, realizedInterest: 500m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); Assert.AreEqual(-500m, result, 0.0001m, "追加预付金(mode=6)与初始预付金(mode=5)同走反向逻辑"); } // ================================================================ // 场景6:完整 5 字段汇总(MtmPnL + Dividend + Fee + Interest×ratio + InterestFee) // 保证金腿收取方向,Interest=200, 其他各 100 // 期望:100 + 100 + 100 + 200×(-1) + 100 = 200 // ================================================================ [TestMethod] public void 完整5字段汇总_保证金腿收取方向_利息反向后合计正确() { var pos = NewPosition( interestMode: (int)InterestModeEnum.初始预付金, interestDirection: (int)SwapDirectionEnum.收取, realizedMtmPnL: 100m, realizedDividend: 100m, realizedFee: 100m, realizedInterest: 200m, realizedInterestFee: 100m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); // 100 + 100 + 100 + 200×(-1) + 100 = 200 Assert.AreEqual(200m, result, 0.0001m, "5 字段汇总:保证金腿收取方向,Interest×(-1) 后合计=200,验证所有字段都参与计算"); } // ================================================================ // 场景7:完整 5 字段汇总(非保证金腿收取方向) // 非保证金腿收取方向,Interest=200, 其他各 100 // 期望:100 + 100 + 100 + 200×(+1) + 100 = 600 // ================================================================ [TestMethod] public void 完整5字段汇总_非保证金腿收取方向_利息原向合计正确() { var pos = NewPosition( interestMode: (int)InterestModeEnum.固定值, interestDirection: (int)SwapDirectionEnum.收取, realizedMtmPnL: 100m, realizedDividend: 100m, realizedFee: 100m, realizedInterest: 200m, realizedInterestFee: 100m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); // 100 + 100 + 100 + 200×(+1) + 100 = 600 Assert.AreEqual(600m, result, 0.0001m, "5 字段汇总:非保证金腿收取方向,Interest×(+1) 后合计=600"); } // ================================================================ // 场景8:RealizedInterest=0 边界 —— 方向反向无影响,结果为其他 4 字段之和 // ================================================================ [TestMethod] public void 利息为零_方向反向无影响_结果为其他4字段之和() { var pos = NewPosition( interestMode: (int)InterestModeEnum.初始预付金, interestDirection: (int)SwapDirectionEnum.收取, realizedMtmPnL: 100m, realizedDividend: 50m, realizedFee: 30m, realizedInterest: 0m, realizedInterestFee: 20m); var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos); // 100 + 50 + 30 + 0×(-1) + 20 = 200 Assert.AreEqual(200m, result, 0.0001m, "RealizedInterest=0 时方向反向无影响,结果为其他 4 字段之和"); } // ================================================================ // Helper:构造 eod_swap_position(只设置参与计算的 7 个字段) // ================================================================ private static eod_swap_position NewPosition( int interestMode, int interestDirection, decimal realizedMtmPnL = 0m, decimal realizedDividend = 0m, decimal realizedFee = 0m, decimal realizedInterest = 0m, decimal realizedInterestFee = 0m) { return new eod_swap_position { InterestMode = interestMode, InterestDirection = interestDirection, RealizedMtmPnL = realizedMtmPnL, RealizedDividend = realizedDividend, RealizedFee = realizedFee, RealizedInterest = realizedInterest, RealizedInterestFee = realizedInterestFee }; } } }