/** * swapCalc.test.js — 前端计算逻辑单元测试 * ============================================================================ * 双重目的: * 1) 回归守卫:锁定 4 个曾出 bug 的纯函数(dcf649f2 / 20ea93d8 / 3c5f25a5 / f873239a) * 2) 交叉校验:用 8 个场景(FC_001~FC_008)对齐 C# FrontendCalcCharacterizationTest 金标准, * 一旦 JS 公式与后端 FrontendCalcReference 分叉,测试即红。 * * 运行:cd YLErpWeb/fe-tests && npm i && npm test */ const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js'); const TOL = 1e-6; function expectClose(actual, expected, msg) { expect(Math.abs(actual - expected)).toBeLessThanOrEqual(TOL, msg || ''); } describe('回归守卫:曾出 bug 的纯函数', () => { // 20ea93d8 / dcf649f2:必须用全价(PosiGrossPrice) 且债券 ×100 test('deriveTradingAmountAvg 债券用全价并 ×100 (守卫 20ea93d8/dcf649f2)', () => { expectClose(SwapCalc.deriveTradingAmountAvg(1.02, 100), 102, '债券: 1.02×100=102(界面百分比态)'); expectClose(SwapCalc.deriveTradingAmountAvg(1.02, 1), 1.02, '非债券: 不缩放'); // 若误用净价(PosiNetPrice) 会偏离,这里锁定"全价"语义 expect(SwapCalc.deriveTradingAmountAvg(1.02, 100)).toBe(102); }); // 3c5f25a5:FloatPnlSum 必须保留 2 位小数 test('calcFloatPnlSum 保留 2 位 (守卫 3c5f25a5)', () => { expectClose(SwapCalc.calcFloatPnlSum(100.456, 1, 0.5, 0), 101.96, '100.456+1+0.5=101.956→101.96'); expectClose(SwapCalc.calcFloatPnlSum(30, 20, 0, 0), 50, '30+20=50'); expect(SwapCalc.calcFloatPnlSum(30, 20, 0, 0)).toBe(50); }); // f873239a:名义本金 round 到 2 位 test('calcStockEqvNotional round 2 位 (守卫 f873239a)', () => { expectClose(SwapCalc.calcStockEqvNotional(1.02, 100 * 100), 10200, '1.02×10000=10200'); expectClose(SwapCalc.calcStockEqvNotional(10.005, 100), 1000.5, '10.005×100=1000.50'); }); test('getPriceScale 债券=0.01 非债券=1', () => { expect(SwapCalc.getPriceScale(100)).toBe(0.01); expect(SwapCalc.getPriceScale(1)).toBe(1); }); }); describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => { // FC_001 平仓-债券多头-默认 test('FC_001 平仓 债券多头 默认', () => { const r = SwapCalc.calcUnwind({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, closeQty: 1000, payDirection: 1, positionType: 1, tradingFee: '20', tradingFeePending: '0', dividendIn: '0' }); expectClose(r.MarkClosePnl, 30, 'MarkClosePnl'); expectClose(r.FloatPnlSum, 50, 'FloatPnlSum'); expectClose(r.SwapRealizedPnL, 50, 'SwapRealizedPnL'); }); // FC_002 改标的价格 105→110 test('FC_002 平仓 改标的价格', () => { const r = SwapCalc.calcUnwind({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 110, closeQty: 1000, payDirection: 1, positionType: 1, tradingFee: '20', tradingFeePending: '0', dividendIn: '0' }); expectClose(r.MarkClosePnl, 80, 'MarkClosePnl'); expectClose(r.FloatPnlSum, 100, 'FloatPnlSum'); }); // FC_003 改平仓数量 1000→500 test('FC_003 平仓 改平仓数量', () => { const r = SwapCalc.calcUnwind({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, closeQty: 500, payDirection: 1, positionType: 1, tradingFee: '20', tradingFeePending: '10', dividendIn: '0' }); expectClose(r.MarkClosePnl, 15, 'MarkClosePnl'); expectClose(r.FloatPnlSum, 45, 'FloatPnlSum'); }); // FC_004 改利息金额 +100 test('FC_004 平仓 改利息金额', () => { const r = SwapCalc.calcUnwind({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, closeQty: 1000, payDirection: 1, positionType: 1, tradingFee: '20', tradingFeePending: '0', dividendIn: '0', interestLegs: [{ interestClosePnL: 100 }] }); expectClose(r.MarkClosePnl, 30, 'MarkClosePnl 不受利息影响'); expectClose(r.SwapRealizedPnL, 150, '含利息 SwapRealizedPnL'); }); // FC_005 非债券空头 方向因子 test('FC_005 平仓 非债券空头 方向因子', () => { const r = SwapCalc.calcUnwind({ multiplier: 1, posiGrossPrice: 100, tradingAmountAvg: 105, closeQty: 1000, payDirection: 1, positionType: 2, tradingFee: '0', tradingFeePending: '0', dividendIn: '0' }); expectClose(r.MarkClosePnl, -5000, '空头价格涨=亏损'); }); // FC_006 结息 债券多头 全量 test('FC_006 结息 债券多头 全量', () => { const r = SwapCalc.calcIncome({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, closeNotionalValue: 10000, closeQty: 0, payDirection: 1, positionType: 1, tradingFee: '0', tradingFeePending: '0', dividendIn: '0' }); expectClose(r.MarkClosePnl, 300, 'income MarkClosePnl'); expectClose(r.SwapRealizedPnL, 300, 'income SwapRealizedPnL'); }); // FC_007 结息 改标的价格 105→110 test('FC_007 结息 改标的价格', () => { const r = SwapCalc.calcIncome({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 110, closeNotionalValue: 10000, closeQty: 0, payDirection: 1, positionType: 1, tradingFee: '0', tradingFeePending: '0', dividendIn: '0' }); expectClose(r.MarkClosePnl, 800, '改价格后 income MarkClosePnl'); }); // FC_008 结息 含利息腿+预付金腿 test('FC_008 结息 含利息腿与预付金腿 总额', () => { const r = SwapCalc.calcIncome({ multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, closeNotionalValue: 10000, closeQty: 0, payDirection: 1, positionType: 1, tradingFee: '0', tradingFeePending: '0', dividendIn: '0', interestLegs: [{ interestClosePnL: 100 }], marginLegs: [{ interestClosePnL: 50 }] }); expectClose(r.SwapRealizedPnL, 450, '含利息+预付金 SwapRealizedPnL'); expectClose(r.SwapMarginRebatePnl, 50, 'SwapMarginRebatePnl'); }); });