Files
zszq-trs/YLErpWeb/fe-tests/swapCalc.test.js
T
hjhan 7f64d48699 test(fe): 接入 SwapCalc 纯计算模块 + 前端精度回归测试
- incomeSwapTrade.js / swapTradeEdit.js 改为调用 SwapCalc.*(缩放/取整/全价推导)

- 新增 fe-tests 守卫:4 个历史 bug 回归 + 8 场景对齐 C# FrontendCalcReference 金标准 + otcformat.js 精度配置守卫

- 新增 .git/hooks/pre-commit 自动运行前端守卫(失败阻断提交)
2026-07-07 12:30:19 +08:00

137 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
// 3c5f25a5FloatPnlSum 必须保留 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');
});
});