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);