From 68a3f0b4fa3de0003adfe843c7cbc71d60fee095 Mon Sep 17 00:00:00 2001 From: hjhan Date: Sat, 8 Aug 2026 12:18:45 +0800 Subject: [PATCH] =?UTF-8?q?debug(swap)+test:=20=E5=B9=B3=E4=BB=93=E5=88=A9?= =?UTF-8?q?=E6=81=AF=20otcdebug=20=E5=9F=8B=E7=82=B9=20+=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D3=E4=B8=AA=E5=89=8D=E7=AB=AF=E7=BA=A2=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- .../fe-tests/bondCalc.integration.test.js | 8 ++++- .../markClosePnlShortConsistency.test.js | 32 ++++++++++--------- YLErpWeb/fe-tests/swapPrecisionConfig.test.js | 22 ++++++------- .../Scripts/app/swaptrade/unwindSwapTrade.js | 12 +++++++ 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/YLErpWeb/fe-tests/bondCalc.integration.test.js b/YLErpWeb/fe-tests/bondCalc.integration.test.js index 1a39b62a..1264c8ed 100644 --- a/YLErpWeb/fe-tests/bondCalc.integration.test.js +++ b/YLErpWeb/fe-tests/bondCalc.integration.test.js @@ -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(''); 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 回车时故意再派发一次 input:event.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', () => { diff --git a/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js b/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js index f829a566..cdddb8a7 100644 --- a/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js +++ b/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js @@ -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。 + * 状态: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")。 + * income 曾缺 longRatio(CalcIncome: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 页 MarkClosePnl(incomeSwapTrade.js:207,无 longRatio) -function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection }) { +// income 页 MarkClosePnl(incomeSwapTrade.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 == income(longRatio 对齐后一致)', () => { const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 2 }); - const i = PROD_incomeMarkClosePnl({ ...CASE, }); - // 空头价格涨应亏损:unwind = -300,income 当前 = +300(符号反了) + const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 }); + // 空头价格涨应亏损:unwind = income = -300(longRatio 对齐后符号一致) expect(u).toBe(i); }); - test('空头:income 应等于 gold(补 longRatio 后才会 PASS)', () => { - const i = PROD_incomeMarkClosePnl({ ...CASE, }); + test('空头:income 应等于 gold(longRatio 对齐后一致)', () => { + const i = PROD_incomeMarkClosePnl({ ...CASE, positionType: 2 }); const g = GOLD({ ...CASE, positionType: 2 }); expect(i).toBe(g); }); diff --git a/YLErpWeb/fe-tests/swapPrecisionConfig.test.js b/YLErpWeb/fe-tests/swapPrecisionConfig.test.js index 42e7dba5..05fc793d 100644 --- a/YLErpWeb/fe-tests/swapPrecisionConfig.test.js +++ b/YLErpWeb/fe-tests/swapPrecisionConfig.test.js @@ -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', () => { diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index a7bd0495..4655829d 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -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();