From d78d1f48e7efddcf5dec34b48eb32b8d3deae634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Fri, 7 Aug 2026 17:44:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E4=BA=92?= =?UTF-8?q?=E6=8D=A2=E4=BB=B7=E6=A0=BC=E5=8F=98=E5=8A=A8=E6=97=B6=E7=9B=88?= =?UTF-8?q?=E4=BA=8F=E8=AE=A1=E7=AE=97=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在后端计算逻辑中添加 longRatio 参数用于区分多空头寸类型 - 修改前端 JavaScript 代码中的盈亏计算公式,加入 longRatio 参与计算 - 更新单元测试,增加空头头寸价格上涨时亏损的测试用例 - 补充前端测试用例验证空头头寸计算逻辑的准确性 - 修复了空头头寸在价格上涨时显示盈利而非亏损的计算错误 --- .../FrontendCalcCharacterizationTest.cs | 25 +++++++++++++++++++ YLErpDAL/Helpers/FrontendCalcReference.cs | 7 +++--- YLErpWeb/fe-tests/swapCalc.test.js | 12 +++++++++ .../Scripts/app/swaptrade/incomeSwapTrade.js | 3 ++- .../wwwroot/Scripts/app/swaptrade/swapCalc.js | 3 ++- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs index 59b7856f..46acb9bd 100644 --- a/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs +++ b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs @@ -258,6 +258,31 @@ namespace YLErp.Modules.SwapModule AssertDecimalEqual(5355000m, result.FloatPnlSum, 0.01m, "income FloatPnlSum包含分红"); } + [TestMethod] + public void 收取空头_价格上涨_应为亏损() + { + var input = new UnwindInput + { + Multiplier = 100, + PosiGrossPrice = 1.01654321m, + TradingAmountAvg = 101.754321m, + PositionQty = 50000000m, + ContractSize = 1m, + CloseNotionalValue = 50827160.5m, + CloseQty = 0m, + PayDirection = 1, + PositionType = 2, + TradingFee = "0", + TradingFeePending = "0", + DividendIn = "-90400" + }; + + var result = FrontendCalcReference.CalcIncome(input); + + AssertDecimalEqual(-50000m, result.MarkClosePnl, 0.01m, "收取空头价格上涨=盯市亏损"); + AssertDecimalEqual(-140400m, result.FloatPnlSum, 0.01m, "盯市亏损加分红"); + } + private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") { Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, diff --git a/YLErpDAL/Helpers/FrontendCalcReference.cs b/YLErpDAL/Helpers/FrontendCalcReference.cs index 7f9880af..741035dd 100644 --- a/YLErpDAL/Helpers/FrontendCalcReference.cs +++ b/YLErpDAL/Helpers/FrontendCalcReference.cs @@ -97,14 +97,15 @@ namespace YLErp.Helpers decimal entryPrice = input.PosiGrossPrice; decimal scale = input.Multiplier == 100 ? 0.01m : 1m; decimal floatRatio = input.PayDirection == 1 ? 1 : -1; + decimal longRatio = input.PositionType == 1 ? 1 : -1; decimal tradingFee = ParseOrZero(input.TradingFee); decimal tradingFeePending = ParseOrZero(input.TradingFeePending); decimal dividendIn = ParseOrZero(input.DividendIn); - // MarkClosePnl = PositionQty × ContractSize × (TradingAmountAvg × scale − EntryPrice) × floatRatio - // (无 longRatio、无 Math.round/10000) - decimal markClosePnl = input.PositionQty * input.ContractSize * (input.TradingAmountAvg * scale - entryPrice) * floatRatio; + // MarkClosePnl = PositionQty × ContractSize × (TradingAmountAvg × scale − EntryPrice) × floatRatio × longRatio + // (无 Math.round/10000) + decimal markClosePnl = input.PositionQty * input.ContractSize * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio; markClosePnl = StockEqvNotional(markClosePnl); decimal floatPnlSum = decimal.Parse( diff --git a/YLErpWeb/fe-tests/swapCalc.test.js b/YLErpWeb/fe-tests/swapCalc.test.js index ce756586..cf137792 100644 --- a/YLErpWeb/fe-tests/swapCalc.test.js +++ b/YLErpWeb/fe-tests/swapCalc.test.js @@ -308,6 +308,18 @@ describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', expectClose(r.MarkClosePnl, 5400000, 'income MarkClosePnl按数量计算'); expectClose(r.FloatPnlSum, 5355000, 'income FloatPnlSum包含分红'); }); + + test('收取空头价格上涨应为亏损', () => { + const r = SwapCalc.calcIncome({ + multiplier: 100, posiGrossPrice: 1.01654321, tradingAmountAvg: 101.754321, + positionQty: 50000000, contractSize: 1, + closeNotionalValue: 50827160.5, closeQty: 0, + payDirection: 1, positionType: 2, + tradingFee: '0', tradingFeePending: '0', dividendIn: '-90400' + }); + expectClose(r.MarkClosePnl, -50000, '收取空头价格上涨=盯市亏损'); + expectClose(r.FloatPnlSum, -140400, '盯市亏损加分红'); + }); }); // ============================================================================ diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 105f68c9..eb8e50b9 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -197,6 +197,7 @@ 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 TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee); let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending); let DividendIn = thisObj.floatPosition.DividendIn == "" ? 0 : parseFloat(thisObj.floatPosition.DividendIn ?? 0); @@ -204,7 +205,7 @@ const vue = new Vue({ // 债券全价是单位价格,价差盈亏应按持仓数量×合约乘数计算; // CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。 let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1); - thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio; + thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio * longRatio; thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红 // 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留) // 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游 diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js index 859d96b0..90106724 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js @@ -179,6 +179,7 @@ var entryPrice = input.posiGrossPrice; var scale = input.multiplier === 100 ? 0.01 : 1; var floatRatio = input.payDirection === 1 ? 1 : -1; + var longRatio = input.positionType === 1 ? 1 : -1; var tradingFee = parseOrZero(input.tradingFee); var tradingFeePending = parseOrZero(input.tradingFeePending); @@ -187,7 +188,7 @@ var contractSize = input.contractSize === undefined || input.contractSize === null ? 1 : Number(input.contractSize); var markClosePnl = roundHalfAwayFromZero( - input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio, 2); + input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio * longRatio, 2); var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);