Files
zszq-trs/YLErpWeb/fe-tests/swapPrecisionConfig.test.js
T
hjhan 68a3f0b4fa debug(swap)+test: 平仓利息 otcdebug 埋点 + 修复3个前端红测试
1) otcdebug 埋点(unwindSwapTrade.js, 仅 ?otcdebug=1 开启, 生产零噪音):
   - changeClosePercent: 记录实际发给后端的 closePercent
   - getInterestList: 记录 POST closePercent / 返回逐腿 InterestMode·Principal·Amount·Rate
   - 用途: 未来再遇"改比例利息腿不动", 开 ?otcdebug=1 即可定位前端没传对还是后端没缩放

2) 修复分支上既有3个红测试(均为配置/生产已改、测试未跟上或契约过时):
   - swapPrecisionConfig.test.js: quantityIntegerDigits 对齐配置(16→8, Bond等12);
     expectedQuantityPrecisions 对齐配置(Fund=4, ExRate=4), 与同文件 line87/89 一致
   - bondCalc.integration.test.js: 补 require('fs')/require('path'); 事件契约更新为
     ['keydown','input','input','enter'](onKeydown 回车故意幂等重发一次 input, 修 QA 高优 Bug)
   - markClosePnlShortConsistency.test.js: 空头一致性测试由 RED 转 GREEN(income 已补 longRatio);
     CASE deliveryPrice 105→1.05 与 GOLD 的 105*scale(0.01) 单位对齐

引入溯源: 固定值腿缩放缺陷见 d1badfe4; 精度配置 16→8 来自 0672dbbc(张名锐);
红测试为已知缺口记录(用户 hjhan)。
2026-08-08 12:18:45 +08:00

211 lines
10 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = path.resolve(__dirname, '..');
const read = (relativePath) => fs.readFileSync(path.join(ROOT, relativePath), 'utf8');
const precisionConfigSrc = read('App_Data/Config/swappriceprecision.js');
const precisionHelperSrc = read('wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
function loadConfiguredPrecision() {
const context = { window: {} };
vm.runInNewContext(precisionConfigSrc, context);
return context.window.main.swapPricePrecision;
}
function loadHelper(config) {
const context = { window: { main: { swapPricePrecision: config } } };
vm.runInNewContext(precisionHelperSrc, context);
return context.swapPricePrecision;
}
function activeViewSource(source) {
return source.replace(/@\*[\s\S]*?\*@/g, '');
}
describe('swap price precision common wiring', () => {
test('configured and fallback common precision expose 2/2/4', () => {
const configured = loadConfiguredPrecision();
expect(configured.common).toEqual({
amount: { precision: 2, grouping: true },
quantity: { integerDigits: 8, precision: 2, grouping: true },
rate: { precision: 4 }
});
const helper = loadHelper(configured);
expect(helper.getCommonPrecision('amount')).toBe(2);
expect(helper.getCommonPrecision('quantity')).toBe(2);
expect(helper.getCommonPrecision('rate')).toBe(4);
const expectedQuantityPrecisions = {
Stock: 2,
StockIndex: 2,
StockIF: 2,
CommodityFutures: 2,
CommoditySpot: 2,
NewOtcStock: 2,
HKStock: 2,
HKStockIndex: 2,
Fund: 4,
Bond: 0,
TBonds: 0,
CreditBonds: 0,
OtherBonds: 0,
TBFutures: 2,
OtherFutures: 2,
GoldSpot: 2,
OtherSpot: 2,
AbroadFutures: 2,
AbroadSpot: 2,
AbroadStock: 2,
AbroadStockIndex: 2,
ExRate: 4,
Shibor: 2,
FixingRepoRate: 2,
RateYield: 2,
BondIndex: 2
};
Object.entries(expectedQuantityPrecisions).forEach(([instrumentType, precision]) => {
expect(configured[instrumentType].quantityPrecision).toBe(precision);
expect(helper.getCommonPrecision('quantity', instrumentType)).toBe(precision);
expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).precision).toBe(precision);
});
const expectedQuantityIntegerDigits = {
Stock: 8, StockIndex: 8, StockIF: 8, CommodityFutures: 8, CommoditySpot: 8,
NewOtcStock: 8, HKStock: 8, HKStockIndex: 8, Fund: 8, TBFutures: 8,
OtherFutures: 8, GoldFutures: 8, GoldSpot: 8, OtherSpot: 8, AbroadFutures: 8,
AbroadSpot: 8, AbroadStock: 8, AbroadStockIndex: 8, Shibor: 8,
FixingRepoRate: 8, RateYield: 8, BondIndex: 8,
Bond: 12, TBonds: 12, CreditBonds: 12, OtherBonds: 12, ExRate: 8
};
Object.entries(expectedQuantityIntegerDigits).forEach(([instrumentType, integerDigits]) => {
expect(configured[instrumentType].quantityIntegerDigits).toBe(integerDigits);
expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).integerDigits).toBe(integerDigits);
});
expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(2);
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').precision).toBe(4);
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').stringMode).toBe(true);
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Bond').precision).toBe(0);
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'OtherRate').integerDigits).toBe(8);
const input = helper.getCommonInputFormat('amount', { append: '', trimTailZeros: true });
expect(input).toEqual(expect.objectContaining({ precision: 2, grouping: true, trimTailZeros: false }));
expect(helper.getCommonInputFormat('rate', { append: '%' })).toEqual(
expect.objectContaining({ precision: 4, grouping: false }));
});
test('invalid common precision falls back and formatted values keep trailing zeros', () => {
const helper = loadHelper({
common: {
amount: { precision: -1 },
quantity: { precision: 14 },
rate: { precision: 'invalid' }
}
});
expect(helper.getCommonPrecision('amount')).toBe(2);
expect(helper.getCommonPrecision('quantity')).toBe(2);
expect(helper.getCommonPrecision('rate')).toBe(4);
const configured = loadHelper({
common: {
amount: { precision: 3 },
quantity: { precision: 1 },
rate: { precision: 2 }
}
});
expect(configured.formatCommon('amount', '12.5')).toBe('12.500');
expect(configured.formatCommon('quantity', 1)).toBe('1.0');
expect(configured.formatCommon('rate', '0.0123')).toBe('1.23%');
expect(configured.formatCommon('amount', '1234.5')).toBe('1,234.500');
expect(configured.normalizeCommon('amount', '1234.5')).toBe('1234.500');
expect(configured.formatCommon('amount', NaN)).toBe('');
expect(configured.formatCommon('amount', Infinity)).toBe('');
});
test('common grouping can be disabled independently from precision', () => {
const helper = loadHelper({
common: {
amount: { precision: 2, grouping: false },
quantity: { precision: 2, grouping: false },
rate: { precision: 4, grouping: false }
}
});
expect(helper.formatCommon('amount', '1234.5')).toBe('1234.50');
expect(helper.getCommonInputFormat('quantity', { append: '' }).grouping).toBe(false);
});
test('quantity display can trim trailing zeros without changing configured precision', () => {
const helper = loadHelper({ common: { quantity: { precision: 8, grouping: true } } });
expect(helper.formatCommon('quantity', '10000.12345600', { trimTailZeros: true }))
.toBe('10,000.123456');
expect(helper.formatCommon('quantity', '10000.12345600')).toBe('10,000.12345600');
});
test('missing or invalid asset quantity precision falls back to common quantity precision', () => {
const helper = loadHelper({
common: { quantity: { precision: 3, grouping: true } },
Stock: { quantityPrecision: 'invalid' },
Bond: {}
});
expect(helper.getCommonPrecision('quantity', 'Stock')).toBe(3);
expect(helper.getCommonPrecision('quantity', 'Bond')).toBe(3);
expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(3);
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(8);
expect(loadHelper({
common: { quantity: { integerDigits: 0, precision: 3 } },
Stock: { quantityIntegerDigits: 'invalid' }
}).getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(8);
});
test('SwapTrade2 common fields use the price helper without changing price responsibilities', () => {
const scripts = {
edit: read('wwwroot/Scripts/app/swaptrade/swapTradeEdit.js'),
income: read('wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js'),
unwind: read('wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js'),
longShort: read('wwwroot/Scripts/app/swaptrade/swapLongShort.js'),
unwindLongShort: read('wwwroot/Scripts/app/swaptrade/unwindLongShort.js'),
view: read('wwwroot/Scripts/app/swaptrade/swapTradeView.js'),
flow: read('wwwroot/Scripts/app/swaptrade/SwapflowList.js'),
helper: precisionHelperSrc
};
const view = read('Views/SwapTrade2/TradeView.cshtml');
const activeView = activeViewSource(view);
for (const script of Object.values(scripts)) {
expect(script).not.toMatch(/otcformat\.trading\.swap(?:Amount|Quantity|RateP)/);
}
expect(scripts.edit).toMatch(/getCommonInputFormat\('amount'/);
expect(scripts.edit).toMatch(/getCommonInputFormat\(\s*['"]quantity['"]/);
expect(scripts.edit).toMatch(/getCommonInputFormat\('rate'/);
expect(scripts.view).toContain("swapPricePrecision.formatCommon('rate', item.Rate)");
expect(scripts.flow).toMatch(/name:\s*'ytm'[\s\S]*formatter:\s*otcformat\.trading\.marginRateP/);
expect(scripts.flow).not.toMatch(/name:\s*'ytm'[\s\S]*formatCommon\('rate'/);
expect(scripts.unwind).toContain('ClosePercent = otcformat.fixed6');
expect(scripts.unwind).toContain('_.round(tradingFee, 2)');
expect(scripts.unwind).toContain('.toFixed(2)');
expect(scripts.edit).toContain('observationRatePrecision = 12');
expect(scripts.edit).toMatch(/roundObservationRate[\s\S]*swapPricePrecision\.roundDecimal/);
expect(scripts.helper).toContain('getRule: getRule');
expect(scripts.helper).toContain('getInputFormat: function');
expect(scripts.helper).toContain('roundForSubmit: function');
expect(scripts.helper).toContain('normalizeCommon: function');
expect(activeView).not.toContain('OtcFormatHelper');
expect(activeView).not.toMatch(/OtcFormatMoney|OtcFormatNotional/);
expect(activeView).toContain('SwapCommonData');
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="amount"/);
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="quantity"/);
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="rate"/);
expect(scripts.view).toContain('formatSwapCommonElements');
expect(scripts.edit).toContain('getQuantityInputFormat');
expect(scripts.unwind).toContain('getQuantityInputFormat');
expect(scripts.flow).toContain('getQuantityInputFormat');
expect(scripts.flow).toMatch(/formatSwapQuantity = function \(value, options, rowObject\)/);
expect(scripts.flow).toMatch(/row\.UnderlyingInstrumentType[\s\S]*row\.InstrumentType[\s\S]*row\.position[\s\S]*UnderlyingInstrumentType/);
expect(scripts.flow).toContain("formatCommon('quantity', value, instrumentType)");
expect(activeView).toMatch(/data-kind="quantity"[^>]*data-instrument-type=/);
});
});