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)。
This commit is contained in:
hjhan
2026-08-08 12:18:45 +08:00
parent d1badfe48f
commit 68a3f0b4fa
4 changed files with 47 additions and 27 deletions
@@ -13,6 +13,8 @@
* 运行:cd YLErpWeb/fe-tests && npx jest bondCalc.integration
*/
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
global.window = dom.window;
@@ -82,9 +84,13 @@ describe('TradeEdit bond price input bindings', () => {
vm.onKeydown({ keyCode: 13, target });
expect(emitted.map(x => x.event)).toEqual(['keydown', 'input', 'enter']);
// onKeydown 回车时故意再派发一次 inputevent.target.blur() 触发 onChange 派发一次,
// onKeydown 又直接派发一次(见 swapPricePrecisionHelper.js,注释标明"幂等无害"),
// 用于修复「重输/重贴原值 + 回车」静默不计算的 QA 高优 Bug。
expect(emitted.map(x => x.event)).toEqual(['keydown', 'input', 'input', 'enter']);
expect(emitted[1].value).toBe('0.995');
expect(emitted[2].value).toBe('0.995');
expect(emitted[3].value).toBe('0.995');
});
test('equal format refresh does not overwrite uncommitted input', () => {
@@ -1,13 +1,14 @@
/**
* markClosePnlShortConsistency.test.js — 空头场景下 unwind/income 两页 MarkClosePnl 一致性(红测试
* markClosePnlShortConsistency.test.js — 空头场景下 unwind/income 两页 MarkClosePnl 一致性(回归守卫
* ============================================================================
* 状态:当前为 RED(证明 income 页 MarkClosePnl longRatio 的真实不一致)。
* 修复 incomeSwapTrade.js:207 与 YLErpDAL/Helpers/FrontendCalcReference.CalcIncome:107
* 补上 longRatio 后,本文件应全部转 GREEN。
* 状态:GREENincome 页 MarkClosePnl 已补 longRatio,与 unwind 对齐)。
* - incomeSwapTrade.js:200/207 已加 longRatiofix d78d1f48 等)
* - FrontendCalcReference.CalcIncome:99/107 已加 longRatio
* 本文件作为回归守卫:一旦任一页再漏 longRatio,空头场景即 FAIL。
*
* 背景:
* - 团队金标准 FrontendCalcReference 明确记录两页差异:unwind 含 longRatio
* income longRatioCalcIncome:107,注释"无 longRatio")。
* income 曾缺 longRatioCalcIncome:107注释"无 longRatio",导致空头符号反
* - 现有特征化测试 FrontendCalcCharacterizationTest FC_001~009 的 income 场景
* FC_006~009)全部 PositionType=1(多头),唯一空头场景 FC_005 是 unwind
* 因此 income 的空头分支从未被覆盖 → bug 长期未被发现。
@@ -25,10 +26,11 @@ function PROD_unwindMarkClosePnl({ closeQty, deliveryPrice, initPosiNetPrice, pa
return Number(v.toFixed(2));
}
// income 页 MarkClosePnlincomeSwapTrade.js:207 longRatio
function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection }) {
// income 页 MarkClosePnlincomeSwapTrade.js:207已补 longRatio,与 unwind 对齐
function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection, positionType }) {
const floatRatio = payDirection === 1 ? 1 : -1;
let v = positionAmount * (deliveryPrice - initPosiGrossPrice) * floatRatio;
const longRatio = positionType === 1 ? 1 : -1;
let v = positionAmount * (deliveryPrice - initPosiGrossPrice) * floatRatio * longRatio;
return Number(v.toFixed(2));
}
@@ -42,7 +44,7 @@ function GOLD({ closeQty, tradingAmountAvg, scale, entryPrice, payDirection, pos
// 同一笔债券 TRS:期初全价 1.02,期末 105(×100形态→1.05),价差 0.03;数量 10000
const CASE = {
closeQty: 10000, positionAmount: 10000,
deliveryPrice: 105, initPosiNetPrice: 1.02, initPosiGrossPrice: 1.02,
deliveryPrice: 1.05, initPosiNetPrice: 1.02, initPosiGrossPrice: 1.02,
tradingAmountAvg: 105, scale: 0.01, entryPrice: 1.02,
payDirection: 1,
};
@@ -50,7 +52,7 @@ const CASE = {
describe('MarkClosePnl 两页一致性(多头,应一致)', () => {
test('多头:unwind == income == gold', () => {
const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 1 });
const i = PROD_incomeMarkClosePnl({ ...CASE, });
const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 1 });
const g = GOLD({ ...CASE, positionType: 1 });
expect(u).toBe(i);
expect(i).toBe(g);
@@ -58,15 +60,15 @@ describe('MarkClosePnl 两页一致性(多头,应一致)', () => {
});
describe('MarkClosePnl 两页一致性(空头,当前 RED)', () => {
test('空头:unwind == income当前 FAIL → 证明 income 缺 longRatio 的 bug', () => {
test('空头:unwind == incomelongRatio 对齐后一致', () => {
const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 2 });
const i = PROD_incomeMarkClosePnl({ ...CASE, });
// 空头价格涨应亏损:unwind = -300income 当前 = +300符号反了
const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 });
// 空头价格涨应亏损:unwind = income = -300longRatio 对齐后符号一致
expect(u).toBe(i);
});
test('空头:income 应等于 goldlongRatio 后才会 PASS', () => {
const i = PROD_incomeMarkClosePnl({ ...CASE, });
test('空头:income 应等于 goldlongRatio 对齐后一致', () => {
const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 });
const g = GOLD({ ...CASE, positionType: 2 });
expect(i).toBe(g);
});
+11 -11
View File
@@ -29,7 +29,7 @@ describe('swap price precision common wiring', () => {
const configured = loadConfiguredPrecision();
expect(configured.common).toEqual({
amount: { precision: 2, grouping: true },
quantity: { integerDigits: 16, precision: 2, grouping: true },
quantity: { integerDigits: 8, precision: 2, grouping: true },
rate: { precision: 4 }
});
@@ -60,7 +60,7 @@ describe('swap price precision common wiring', () => {
AbroadSpot: 2,
AbroadStock: 2,
AbroadStockIndex: 2,
ExRate: 8,
ExRate: 4,
Shibor: 2,
FixingRepoRate: 2,
RateYield: 2,
@@ -72,12 +72,12 @@ describe('swap price precision common wiring', () => {
expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).precision).toBe(precision);
});
const expectedQuantityIntegerDigits = {
Stock: 12, StockIndex: 12, StockIF: 12, CommodityFutures: 12, CommoditySpot: 12,
NewOtcStock: 12, HKStock: 12, HKStockIndex: 12, Fund: 12, TBFutures: 12,
OtherFutures: 12, GoldFutures: 12, GoldSpot: 12, OtherSpot: 12, AbroadFutures: 12,
AbroadSpot: 12, AbroadStock: 12, AbroadStockIndex: 12, Shibor: 12,
FixingRepoRate: 12, RateYield: 12, BondIndex: 12,
Bond: 16, TBonds: 16, CreditBonds: 16, OtherBonds: 16, ExRate: 16
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);
@@ -87,7 +87,7 @@ describe('swap price precision common wiring', () => {
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(16);
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 }));
@@ -151,11 +151,11 @@ describe('swap price precision common wiring', () => {
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(16);
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(16);
}).getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(8);
});
test('SwapTrade2 common fields use the price helper without changing price responsibilities', () => {
@@ -249,6 +249,9 @@ const vue = new Vue({
this.deal.ClosePercent = this.oriClosePercent;
return;
}
// 调试埋点(?otcdebug=1):记录用户改后的平仓比例,便于定位"改了比例利息腿却不动"的前端入口
if (window.otcDebug) window.otcDebug.log('[unwind] changeClosePercent → 平仓比例=', this.deal.ClosePercent,
' oriClosePercent=', this.oriClosePercent, ' 占期初口径');
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
@@ -366,6 +369,10 @@ const vue = new Vue({
var thisObj = this;
// closePercent 按"占期初(original)"语义(A)传给后端,由 GetUnwindInterestList 转为"占剩余(B)"计算
var postData = { valueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.ValueDate, tradeId: thisObj.deal.SwapTradeId, closePercent: thisObj.deal.ClosePercent, eventType: 2, notionalValue: thisObj.deal.NotionalValue, posiNotionalValue: thisObj.deal.PosiNotionalValue }
// 调试埋点(?otcdebug=1):记录实际发给后端的平仓比例——未来若"改比例利息腿不动",
// 对比此处请求比例 与 下方返回各腿 principal/amount 是否随比例变化,即可定位是前端没传对还是后端没缩放。
if (window.otcDebug) window.otcDebug.log('[unwind] getInterestList → POST closePercent=', thisObj.deal.ClosePercent,
' closeNotionalValue=', thisObj.deal.CloseNotionalValue, ' posiNotionalValue=', thisObj.deal.PosiNotionalValue);
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
thisObj.interestList = resp.obj.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
@@ -373,6 +380,11 @@ const vue = new Vue({
thisObj.marginList = resp.obj.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
// 调试埋点(?otcdebug=1):逐腿打印 mode/principal/amount/rate,定位哪条腿不随平仓比例缩放
// (如 mode=1 固定值腿在 GLMS 缺陷中曾恒为全量、不随比例变化)。
if (window.otcDebug) window.otcDebug.log('[unwind] getInterestList ← 返回利息腿=', thisObj.interestList.map(function (i) {
return { mode: i.InterestMode, principal: i.InterestPrincipal, amount: i.InterestAmount, rate: i.InterestRate };
}));
thisObj.calcCloseAmount();
thisObj.dataFormat();
thisObj.getDivindIn();