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:
@@ -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 回车时故意再派发一次 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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user