diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml index c1fdaf8d..353c255e 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml @@ -27,6 +27,7 @@ + }
diff --git a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js index 2fb4a7eb..5d31102f 100644 --- a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js +++ b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js @@ -155,6 +155,8 @@ function loadVueApp(model, mockPost) { roundExitYtm(v) { return v; }, applyManualEdit(s) { return s; } }, + // 收付方向符号:纯函数零依赖,直接喂真实模块(映射冻结由 unwindLegSign.test.js 覆盖) + UnwindLegSign: require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js'), _: { round(value, precision) { return Number(Number(value || 0).toFixed(precision || 0)); diff --git a/YLErpWeb/fe-tests/unwindLegSign.test.js b/YLErpWeb/fe-tests/unwindLegSign.test.js new file mode 100644 index 00000000..3258511c --- /dev/null +++ b/YLErpWeb/fe-tests/unwindLegSign.test.js @@ -0,0 +1,47 @@ +/** + * unwindLegSign.test.js — 平仓页收付方向符号映射(隐式约定显式化) + * ============================================================================ + * 冻结的领域事实(2026-08-21 业务确认): + * 利息腿(融资成本)与保证金腿(返息/返还本金)方向【必须相反】—— + * 利息是买方持有标的向交易商融资的成本(买方付出去的钱); + * 保证金是客户自己交的抵押金,返息/返还是把客户自己的钱退回来。 + * 两者对同一 InterestDirection 枚举符号互为镜像,这是业务事实而非笔误, + * 任何人"顺手统一"这两个符号都会翻转保证金返还方向(资损级 bug)。 + * + * 另冻结:PayDirection==1 → +1、PositionType==1(多头) → +1 的浮动端/多空符号。 + * + * 运行:cd YLErpWeb/fe-tests && npx jest unwindLegSign + */ +const UnwindLegSign = require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js'); + +describe('收付方向符号:各枚举映射', () => { + test('利息盈亏(利息腿/保证金腿通用):收取(1)→+1,支付(其他)→-1', () => { + expect(UnwindLegSign.interestPnlSign(1)).toBe(1); + expect(UnwindLegSign.interestPnlSign(0)).toBe(-1); + expect(UnwindLegSign.interestPnlSign(null)).toBe(-1); + }); + + test('保证金腿返还本金:与利息腿同枚举反号(返的是客户自己的钱)', () => { + expect(UnwindLegSign.marginRebatePrincipalSign(1)).toBe(-1); + expect(UnwindLegSign.marginRebatePrincipalSign(0)).toBe(1); + expect(UnwindLegSign.marginRebatePrincipalSign(null)).toBe(1); + }); + + test('浮动端收付:PayDirection==1(收取)→+1;多空:PositionType==1(多头)→+1', () => { + expect(UnwindLegSign.payDirectionSign(1)).toBe(1); + expect(UnwindLegSign.payDirectionSign(0)).toBe(-1); + expect(UnwindLegSign.positionTypeSign(1)).toBe(1); + expect(UnwindLegSign.positionTypeSign(0)).toBe(-1); + }); +}); + +describe('不变量:利息腿与保证金腿符号互为镜像(业务事实,禁止统一)', () => { + test('同一 InterestDirection 下两符号之和恒为 0', () => { + [1, 0, null, undefined, 2].forEach(dir => { + expect( + UnwindLegSign.interestPnlSign(dir) + + UnwindLegSign.marginRebatePrincipalSign(dir) + ).toBe(0); + }); + }); +}); diff --git a/YLErpWeb/fe-tests/unwindSwapTrade.test.js b/YLErpWeb/fe-tests/unwindSwapTrade.test.js index f0774f96..488a0c3b 100644 --- a/YLErpWeb/fe-tests/unwindSwapTrade.test.js +++ b/YLErpWeb/fe-tests/unwindSwapTrade.test.js @@ -66,6 +66,8 @@ function loadUnwindHelpers() { roundHalfAwayFromZero(value) { return value; }, calcCloseQtyByOriginalPercent() { return 0; } }, + // 收付方向符号:纯函数零依赖,直接喂真实模块(映射冻结由 unwindLegSign.test.js 覆盖) + UnwindLegSign: require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js'), _: { round(value, precision) { return Number(Number(value || 0).toFixed(precision || 0)); diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js new file mode 100644 index 00000000..a07d62cd --- /dev/null +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js @@ -0,0 +1,45 @@ +/** + * unwindLegSign.js — 平仓页收付方向符号(纯函数) + * ============================================================================ + * 把散落在 unwindSwapTrade.js 各计算式里的裸三元(xxx==1 ? ±1 : ∓1)显式化为命名函数。 + * 【为什么必须显式化】利息盈亏与保证金返还本金对同一 InterestDirection 枚举符号相反—— + * 这是业务事实而非笔误,任何"顺手统一"都会翻转保证金返还方向(资损级 bug): + * 利息盈亏(利息腿/保证金腿通用,changeInterestAmount 对两类行都生效): + * 收益互换中买方为持有标的向交易商融资,利息=融资成本(买方付出去的钱), + * InterestDirection==1(收取) → 盈亏记 +1; + * 保证金返还本金:保证金是客户自己交的抵押金,返息/返还本金是把客户自己的钱退回来, + * 本金流方向与利息(成本)相反 → 同枚举符号取镜像 -1。 + * + * 其余两个符号:PayDirection==1(浮动端收取) → +1;PositionType==1(多头) → +1。 + * + * 加载:浏览器 script 标签(先于 unwindSwapTrade.js);Node:module.exports 供 jest。 + */ +var UnwindLegSign = (function () { + 'use strict'; + + // Number()===1 与页面原 ==1 对 '1'/true/数字等实际入参等价,且对 null/undefined 同样落到 -1 分支 + function interestPnlSign(interestDirection) { + return Number(interestDirection) === 1 ? 1 : -1; + } + + function marginRebatePrincipalSign(interestDirection) { + return Number(interestDirection) === 1 ? -1 : 1; + } + + function payDirectionSign(payDirection) { + return Number(payDirection) === 1 ? 1 : -1; + } + + function positionTypeSign(positionType) { + return Number(positionType) === 1 ? 1 : -1; + } + + return Object.freeze({ + interestPnlSign: interestPnlSign, + marginRebatePrincipalSign: marginRebatePrincipalSign, + payDirectionSign: payDirectionSign, + positionTypeSign: positionTypeSign + }); +}()); + +if (typeof module === 'object' && module.exports) module.exports = UnwindLegSign; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 764e9623..40b5da7c 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -69,8 +69,6 @@ const vue = new Vue({ // 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。 // oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。 oriClosePercent: 1, - ratio: 1, - shortRatio: 1, }, computed: { maxUnwindDate() { @@ -125,8 +123,6 @@ const vue = new Vue({ this.marginList = model.FlowEvents.filter((item) => { return item.InterestMode == 5 || item.InterestMode == 6; }); - this.ratio = this.floatPosition.PayDirection == 1 ? -1 : 1; - this.shortRatio = this.floatPosition.PositionType == 1 ? 1 : -1; this.TradeStartDate = model.TradeStartDate; // 最多可平比例(占期初口径) = 剩余名义本金 / 期初名义本金;分母为 0 时兜底为 1 this.oriClosePercent = (this.deal.NotionalValue && this.deal.PosiNotionalValue) @@ -424,8 +420,8 @@ const vue = new Vue({ }, calcFloatClosePnl() {//计算浮动端平仓盈亏 var thisObj = this; - let floatRatio = thisObj.floatPosition.PayDirection == 1 ? 1 : -1; - let longRatio = thisObj.floatPosition.PositionType == 1 ? 1 : -1; + let floatRatio = UnwindLegSign.payDirectionSign(thisObj.floatPosition.PayDirection); + let longRatio = UnwindLegSign.positionTypeSign(thisObj.floatPosition.PositionType); let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending); let deliveryPrice = thisObj.getStorageDeliveryPrice(); @@ -440,7 +436,7 @@ const vue = new Vue({ this.calcFloatClosePnl(); }, changeInterestAmount(item) {//修改利息金额 - let interestRatio = item.InterestDirection == 1 ? 1 : -1; + let interestRatio = UnwindLegSign.interestPnlSign(item.InterestDirection); item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio); this.calcCloseAmount(); }, @@ -450,8 +446,8 @@ const vue = new Vue({ // this.calcCloseAmount(); //}, calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付 - let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; - let ratio = this.floatPosition.PositionType == 1 ? 1 : -1; + let floatRatio = UnwindLegSign.payDirectionSign(this.floatPosition.PayDirection); + let ratio = UnwindLegSign.positionTypeSign(this.floatPosition.PositionType); let thisObj = this; let pnl = parseFloat(this.floatPosition.FloatPnlSum); let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); @@ -468,14 +464,14 @@ const vue = new Vue({ thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * ratio; } this.interestList.forEach(x => { - /*let interestRatio = x.InterestDirection == 1 ? 1 : -1;*/ let interestAmount = parseFloat(x.InterestClosePnL); thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount; thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount; }); this.marginList.forEach(x => { let interestAmount = parseFloat(x.InterestClosePnL); - let interestRatio = x.InterestDirection == 1 ? -1 : 1; + // 保证金返还本金符号与利息盈亏同枚举反号(见 unwindLegSign.js 头注——业务事实勿统一) + let interestRatio = UnwindLegSign.marginRebatePrincipalSign(x.InterestDirection); thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount; thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount; thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;