diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs index 7d1dfc27..dd7726cb 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs @@ -237,5 +237,26 @@ namespace YLErp.Modules.SwapModule Assert.AreEqual("确认成交", td.TradeStatus, "部分平仓 TradeStatus 保持不变"); Console.WriteLine($"UW_008: A=0.3→B={service.SaveSwapDealCalls[0].data.ClosePercent}, HasPartialUnWind={td.HasPartialUnWind} ✅"); } + + [TestMethod] + public void UW_009_SwapUnwind_名义本金写入前舍入两位小数() + { + var td = SwapDealTestFactory.CreateTrade(); + td.StockEqvNotional = 1000000.006; + var service = new TestableSwapDealService(td); + var unwindData = SwapDealTestFactory.CreateUnwindData( + swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum.部分平仓, closePercent: 0.5m, + closeQty: 5000m, closeNotionalValue: 500000.004m, positionQty: 10000m); + unwindData.NotionalValue = 1000000.006m; + unwindData.PosiNotionalValue = 1000000.006m; + + service.SwapUnwind(unwindData); + + var savedData = service.SaveSwapDealCalls[0].data; + Assert.AreEqual(1000000.01m, savedData.NotionalValue, "期初名义本金应按两位小数写入事件"); + Assert.AreEqual(1000000.01m, savedData.PosiNotionalValue, "剩余名义本金应按两位小数写入事件"); + Assert.AreEqual(500000.00m, savedData.CloseNotionalValue, "平仓名义本金应按两位小数写入事件"); + Assert.AreEqual(500000.01, td.StockEqvNotional, 0.000001, "trade 剩余名义本金应在扣减后舍入两位小数"); + } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 6b0d5de6..58fbae31 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -36,9 +36,17 @@ namespace YLErp.Modules.SwapModule /// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。 protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) { + NormalizeNotionalValues(unwindData); return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve); } + private static void NormalizeNotionalValues(UnwindData unwindData) + { + unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + } + /// 保存所有变更(生产: DbContext.SaveChanges;测试: 空操作) protected virtual void SaveAllChanges() { @@ -1222,6 +1230,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } + NormalizeNotionalValues(unwindData); //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易 // 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。 @@ -1252,7 +1261,7 @@ namespace YLErp.Modules.SwapModule td.HasPartialUnWind = 1; } td.UnWindDate = unwindData.UnwindDate; - td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue); + td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty); SaveAllChanges(); cofirm = true; @@ -1568,7 +1577,7 @@ namespace YLErp.Modules.SwapModule td.HasPartialUnWind = 1; } td.UnWindDate = unwindData.UnwindDate; - td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue); + td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty); td.Notional = td.TradeAmount; td.OptDate = DateTime.Now; @@ -1767,7 +1776,7 @@ namespace YLErp.Modules.SwapModule td.UnWindDate = swapEvent.unwindData.UnwindDate; if (eventType != (int)SwapEventTypeEnum.互换) { - td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue); + td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty); } @@ -1933,7 +1942,7 @@ namespace YLErp.Modules.SwapModule { // 平仓时才扣减持仓 position.PosiQuantity -= unwindData.CloseQty; - position.PosiNotionalValue -= unwindData.CloseNotionalValue; + position.PosiNotionalValue = Math.Round(position.PosiNotionalValue - unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent; position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent; } diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 55f0f88c..6965a924 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -37,6 +37,7 @@ namespace YLErp.Modules.SwapModule /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) protected virtual void PersistEodSwapPosition(eod_swap_position position) { + position.PosiNotionalValue = Math.Round(position.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (position.id == 0) { DbContext.eod_swap_position.Add(position); @@ -1949,9 +1950,9 @@ namespace YLErp.Modules.SwapModule var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 // 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模, // 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。 - eod_Swap.NotionalValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue); - eod_Swap.NotionalValueShort = -Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)); - eod_Swap.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional); + eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); eod_Swap.SwapTradeId = td.id; eod_Swap.SwapTradeNo = td.TradeNumber; eod_Swap.ClientId = td.ClientId; @@ -2026,9 +2027,9 @@ namespace YLErp.Modules.SwapModule var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).ToList(); var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿 var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 - eod_Swap.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional); - eod_Swap.NotionalValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue); - eod_Swap.NotionalValueShort = -Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)); + eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs index 90a018be..bfc2e45d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs @@ -62,7 +62,7 @@ namespace YLErp.Modules.SwapModule position.PosiGrossPrice = eodPayPosition.PosiGrossPrice; position.PosiNetFeePrice = eodPayPosition.PosiNetFeePrice; position.PosiNetNoFeePrice = eodPayPosition.PosiNetNoFeePrice; - position.PosiNotionalValue = eodPayPosition.PosiNotionalValue; + position.PosiNotionalValue = Math.Round(eodPayPosition.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); position.PosiQuantity = eodPayPosition.PosiQuantity; position.PosiStartDate = eodPayPosition.PosiStartDate; position.OptTime = DateTime.Now; @@ -85,7 +85,7 @@ namespace YLErp.Modules.SwapModule position.PosiGrossPrice = eodPayPosition.PosiGrossPrice; position.PosiNetFeePrice = eodPayPosition.PosiNetFeePrice; position.PosiNetNoFeePrice = eodPayPosition.PosiNetNoFeePrice; - position.PosiNotionalValue = eodPayPosition.PosiNotionalValue; + position.PosiNotionalValue = Math.Round(eodPayPosition.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); position.PosiTradingFeePending = eodPayPosition.PosiFeePending; position.PosiQuantity = eodPayPosition.PosiQuantity; position.PosiDirection = eodPayPosition.PosiDirection; diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index f95b4718..23483515 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -332,7 +332,7 @@ namespace YLErp.Modules.SwapModule AssetBookName = asset.Name, Notional = Convert.ToDouble(flowMerge.TradingQtyAbs), TradeAmount = Convert.ToDouble(flowMerge.TradingQtyAbs), - StockEqvNotional = Convert.ToDouble(flowMerge.TradingAmount), + StockEqvNotional = Math.Round(Convert.ToDouble(flowMerge.TradingAmount), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero), IsAutoGenerate = true, }; if (flowMerge.SettleDate.HasValue) @@ -374,7 +374,7 @@ namespace YLErp.Modules.SwapModule PosiNetFeePrice = flowMerge.TradingAmountNetFeeAvg ?? 0, PosiNetNoFeePrice = flowMerge.TradingAmountNetAvg ?? 0, PosiQuantity = flowMerge.TradingQtyAbs, - PosiNotionalValue = flowMerge.TradingAmount, + PosiNotionalValue = Math.Round(flowMerge.TradingAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero), PosiTradingFeePending = flowMerge.TradingFeePending, PosiTradingFee = 0, PosiTradingFeeUnit = 0, @@ -762,6 +762,7 @@ namespace YLErp.Modules.SwapModule req.SpotPrice = Convert.ToDouble(swapPosition.PosiNetPrice); } req.Strike = null; + req.StockEqvNotional = Math.Round(req.StockEqvNotional, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); req.OriginalStockEqvNotional = req.StockEqvNotional; req.StockEqvNotionalReal = req.StockEqvNotional; @@ -1355,7 +1356,7 @@ namespace YLErp.Modules.SwapModule position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice; position.PosiNetFeePrice = swap.PosiQuantity == 0 ? 0 : (swap.PosiNetNoFeePrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); position.PosiNetFeePrice = Math.Round(position.PosiNetFeePrice??0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - position.PosiNotionalValue = swap.PosiNotionalValue; + position.PosiNotionalValue = Math.Round(swap.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); position.PosiQuantity = swap.PosiQuantity; position.InterestDirection = swap.InterestDirection; position.InterestMode = swap.InterestMode; @@ -1479,7 +1480,7 @@ namespace YLErp.Modules.SwapModule td.ProcessStatus = null; if (backToBegin) { - td.StockEqvNotional = td.OriginalStockEqvNotional ?? 0; + td.StockEqvNotional = Math.Round(td.OriginalStockEqvNotional ?? 0, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); td.UnWindDate = null; td.HasPartialUnWind = null; SingleTradeBackToBegin(td, swapPositions); @@ -1641,10 +1642,10 @@ namespace YLErp.Modules.SwapModule posi.PosiGrossPrice = eodPosi.PosiGrossPrice; posi.PosiNetFeePrice = eodPosi.PosiNetFeePrice; posi.PosiNetNoFeePrice = eodPosi.PosiNetNoFeePrice; - posi.PosiNotionalValue = eodPosi.PosiNotionalValue; + posi.PosiNotionalValue = Math.Round(eodPosi.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (posi.PosiDirection > 0) { - td.StockEqvNotional = Convert.ToDouble(posi.PosiNotionalValue); + td.StockEqvNotional = Math.Round(Convert.ToDouble(posi.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); td.TradeAmount = Convert.ToDouble(posi.PosiQuantity); } } diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js index b92a89e3..94481d55 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js @@ -669,7 +669,7 @@ function getColModelGridStep4() { label: '名义本金', width: 160, align: 'center', - formatter: otcformat.trading.umprice + formatter: otcformat.trading.StockEqvNotional } , { name: 'position.PosiTradingFee', @@ -1221,4 +1221,4 @@ var vue = new Vue({ 'vue-underlying': vueUnderlying() } }); -window.reloadData = getList(); \ No newline at end of file +window.reloadData = getList(); diff --git a/YLErpWeb/wwwroot/Statics/views/TradeDetailsListMailV2.cshtml b/YLErpWeb/wwwroot/Statics/views/TradeDetailsListMailV2.cshtml index 36b18756..44cc8c9f 100644 --- a/YLErpWeb/wwwroot/Statics/views/TradeDetailsListMailV2.cshtml +++ b/YLErpWeb/wwwroot/Statics/views/TradeDetailsListMailV2.cshtml @@ -897,7 +897,7 @@ @tr.MetaDic["互换_收取方初始预付金"] @tr.MetaDic["互换_收取方交易费用"] @tr.MetaDic["互换_收取方多空方向"] - @tr.OriginalStockEqvNotional + @tr.TdDetail.OriginalStockEqvNotional.OtcFormat(OtcFormatFlag.StockEqvNotional) @tr.MetaDic["年化天数"] @tr.MetaDic["互换_互换日期"]