fix 部分平仓 选择切换 会导致不一致,浮动端会错误
This commit is contained in:
@@ -101,6 +101,66 @@ describe('多次部分平仓:比例↔名义本金换算以 PosiNotionalValue
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 多次部分平仓:全部↔部分切换时 CloseQty 不应随 ClosePercent 口径变化而跳变
|
||||
// 守卫 unwindSwapTrade.js changeCloseMethod/changeClosePercent/changeCloseQty/changeCloseNotionalValue
|
||||
// 旧 bug:ClosePercent 是占期初口径(A),但 CloseQty=PositionQty*ClosePercent 错当占剩余(B)用
|
||||
// 全部(ClosePercent=0.7,CloseQty=720000) → 部分(ClosePercent没变, CloseQty=504000) ❌ 跳变
|
||||
// 修复:CloseQty = PositionQty × (ClosePercent / oriClosePercent)
|
||||
// ============================================================================
|
||||
describe('多次部分平仓:全部↔部分切换 CloseQty 不跳变(占期初口径自洽)', () => {
|
||||
// 场景基于 GLMS-20260701-0013:
|
||||
// 期初 NotionalValue=980000,已平两次剩 PosiNotionalValue=686000, PositionQty=720000
|
||||
// oriClosePercent = 686000/980000 = 0.7
|
||||
const NotionalValue = 980000;
|
||||
const PosiNotionalValue = 686000;
|
||||
const PositionQty = 720000;
|
||||
const oriClosePercent = PosiNotionalValue / NotionalValue; // 0.7
|
||||
|
||||
test('全部平仓 ClosePercent=0.7 → CloseQty=720000(剩余全部)', () => {
|
||||
const closePercent = oriClosePercent; // 全部平仓 = oriClosePercent
|
||||
const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, PositionQty);
|
||||
expectClose(closeQty, 720000, '全部平仓应平剩余全部数量');
|
||||
});
|
||||
|
||||
test('全部→部分切换(ClosePercent 没变=0.7):CloseQty 应保持 720000 不跳变', () => {
|
||||
// 旧 bug:CloseQty = 720000 * 0.7 = 504000(错误跳变)
|
||||
// 修复:CloseQty = 720000 * (0.7/0.7) = 720000(不变)
|
||||
const closePercent = oriClosePercent;
|
||||
const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, PositionQty);
|
||||
const buggyCloseQty = Math.round(PositionQty * closePercent * 100) / 100; // 旧 bug 公式
|
||||
expectClose(closeQty, 720000, '修复后应不变');
|
||||
expect(Math.abs(buggyCloseQty - 504000)).toBeLessThanOrEqual(TOL, '旧 bug 会跳变到 504000');
|
||||
expect(Math.abs(closeQty - buggyCloseQty)).toBeGreaterThan(TOL, '修复与旧 bug 必须有差异');
|
||||
});
|
||||
|
||||
test('改比例 0.5(占期初)→ CloseQty=720000×(0.5/0.7)=514285.71', () => {
|
||||
const closePercent = 0.5;
|
||||
const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, PositionQty);
|
||||
expectClose(closeQty, 514285.71, '占期初 50% 对应占剩余 71.43%,×720000=514285.71');
|
||||
});
|
||||
|
||||
test('逆运算:CloseQty=514285.71 → ClosePercent=0.5(占期初)', () => {
|
||||
const closeQty = 514285.71;
|
||||
const closePercent = SwapCalc.calcOriginalClosePercentByQty(closeQty, PositionQty, oriClosePercent);
|
||||
expectClose(closePercent, 0.5, '逆运算应还原为占期初 0.5');
|
||||
});
|
||||
|
||||
test('逆运算自洽:CloseQty=720000(=PositionQty)→ ClosePercent=0.7(=oriClosePercent)', () => {
|
||||
const closeQty = PositionQty; // 720000
|
||||
const closePercent = SwapCalc.calcOriginalClosePercentByQty(closeQty, PositionQty, oriClosePercent);
|
||||
expectClose(closePercent, oriClosePercent, '平全部剩余 → ClosePercent=oriClosePercent');
|
||||
});
|
||||
|
||||
test('除零保护:oriClosePercent=0(已全平完)→ CloseQty=0', () => {
|
||||
expect(SwapCalc.calcCloseQtyByOriginalPercent(0.5, 0, 720000)).toBe(0);
|
||||
});
|
||||
|
||||
test('除零保护:PositionQty=0 → ClosePercent=0', () => {
|
||||
expect(SwapCalc.calcOriginalClosePercentByQty(100, 0, oriClosePercent)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => {
|
||||
// FC_001 平仓-债券多头-默认
|
||||
test('FC_001 平仓 债券多头 默认', () => {
|
||||
|
||||
@@ -78,6 +78,26 @@
|
||||
return roundHalfAwayFromZero(Number(closeNotionalValue) / Number(posiNotionalValue), 6);
|
||||
}
|
||||
|
||||
// 平仓数量(占期初口径 A):CloseQty = PositionQty × (closePercent / oriClosePercent)
|
||||
// closePercent 是"占期初名义本金比例"(A),需先除以 oriClosePercent(=剩余/期初) 转成"占剩余比例"(B),
|
||||
// 再乘以剩余持仓数量 PositionQty。
|
||||
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)。
|
||||
// 除零保护:oriClosePercent=0(剩余为0,已全部平完)时返回 0。
|
||||
function calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
|
||||
var ori = Number(oriClosePercent);
|
||||
if (ori === 0) return 0;
|
||||
return roundHalfAwayFromZero(Number(positionQty) * (Number(closePercent) / ori), 2);
|
||||
}
|
||||
|
||||
// 平仓比例(占期初口径 A)= (CloseQty / PositionQty) × oriClosePercent
|
||||
// CloseQty/PositionQty 得到"占剩余比例"(B),乘以 oriClosePercent(=剩余/期初) 转成"占期初比例"(A)。
|
||||
// 除零保护:PositionQty=0 时返回 0。
|
||||
function calcOriginalClosePercentByQty(closeQty, positionQty, oriClosePercent) {
|
||||
var qty = Number(positionQty);
|
||||
if (qty === 0) return 0;
|
||||
return roundHalfAwayFromZero((Number(closeQty) / qty) * Number(oriClosePercent), 6);
|
||||
}
|
||||
|
||||
// 盯市平仓盈亏(unwind):CloseQty × (期末全价×scale − 期初全价) × floatRatio × longRatio
|
||||
// 对齐 FrontendCalcReference.CalcUnwind:先 ×10000 取整再 ÷10000,最后 toFixed(2)
|
||||
// 干净输入下等价于直接 round(.., 2)
|
||||
@@ -179,6 +199,8 @@
|
||||
calcStockEqvNotional: calcStockEqvNotional,
|
||||
calcCloseNotionalByRemaining: calcCloseNotionalByRemaining,
|
||||
calcClosePercentByRemaining: calcClosePercentByRemaining,
|
||||
calcCloseQtyByOriginalPercent: calcCloseQtyByOriginalPercent,
|
||||
calcOriginalClosePercentByQty: calcOriginalClosePercentByQty,
|
||||
calcMarkClosePnl: calcMarkClosePnl,
|
||||
calcUnwind: calcUnwind,
|
||||
calcIncome: calcIncome
|
||||
|
||||
@@ -131,12 +131,20 @@ const vue = new Vue({
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseQty = this.deal.PositionQty;
|
||||
} else {
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
// ClosePercent 是占期初口径(A),需除以 oriClosePercent 转占剩余(B) 再乘剩余数量
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
// 按"占期初口径(A)"的 ClosePercent 反算平仓数量:CloseQty = PositionQty × (ClosePercent / oriClosePercent)
|
||||
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)
|
||||
calcCloseQtyByPercent(closePercent) {
|
||||
var ori = parseFloat(this.oriClosePercent) || 0;
|
||||
var ratio = ori > 0 ? parseFloat(closePercent) / ori : 0;
|
||||
return otcformat.trading.notional(parseFloat(this.deal.PositionQty) * ratio);
|
||||
},
|
||||
calcTradingFeePending() {
|
||||
this.floatPosition.TradingFeePending = this.floatPosition.BeforeCloseFee * parseFloat(this.deal.ClosePercent);
|
||||
},
|
||||
@@ -145,7 +153,9 @@ const vue = new Vue({
|
||||
main.message("平仓数量不能超过持仓数量");
|
||||
return;
|
||||
}
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseQty) / parseFloat(this.deal.PositionQty));
|
||||
// CloseQty/PositionQty 得占剩余(B),× oriClosePercent 转回占期初(A)
|
||||
var ori = parseFloat(this.oriClosePercent) || 0;
|
||||
this.deal.ClosePercent = otcformat.fixed6((parseFloat(this.deal.CloseQty) / parseFloat(this.deal.PositionQty)) * ori);
|
||||
if (parseFloat(this.deal.CloseQty) == parseFloat(this.deal.PositionQty)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
@@ -163,13 +173,13 @@ const vue = new Vue({
|
||||
this.deal.ClosePercent = this.oriClosePercent;
|
||||
return;
|
||||
}
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
if (parseFloat(this.deal.CloseNotionalValue) == parseFloat(this.deal.PosiNotionalValue)) {
|
||||
this.floatPosition.CloseMethod = 1;
|
||||
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
this.floatPosition.CloseMethod = 2;
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
@@ -183,7 +193,12 @@ const vue = new Vue({
|
||||
}
|
||||
// 占期初口径:平仓比例 = 平仓名义本金 / 期初名义本金(NotionalValue)
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseNotionalValue) / parseFloat(this.deal.NotionalValue));
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
|
||||
Reference in New Issue
Block a user