Merge branch 'glms/feature/1.4.2' of http://git.yiliantech.com/gitlab/otc-dev/zszq-trs into glms/feature/1.4.2
This commit is contained in:
@@ -24,6 +24,10 @@ function makeExpect(actual) {
|
||||
if (actual <= n) { passed++; }
|
||||
else { failed++; failures.push('toBeLessThanOrEqual: ' + fmt(actual) + ' <= ' + n + ' failed'); }
|
||||
},
|
||||
toBeGreaterThan(n) {
|
||||
if (actual > n) { passed++; }
|
||||
else { failed++; failures.push('toBeGreaterThan: ' + fmt(actual) + ' > ' + n + ' failed'); }
|
||||
},
|
||||
toBeGreaterThanOrEqual(n) {
|
||||
if (actual >= n) { passed++; }
|
||||
else { failed++; failures.push('toBeGreaterThanOrEqual: ' + fmt(actual) + ' >= ' + n + ' failed'); }
|
||||
|
||||
@@ -43,6 +43,64 @@ describe('回归守卫:曾出 bug 的纯函数', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 多次部分平仓:平仓比例 ↔ 平仓名义本金 换算必须以"剩余持仓名义本金"为基准
|
||||
// 守卫 unwindSwapTrade.js changeCloseQty/changeClosePercent/changeCloseNotionalValue
|
||||
// 旧 bug:用原始 NotionalValue 换算,首次部分平仓后 PosiNotionalValue 被扣减,
|
||||
// 导致 ClosePercent 偏小 / CloseNotionalValue 偏大,后端预付金返还本金计算错误
|
||||
// ============================================================================
|
||||
describe('多次部分平仓:比例↔名义本金换算以 PosiNotionalValue 为基准', () => {
|
||||
// 场景:原始名义本金 1,000,000,首次平 30% 后剩余 700,000
|
||||
// 第 2 次按名义本金输入 350,000(意图平剩余 50%)
|
||||
const NotionalValue = 1_000_000; // 原始(不变)
|
||||
const PosiNotionalValue = 700_000; // 首次部分平仓后剩余
|
||||
|
||||
test('按名义本金反算比例:应=0.5(用剩余),旧 bug 会算成 0.35(用原始)', () => {
|
||||
const closeNotionalValue = 350_000;
|
||||
const correctPct = SwapCalc.calcClosePercentByRemaining(closeNotionalValue, PosiNotionalValue);
|
||||
const buggyPct = SwapCalc.calcClosePercentByRemaining(closeNotionalValue, NotionalValue);
|
||||
expectClose(correctPct, 0.5, '剩余本金为分母应得 50%');
|
||||
expectClose(buggyPct, 0.35, '旧 bug 用原始本金会算成 0.35');
|
||||
expect(Math.abs(correctPct - buggyPct)).toBeGreaterThan(TOL);
|
||||
});
|
||||
|
||||
test('按比例正算名义本金:应=350,000(用剩余),旧 bug 会算成 500,000(用原始)', () => {
|
||||
const closePercent = 0.5;
|
||||
const correctNotional = SwapCalc.calcCloseNotionalByRemaining(closePercent, PosiNotionalValue);
|
||||
const buggyNotional = SwapCalc.calcCloseNotionalByRemaining(closePercent, NotionalValue);
|
||||
expectClose(correctNotional, 350_000, '剩余本金×50%应=350,000');
|
||||
expectClose(buggyNotional, 500_000, '旧 bug 用原始本金会算成 500,000');
|
||||
expect(Math.abs(correctNotional - buggyNotional)).toBeGreaterThan(TOL);
|
||||
});
|
||||
|
||||
test('第 3 次全平剩余 350,000:比例应=1.0(用剩余),旧 bug 会算成 0.35', () => {
|
||||
const remaining = 350_000;
|
||||
const closeNotionalValue = 350_000;
|
||||
const correctPct = SwapCalc.calcClosePercentByRemaining(closeNotionalValue, remaining);
|
||||
const buggyPct = SwapCalc.calcClosePercentByRemaining(closeNotionalValue, NotionalValue);
|
||||
expectClose(correctPct, 1.0, '剩余全平应=100%');
|
||||
expectClose(buggyPct, 0.35, '旧 bug 用原始本金会算成 0.35');
|
||||
});
|
||||
|
||||
test('多次部分平仓合计本金应=原始名义本金(3 次:30% / 50% / 全平)', () => {
|
||||
// 模拟 3 次部分平仓的换算,每次以"当前剩余"为基准
|
||||
let remaining = 1_000_000;
|
||||
const steps = [0.3, 0.5, 1.0];
|
||||
let totalClosed = 0;
|
||||
steps.forEach((pct) => {
|
||||
const closed = SwapCalc.calcCloseNotionalByRemaining(pct, remaining);
|
||||
totalClosed += closed;
|
||||
remaining -= closed;
|
||||
});
|
||||
expectClose(totalClosed, 1_000_000, '3 次部分平仓合计应=原始名义本金');
|
||||
expectClose(remaining, 0, '剩余应为 0');
|
||||
});
|
||||
|
||||
test('除零保护:剩余本金为 0 时比例返回 0', () => {
|
||||
expect(SwapCalc.calcClosePercentByRemaining(100, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => {
|
||||
// FC_001 平仓-债券多头-默认
|
||||
test('FC_001 平仓 债券多头 默认', () => {
|
||||
|
||||
@@ -65,6 +65,19 @@
|
||||
return roundHalfAwayFromZero(posiGrossPrice * factor, 2);
|
||||
}
|
||||
|
||||
// 平仓名义本金 = 平仓比例 × 剩余持仓名义本金(PosiNotionalValue)
|
||||
// 多次部分平仓后必须用剩余本金 PosiNotionalValue,不能用原始 NotionalValue,否则偏大
|
||||
function calcCloseNotionalByRemaining(closePercent, posiNotionalValue) {
|
||||
return roundHalfAwayFromZero(Number(closePercent) * Number(posiNotionalValue), 2);
|
||||
}
|
||||
|
||||
// 平仓比例 = 平仓名义本金 / 剩余持仓名义本金(PosiNotionalValue)
|
||||
// 多次部分平仓后必须以剩余本金为分母,否则比例偏小,导致后端预付金返还本金计算错误
|
||||
function calcClosePercentByRemaining(closeNotionalValue, posiNotionalValue) {
|
||||
if (Number(posiNotionalValue) === 0) return 0;
|
||||
return roundHalfAwayFromZero(Number(closeNotionalValue) / Number(posiNotionalValue), 6);
|
||||
}
|
||||
|
||||
// 盯市平仓盈亏(unwind):CloseQty × (期末全价×scale − 期初全价) × floatRatio × longRatio
|
||||
// 对齐 FrontendCalcReference.CalcUnwind:先 ×10000 取整再 ÷10000,最后 toFixed(2)
|
||||
// 干净输入下等价于直接 round(.., 2)
|
||||
@@ -164,6 +177,8 @@
|
||||
roundMoney: roundMoney,
|
||||
calcFloatPnlSum: calcFloatPnlSum,
|
||||
calcStockEqvNotional: calcStockEqvNotional,
|
||||
calcCloseNotionalByRemaining: calcCloseNotionalByRemaining,
|
||||
calcClosePercentByRemaining: calcClosePercentByRemaining,
|
||||
calcMarkClosePnl: calcMarkClosePnl,
|
||||
calcUnwind: calcUnwind,
|
||||
calcIncome: calcIncome
|
||||
|
||||
@@ -17,7 +17,9 @@ const vue = new Vue({
|
||||
marginList: [],
|
||||
initPosiNetPrice: 0,
|
||||
multiplier: 1,
|
||||
oriClosePercent: model.ClosePercent,
|
||||
// 多次部分平仓后,ClosePercent 语义为“占剩余持仓的比例”(后端 GetUnwindInterests 用 remainingBase×closePercent 计算预付金返还),
|
||||
// 故最多可平 100% 剩余,oriClosePercent 恒为 1;不能用 model.ClosePercent(=剩余/原始,Definition A 旧口径),否则全部平仓/按比例会少返预付金
|
||||
oriClosePercent: 1,
|
||||
ratio: 1,
|
||||
shortRatio: 1,
|
||||
},
|
||||
@@ -53,6 +55,12 @@ const vue = new Vue({
|
||||
this.ratio = this.floatPosition.PayDirection == 1 ? -1 : 1;
|
||||
this.shortRatio = this.floatPosition.PositionType == 1 ? 1 : -1;
|
||||
this.TradeStartDate = model.TradeStartDate;
|
||||
// 多次部分平仓后 ClosePercent 语义为"占剩余持仓比例"。
|
||||
// 仅当"全部平仓"(CloseMethod==1) 时修正旧口径(model.ClosePercent 可能=剩余/原始<1)为 1;
|
||||
// "部分平仓"(CloseMethod==2) 时保留已提交比例(平仓待复核场景),避免覆盖用户已提交的 closePercent
|
||||
if (this.deal.CloseMethod === 1) {
|
||||
this.deal.ClosePercent = 1;
|
||||
}
|
||||
// 转换期末标的价格为百分比形式
|
||||
if (this.floatPosition.TradingAmountAvg) {
|
||||
this.floatPosition.TradingAmountAvg = this.floatPosition.TradingAmountAvg * this.multiplier;
|
||||
@@ -146,7 +154,8 @@ const vue = new Vue({
|
||||
} else {
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
// 多次部分平仓后 PosiNotionalValue 才是剩余本金,不能用原始 NotionalValue,否则平仓名义本金偏大
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.PosiNotionalValue));
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
@@ -158,7 +167,8 @@ const vue = new Vue({
|
||||
return;
|
||||
}
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
// 多次部分平仓后 PosiNotionalValue 才是剩余本金,不能用原始 NotionalValue,否则平仓名义本金偏大
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.PosiNotionalValue));
|
||||
if (parseFloat(this.deal.CloseNotionalValue) == parseFloat(this.deal.PosiNotionalValue)) {
|
||||
this.floatPosition.CloseMethod = 1;
|
||||
} else {
|
||||
@@ -174,7 +184,8 @@ const vue = new Vue({
|
||||
this.deal.CloseNotionalValue = this.deal.PosiNotionalValue;
|
||||
return;
|
||||
}
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseNotionalValue) / parseFloat(this.deal.NotionalValue));
|
||||
// 多次部分平仓后应以 PosiNotionalValue(剩余) 为分母,否则 ClosePercent 偏小,导致后端预付金返还本金计算错误
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseNotionalValue) / parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
|
||||
@@ -432,7 +432,6 @@
|
||||
function __onInput() {
|
||||
if (_ctrlV) {
|
||||
_ctrlV = false;
|
||||
console.log('[numberInput paste] raw=', this.value, 'append=', _options.append, 'percent=', _options.percent);
|
||||
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
|
||||
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
|
||||
let f = '';
|
||||
@@ -441,7 +440,6 @@
|
||||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||||
else if (_options.append === '‱') f /= 10000;
|
||||
}
|
||||
console.log('[numberInput paste] parsed=', f);
|
||||
return setValue(f);
|
||||
}
|
||||
if (_chnInput >= 0) {
|
||||
@@ -464,7 +462,6 @@
|
||||
|
||||
//输入完成处理
|
||||
function __change() {
|
||||
console.log('[numberInput change] raw=', this.value, 'append=', _options.append, 'percent=', _options.percent);
|
||||
let f = '';
|
||||
if (this.value && this.value !== _options.append) {
|
||||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||||
@@ -472,7 +469,6 @@
|
||||
else if (_options.append === '‱') f /= 10000;
|
||||
|
||||
}
|
||||
console.log('[numberInput change] parsed=', f, 'value=', this.value);
|
||||
_options.onchange && _options.onchange(f, this.value);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user