/** * markClosePnlShortConsistency.test.js — 空头场景下 unwind/income 两页 MarkClosePnl 一致性(回归守卫) * ============================================================================ * 状态:GREEN(income 页 MarkClosePnl 已补 longRatio,与 unwind 对齐)。 * - incomeSwapTrade.js:200/207 已加 longRatio(fix d78d1f48 等) * - FrontendCalcReference.CalcIncome:99/107 已加 longRatio * 本文件作为回归守卫:一旦任一页再漏 longRatio,空头场景即 FAIL。 * * 背景: * - 团队金标准 FrontendCalcReference 明确记录两页差异:unwind 含 longRatio, * income 曾缺 longRatio(CalcIncome:107,旧注释"无 longRatio"),导致空头符号反。 * - 现有特征化测试 FrontendCalcCharacterizationTest FC_001~009 的 income 场景 * (FC_006~009)全部 PositionType=1(多头),唯一空头场景 FC_005 是 unwind, * 因此 income 的空头分支从未被覆盖 → bug 长期未被发现。 * - 后端 ValidateFrontendPnL 用同一 CalcIncome 重算比对,公式同源故永远自洽,抓不到。 * * 公式逐字抄录(来源见注释行号),与生产代码一致;不改动任何生产文件。 */ const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js'); // unwind 页 MarkClosePnl(unwindSwapTrade.js:313,含 longRatio) function PROD_unwindMarkClosePnl({ closeQty, deliveryPrice, initPosiNetPrice, payDirection, positionType }) { const floatRatio = payDirection === 1 ? 1 : -1; const longRatio = positionType === 1 ? 1 : -1; let v = Math.round(closeQty * (deliveryPrice - initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000; return Number(v.toFixed(2)); } // income 页 MarkClosePnl(incomeSwapTrade.js:207,已补 longRatio,与 unwind 对齐) function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection, positionType }) { const floatRatio = payDirection === 1 ? 1 : -1; const longRatio = positionType === 1 ? 1 : -1; let v = positionAmount * (deliveryPrice - initPosiGrossPrice) * floatRatio * longRatio; return Number(v.toFixed(2)); } // 金标准(swapCalc.calcMarkClosePnl,对齐 C# FrontendCalcReference.CalcUnwind,含 longRatio) function GOLD({ closeQty, tradingAmountAvg, scale, entryPrice, payDirection, positionType }) { const floatRatio = payDirection === 1 ? 1 : -1; const longRatio = positionType === 1 ? 1 : -1; return SwapCalc.calcMarkClosePnl(closeQty, tradingAmountAvg, scale, entryPrice, floatRatio, longRatio); } // 同一笔债券 TRS:期初全价 1.02,期末 105(×100形态→1.05),价差 0.03;数量 10000 const CASE = { closeQty: 10000, positionAmount: 10000, deliveryPrice: 1.05, initPosiNetPrice: 1.02, initPosiGrossPrice: 1.02, tradingAmountAvg: 105, scale: 0.01, entryPrice: 1.02, payDirection: 1, }; describe('MarkClosePnl 两页一致性(多头,应一致)', () => { test('多头:unwind == income == gold', () => { const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 1 }); const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 1 }); const g = GOLD({ ...CASE, positionType: 1 }); expect(u).toBe(i); expect(i).toBe(g); }); }); describe('MarkClosePnl 两页一致性(空头,当前 RED)', () => { test('空头:unwind == income(longRatio 对齐后一致)', () => { const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 2 }); const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 }); // 空头价格涨应亏损:unwind = income = -300(longRatio 对齐后符号一致) expect(u).toBe(i); }); test('空头:income 应等于 gold(longRatio 对齐后一致)', () => { const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 }); const g = GOLD({ ...CASE, positionType: 2 }); expect(i).toBe(g); }); });