diff --git a/UnitTestProject/Modules/SwapModule/InitUnwindTradingFeeTest.cs b/UnitTestProject/Modules/SwapModule/InitUnwindTradingFeeTest.cs index 8691bb11..f9edd583 100644 --- a/UnitTestProject/Modules/SwapModule/InitUnwindTradingFeeTest.cs +++ b/UnitTestProject/Modules/SwapModule/InitUnwindTradingFeeTest.cs @@ -17,6 +17,17 @@ namespace YLErp.Modules.SwapModule return (decimal)method.Invoke(null, new object[] { position, unwindData }); } + private static decimal InvokeCalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData) + { + var method = typeof(SwapDealService).GetMethod( + "CalcInitTradingFeePending", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.IsNotNull(method, "CalcInitTradingFeePending was not found"); + + return (decimal)method.Invoke(null, new object[] { oriPosition, position, unwindData }); + } + [TestMethod] public void 百分比模式_按平仓名义本金计算并四舍五入到两位() { @@ -61,5 +72,49 @@ namespace YLErp.Modules.SwapModule Assert.AreEqual(0m, InvokeCalcInitTradingFee(null, new UnwindData())); Assert.AreEqual(0m, InvokeCalcInitTradingFee(new swap_position(), null)); } + + [TestMethod] + public void BaseRatePendingFeeUsesTheSameActualCloseAmountAsCloseFee() + { + var oriPosition = new swap_position + { + PosiFeeType = 1, + PosiTradingFeeUnit = 0.2m + }; + oriPosition.PosiTradingFeePending = 2000m; + var position = new swap_position { PosiTradingFeePending = 840m }; + var unwindData = new UnwindData { NotionalQty = 10000m, CloseQty = 3000m, CloseNotionalValue = 4200m }; + + var fee = InvokeCalcInitTradingFeePending(oriPosition, position, unwindData); + + Assert.AreEqual(600m, fee); + } + + [TestMethod] + public void BaseRatePendingFeeAllocatesManuallyAdjustedOriginalPendingFee() + { + var oriPosition = new swap_position + { + PosiFeeType = 1, + PosiTradingFeeUnit = 0.2m, + PosiTradingFeePending = 1500m + }; + var unwindData = new UnwindData { NotionalQty = 10000m, CloseQty = 3000m }; + + var fee = InvokeCalcInitTradingFeePending(oriPosition, new swap_position(), unwindData); + + Assert.AreEqual(450m, fee); + } + + [TestMethod] + public void LegacyPendingFeeKeepsCurrentPositionValueWhenNoBaseRateIsConfigured() + { + var oriPosition = new swap_position { PosiTradingFeeUnit = 0m }; + var position = new swap_position { PosiTradingFeePending = 840m }; + + var fee = InvokeCalcInitTradingFeePending(oriPosition, position, new UnwindData()); + + Assert.AreEqual(840m, fee); + } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7bf7f637..875533e1 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -306,7 +306,7 @@ namespace YLErp.Modules.SwapModule floatEvent.ContractSize = position.ContractSize; floatEvent.TradingAmount = floatEvent.Quantity * floatEvent.ContractSize; var ratio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? -1m : 1m; - floatEvent.TradingFeePending = position.PosiTradingFeePending; + floatEvent.TradingFeePending = CalcInitTradingFeePending(oriPosition, position, unwindData); floatEvent.DataState = (int)SwapFlowDateStateEnum.完成; floatEvent.InterestMode = position.InterestMode; floatEvent.ClientId = td.ClientId; @@ -330,6 +330,23 @@ namespace YLErp.Modules.SwapModule return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); } + + private static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData) + { + if (oriPosition == null || unwindData == null || oriPosition.PosiTradingFeeUnit == 0) + { + return position?.PosiTradingFeePending ?? 0; + } + + var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue; + var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue; + if (originalBase <= 0) + { + return position?.PosiTradingFeePending ?? 0; + } + + return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + } /// /// 校验上日是否收盘 /// diff --git a/YLErpWeb/fe-tests/unwindSwapTrade.test.js b/YLErpWeb/fe-tests/unwindSwapTrade.test.js index b977b41e..2cd6472e 100644 --- a/YLErpWeb/fe-tests/unwindSwapTrade.test.js +++ b/YLErpWeb/fe-tests/unwindSwapTrade.test.js @@ -90,3 +90,31 @@ describe('unwindSwapTrade 基础费率计算', () => { expectClose(result, 200.00); }); }); + +describe('base-rate pending trading fee', () => { + const { swapPosiFeeCalc, consPosiFeeType } = loadUnwindHelpers(); + + test('unit rate follows actual close quantity instead of an inconsistent close percent', () => { + const result = swapPosiFeeCalc.calcTradingFeePending( + 2000, consPosiFeeType.Unit, 0.2, 4200, 3000, 10000, 10000, 0.42); + expectClose(result, 600.00); + }); + + test('percentage rate follows actual close notional value', () => { + const result = swapPosiFeeCalc.calcTradingFeePending( + 2000, consPosiFeeType.Percent, 0.2, 300000, 4200, 1000000, 10000, 0.42); + expectClose(result, 600.00); + }); + + test('a manually adjusted original pending fee is allocated by actual close quantity', () => { + const result = swapPosiFeeCalc.calcTradingFeePending( + 1500, consPosiFeeType.Unit, 0.2, 4200, 3000, 10000, 10000, 0.42); + expectClose(result, 450.00); + }); + + test('without a configured base rate, the legacy close-percent calculation remains', () => { + const result = swapPosiFeeCalc.calcTradingFeePending( + 2000, consPosiFeeType.Percent, 0, 300000, 3000, 1000000, 10000, 0.42); + expectClose(result, 840.00); + }); +}); diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 554329ac..b547887e 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -22,6 +22,19 @@ const swapPosiFeeCalc = { ? normalizedFeeUnit * normalizedCloseQty : normalizedFeeUnit / 100 * normalizedCloseNotionalValue; return otcformat.trading.StockEqvNotional(_.round(tradingFee, 2)); + }, + calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) { + const normalizedFeeUnit = Number(feeUnit) || 0; + if (normalizedFeeUnit !== 0) { + const normalizedFeeType = this.normalizeFeeType(feeType); + const closeBase = normalizedFeeType === consPosiFeeType.Unit ? Number(closeQty) || 0 : Number(closeNotionalValue) || 0; + const originalBase = normalizedFeeType === consPosiFeeType.Unit ? Number(notionalQty) || 0 : Number(notionalValue) || 0; + if (originalBase > 0) { + return otcformat.trading.StockEqvNotional(_.round((Number(beforeCloseFee) || 0) * closeBase / originalBase, 2)); + } + } + + return (Number(beforeCloseFee) || 0) * (Number(closePercent) || 0); } }; let ValueDate = model.ValueDate; @@ -168,7 +181,15 @@ const vue = new Vue({ return SwapCalc.calcCloseQtyByOriginalPercent(closePercent, this.oriClosePercent, this.deal.PositionQty); }, calcTradingFeePending() { - this.floatPosition.TradingFeePending = this.floatPosition.BeforeCloseFee * parseFloat(this.deal.ClosePercent); + this.floatPosition.TradingFeePending = swapPosiFeeCalc.calcTradingFeePending( + this.floatPosition.BeforeCloseFee, + this.floatPosition.PosiFeeType, + this.floatPosition.PosiTradingFeeUnit, + this.deal.CloseNotionalValue, + this.deal.CloseQty, + this.deal.NotionalValue, + this.deal.NotionalQty, + this.deal.ClosePercent); }, refreshTradingFeeByUnit() { this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(