using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Helpers; namespace YLErp.Modules.SwapModule { /// /// 前端计算逻辑特征化测试(Characterization Test) /// ============================================================================ /// 目的:用 golden 冻结前端 JS 的计算行为(含用户可变输入分支), /// 作为下一轮"计算下沉后端"的金标准——后端结果必须匹配这些 golden。 /// /// 背景:前端 unwindSwapTrade.js / incomeSwapTrade.js 是实时响应式计算器, /// 用户改标的价格/平仓数量/交易费用/利息金额时,前端立刻重算 MarkClosePnl/ /// SwapRealizedPnL/SwapCloseAmount,后端拿到"前端算好的最终结果"直接记账。 /// 本测试用 C# 忠实重写前端公式作参考实现,手算真实输入的期望值存 golden。 /// /// 命名规范(见命名决策文档):参考实现内部用规范名(EntryPrice/ExitPrice/ /// floatRatio/longRatio),注释标明对应前端字段与规范语义。 /// ============================================================================ [TestClass] public class FrontendCalcCharacterizationTest { private static readonly string GoldenDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "FrontendCalc"); // FrontendCalcReference 已搬迁到生产代码 YLErpDAL/Helpers/FrontendCalcReference.cs, // 生产代码(SwapDealService校验)与测试共用同一份公式实现,避免分叉。 // ================================================================ // 8 个测试场景(含用户可变输入分支) // ================================================================ // ---- 平仓页(unwind)场景 ---- /// /// [FC_001] 平仓-债券多头-默认值(基线) /// EntryDirtyPrice(PosiGrossPrice)=1.02, ExitPrice(TradingAmountAvg,×100形态)=105, /// CloseQty=1000, PayDirection=1(收取), PositionType=1(多头), TradingFee="20" /// scale=0.01, floatRatio=1, longRatio=1 /// MarkClosePnl = round(1000×(105×0.01−1.02)×1×1×10000)/10000 = round(1000×0.03×10000)/10000 = 30 /// [TestMethod] public void FC_001_平仓_债券多头_默认值() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, CloseQty = 1000, PayDirection = 1, PositionType = 1, TradingFee = "20", TradingFeePending = "0", DividendIn = "0" }; var result = FrontendCalcReference.CalcUnwind(input); // MarkClosePnl = 1000×(1.05−1.02)×1×1 = 30 AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl"); // FloatPnlSum = 30 + 20 + 0 + 0 = 50 AssertDecimalEqual(50m, result.FloatPnlSum, 0.01m, "FloatPnlSum"); // SwapRealizedPnL = FloatPnlSum(50) AssertDecimalEqual(50m, result.SwapRealizedPnL, 0.01m, "SwapRealizedPnL"); Console.WriteLine($"FC_001: MarkClosePnl={result.MarkClosePnl}, FloatPnlSum={result.FloatPnlSum} ✅"); } /// /// [FC_002] 平仓-用户改标的价格(TradingAmountAvg 100→110) /// MarkClosePnl = round(1000×(110×0.01−1.02)×10000)/10000 = round(1000×0.08×10000)/10000 = 80 /// [TestMethod] public void FC_002_平仓_用户改标的价格() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m, // 改成110 CloseQty = 1000, PayDirection = 1, PositionType = 1, TradingFee = "20", TradingFeePending = "0", DividendIn = "0" }; var result = FrontendCalcReference.CalcUnwind(input); AssertDecimalEqual(80m, result.MarkClosePnl, 0.01m, "改价格后 MarkClosePnl"); AssertDecimalEqual(100m, result.FloatPnlSum, 0.01m, "改价格后 FloatPnlSum"); Console.WriteLine($"FC_002: 改标的价格后 MarkClosePnl={result.MarkClosePnl} ✅"); } /// /// [FC_003] 平仓-用户改平仓数量(CloseQty 1000→500,TradingFeePending 随比例变) /// MarkClosePnl = round(500×(105×0.01−1.02)×10000)/10000 = round(500×0.03×10000)/10000 = 15 /// TradingFeePending 按比例=BeforeCloseFee×ClosePercent(0.5),假设=10 /// [TestMethod] public void FC_003_平仓_用户改平仓数量() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, CloseQty = 500, // 改成500(原1000) PayDirection = 1, PositionType = 1, TradingFee = "20", TradingFeePending = "10", DividendIn = "0" }; var result = FrontendCalcReference.CalcUnwind(input); // MarkClosePnl = 500×0.03 = 15 AssertDecimalEqual(15m, result.MarkClosePnl, 0.01m, "改数量后 MarkClosePnl"); // FloatPnlSum = 15 + 20 + 10 + 0 = 45 AssertDecimalEqual(45m, result.FloatPnlSum, 0.01m, "改数量后 FloatPnlSum"); Console.WriteLine($"FC_003: 改平仓数量后 MarkClosePnl={result.MarkClosePnl} ✅"); } /// /// [FC_004] 平仓-用户改利息金额(InterestClosePnL=100) /// SwapRealizedPnL = FloatPnlSum(50) + InterestClosePnL(100) = 150 /// [TestMethod] public void FC_004_平仓_用户改利息金额() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, CloseQty = 1000, PayDirection = 1, PositionType = 1, TradingFee = "20", TradingFeePending = "0", DividendIn = "0" }; input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m }); var result = FrontendCalcReference.CalcUnwind(input); AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl 不受利息影响"); // SwapRealizedPnL = 50 + 100 = 150 AssertDecimalEqual(150m, result.SwapRealizedPnL, 0.01m, "含利息的 SwapRealizedPnL"); Console.WriteLine($"FC_004: 改利息后 SwapRealizedPnL={result.SwapRealizedPnL} ✅"); } /// /// [FC_005] 平仓-非债券空头(PositionType=Short=2, multiplier=1) /// floatRatio=1(收取), longRatio=-1(空头) /// MarkClosePnl = round(1000×(100×1−100)×1×(−1)×10000)/10000 = 0(价格不变时空头盈亏=0) /// 改成价格涨:TradingAmountAvg=105, MarkClosePnl=round(1000×(105−100)×1×(−1)×10000)/10000=−50000 /// 空头价格涨=亏损 /// [TestMethod] public void FC_005_平仓_非债券空头_方向因子() { var input = new UnwindInput { Multiplier = 1, PosiGrossPrice = 100m, TradingAmountAvg = 105m, // 涨了5 CloseQty = 1000, PayDirection = 1, PositionType = 2, // 空头 TradingFee = "0", TradingFeePending = "0", DividendIn = "0" }; var result = FrontendCalcReference.CalcUnwind(input); // 空头价格涨=亏损:1000×(105−100)×1×(−1) = −5000 AssertDecimalEqual(-5000m, result.MarkClosePnl, 0.01m, "空头价格涨=亏损"); Console.WriteLine($"FC_005: 空头方向因子 MarkClosePnl={result.MarkClosePnl} ✅"); } // ---- 结息页(income)场景 ---- /// /// [FC_006] 结息-债券多头-全量结算(基线) /// income 使用持仓数量和合约乘数,无 longRatio /// EntryPrice=1.02, TradingAmountAvg=105(×100形态), PositionQty=10000, ContractSize=1 /// MarkClosePnl = 10000×1×(105×0.01−1.02)×1 = 10000×0.03 = 300 /// [TestMethod] public void FC_006_结息_债券多头_全量结算() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, PositionQty = 10000, ContractSize = 1, CloseNotionalValue = 10200, // 与数量刻意不同,守卫 income 不再误用名义本金 CloseQty = 0, // income 不用数量 PayDirection = 1, PositionType = 1, TradingFee = "0", TradingFeePending = "0", DividendIn = "0" }; var result = FrontendCalcReference.CalcIncome(input); AssertDecimalEqual(300m, result.MarkClosePnl, 0.01m, "income MarkClosePnl"); AssertDecimalEqual(300m, result.SwapRealizedPnL, 0.01m, "income SwapRealizedPnL"); Console.WriteLine($"FC_006: income MarkClosePnl={result.MarkClosePnl} ✅"); } /// /// [FC_007] 结息-用户改标的价格(TradingAmountAvg 105→110) /// MarkClosePnl = 10000×(110×0.01−1.02) = 10000×0.08 = 800 /// [TestMethod] public void FC_007_结息_用户改标的价格() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m, PositionQty = 10000, ContractSize = 1, CloseNotionalValue = 10200, CloseQty = 0, PayDirection = 1, PositionType = 1, TradingFee = "0", TradingFeePending = "0", DividendIn = "0" }; var result = FrontendCalcReference.CalcIncome(input); AssertDecimalEqual(800m, result.MarkClosePnl, 0.01m, "改价格后 income MarkClosePnl"); Console.WriteLine($"FC_007: 改价格后 income MarkClosePnl={result.MarkClosePnl} ✅"); } /// /// [FC_008] 结息-含利息腿与预付金腿(InterestClosePnL + margin InterestClosePnL) /// SwapRealizedPnL = FloatPnlSum(300) + 利息腿(100) + 预付金腿(50) = 450 /// SwapMarginRebatePnl = 预付金腿(50) /// [TestMethod] public void FC_008_结息_含利息腿与预付金腿_总额() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m, PositionQty = 10000, ContractSize = 1, CloseNotionalValue = 10200, CloseQty = 0, PayDirection = 1, PositionType = 1, TradingFee = "0", TradingFeePending = "0", DividendIn = "0" }; input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m }); input.MarginLegs.Add(new LegInput { InterestClosePnL = 50m }); var result = FrontendCalcReference.CalcIncome(input); // SwapRealizedPnL = 300 + 100 + 50 = 450 AssertDecimalEqual(450m, result.SwapRealizedPnL, 0.01m, "含利息+预付金的 SwapRealizedPnL"); // SwapMarginRebatePnl = 50 AssertDecimalEqual(50m, result.SwapMarginRebatePnl, 0.01m, "SwapMarginRebatePnl"); Console.WriteLine($"FC_008: SwapRealizedPnL={result.SwapRealizedPnL}, SwapMarginRebatePnl={result.SwapMarginRebatePnl} ✅"); } /// /// [FC_009] 结息-债券支付端:价差盈亏必须按数量计算,不能按期初名义本金计算。 /// 纯价差 = 30000000×1×(80%−98%)×(−1) = 5400000;加分红-45000后合计5355000。 /// [TestMethod] public void FC_009_结息_债券价差按数量计算() { var input = new UnwindInput { Multiplier = 100, PosiGrossPrice = 0.98m, TradingAmountAvg = 80m, PositionQty = 30000000m, ContractSize = 1m, CloseNotionalValue = 29400000m, CloseQty = 0m, PayDirection = 2, PositionType = 1, TradingFee = "0", TradingFeePending = "0", DividendIn = "-45000" }; var result = FrontendCalcReference.CalcIncome(input); AssertDecimalEqual(5400000m, result.MarkClosePnl, 0.01m, "income MarkClosePnl按数量计算"); AssertDecimalEqual(5355000m, result.FloatPnlSum, 0.01m, "income FloatPnlSum包含分红"); } private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") { Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); } } }