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;