From 0755233ab63cd0a7be4a1e8117d2f41b95a28d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Wed, 22 Jul 2026 13:05:49 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(swap):=20=E7=BB=9F=E4=B8=80=E5=A4=84?= =?UTF-8?q?=E7=90=86=E3=80=90=E5=90=8D=E4=B9=89=E6=9C=AC=E9=87=91=E3=80=91?= =?UTF-8?q?=E6=95=B0=E5=80=BC=E7=B2=BE=E5=BA=A6=E8=88=8D=E5=85=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多个位置添加 Math.Round 函数确保金额计算精度 - 新增 NormalizeNotionalValues 方法统一处理 UnwindData 中的名义本金舍入 - 修复交易平仓时 StockEqvNotional 扣减计算的精度问题 - 解决持仓数据 PosiNotionalValue 的精度舍入处理 - 修复前端页面显示格式化问题 - 添加单元测试验证名义本金舍入逻辑正确性 --- .../SwapModule/SwapUnwindScenarioTest.cs | 21 +++++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 17 +++++++++++---- .../SwapModule/SwapEodPositionService.cs | 13 ++++++------ .../SwapModule/SwapTradeBaseService.cs | 4 ++-- .../Modules/SwapModule/SwapTradeService.cs | 13 ++++++------ .../Scripts/app/swaptrade/SwapflowList.js | 4 ++-- .../views/TradeDetailsListMailV2.cshtml | 2 +- 7 files changed, 53 insertions(+), 21 deletions(-) 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["互换_互换日期"] From c58a9d40c4303d5df9de823ce1caa986adf89d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Wed, 22 Jul 2026 14:53:46 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat(swap):=20=E7=BB=9F=E4=B8=80=E3=80=90?= =?UTF-8?q?=E6=9C=9F=E5=88=9D=E3=80=81=E6=9C=9F=E6=9C=AB=E4=BA=A4=E5=89=B2?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E3=80=91=E7=B2=BE=E5=BA=A6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ConsGlobal中新增SwapDeliveryPriceRound常量定义为9位小数 - 更新incomeSwapTrade.js中交割价格计算逻辑,添加getStorageDeliveryPrice方法 - 修改SwapConsumerService中TradingAmountAvg字段的精度处理 - 添加ValidateDeliveryPrices和NormalizeDeliveryPrices方法进行交割价格验证和标准化 - 在SwapEodPositionService中对PosiGrossPrice和UnderlyingPrice进行精度处理 - 更新SwapFlowEventService中TradingAmountAvg字段的精度处理 - 在SwapFlowImportService中添加交割价格精度处理 - 更新前端界面中的输入格式配置,使用新的交割价格精度设置 - 修改SwapflowList.js中交割价格精度处理逻辑 - 在SwapFlowService中添加交割价格验证和精度处理 - 更新SwapTradeAutoService中交割价格精度处理 - 修改SwapTradeService中PosiGrossPrice字段的精度处理 - 更新unwindSwapTrade.js中交割价格计算逻辑 --- Framework/YLErp.Core/ConsGlobal.cs | 4 +++ .../Modules/SwapModule/SwapConsumerService.cs | 2 +- .../Modules/SwapModule/SwapDealService.cs | 36 +++++++++++++++++++ .../SwapModule/SwapEodPositionService.cs | 14 ++++---- .../SwapModule/SwapFlowEventService.cs | 12 ++++--- .../SwapModule/SwapFlowImportService.cs | 1 + .../Modules/SwapModule/SwapFlowService.cs | 12 +++++-- .../SwapModule/SwapTradeAutoService.cs | 7 ++-- .../Modules/SwapModule/SwapTradeService.cs | 20 +++++++++-- YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml | 2 +- YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml | 2 +- YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml | 2 +- YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml | 4 +-- .../Scripts/app/swaptrade/SwapflowList.js | 2 ++ .../Scripts/app/swaptrade/incomeSwapTrade.js | 20 ++++++----- .../Scripts/app/swaptrade/swapTradeEdit.js | 11 ++++-- .../Scripts/app/swaptrade/unwindSwapTrade.js | 20 ++++++----- 17 files changed, 127 insertions(+), 44 deletions(-) diff --git a/Framework/YLErp.Core/ConsGlobal.cs b/Framework/YLErp.Core/ConsGlobal.cs index bdf59a00..049817e7 100644 --- a/Framework/YLErp.Core/ConsGlobal.cs +++ b/Framework/YLErp.Core/ConsGlobal.cs @@ -64,6 +64,10 @@ namespace YLErp public const int PriceRound = 11; /// + /// 互换期初、期末交割价四舍五入保留位数 + /// + public const int SwapDeliveryPriceRound = 9; + /// /// 金额四舍五入保留位数 /// diff --git a/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs b/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs index 79b1a1a9..10dc3845 100644 --- a/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs @@ -137,7 +137,7 @@ namespace YLErp.Modules.SwapModule swapFlow.OptTime = result.OptTime; swapFlow.SettleDate = result.SettleDate; swapFlow.TradingAmount = result.TradingAmount; - swapFlow.TradingAmountAvg = result.TradingAmountAvg; + swapFlow.TradingAmountAvg = Math.Round(result.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swapFlow.TradingAmountFeeAvg = result.TradingAmountFeeAvg; swapFlow.TradingAmountNet = result.TradingAmountNet; swapFlow.TradingAmountNetFee = result.TradingAmountNetFee; diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 58fbae31..d044e9a6 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -37,6 +37,7 @@ namespace YLErp.Modules.SwapModule protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false) { NormalizeNotionalValues(unwindData); + NormalizeDeliveryPrices(unwindData.FlowEvents); return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve); } @@ -47,6 +48,35 @@ namespace YLErp.Modules.SwapModule unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); } + private static void ValidateDeliveryPrices(UnwindData unwindData) + { + if (unwindData.FlowEvents == null) + { + return; + } + foreach (var item in unwindData.FlowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode))) + { + var roundedPrice = Math.Round(item.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + if (item.TradingAmountAvg != roundedPrice) + { + throw new ServiceException($"期末交割价最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数"); + } + item.TradingAmountAvg = roundedPrice; + } + } + + private static void NormalizeDeliveryPrices(IEnumerable flowEvents) + { + if (flowEvents == null) + { + return; + } + foreach (var item in flowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode))) + { + item.TradingAmountAvg = Math.Round(item.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + } + } + /// 保存所有变更(生产: DbContext.SaveChanges;测试: 空操作) protected virtual void SaveAllChanges() { @@ -1230,6 +1260,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } + ValidateDeliveryPrices(unwindData); NormalizeNotionalValues(unwindData); //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易 @@ -1279,6 +1310,7 @@ namespace YLErp.Modules.SwapModule /// public void AuotoSwapUnwind(int tradeid, decimal unwindPrice, decimal unwindPriceFee, decimal unwindNetFee, decimal unwindNet, DateTime valueDate, decimal unwindQty, decimal closeFee) { + unwindPrice = Math.Round(unwindPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); unwindPriceFee = decimal.Parse(unwindPriceFee.ToString("F10")); var td = DbContext.trade.Find(tradeid); var positions = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid); @@ -1710,6 +1742,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } + ValidateDeliveryPrices(unwindData); NormalizeIncomeUnwindDate(unwindData); ValidateIncomeValueDate(unwindData, td); //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 @@ -1750,12 +1783,14 @@ namespace YLErp.Modules.SwapModule throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效"); } swapEvent.unwindData = JsonConvert.DeserializeObject(swapEvent.EventData); + NormalizeDeliveryPrices(swapEvent.unwindData.FlowEvents); if (eventType == (int)SwapEventTypeEnum.互换) { NormalizeIncomeUnwindDate(swapEvent.unwindData); ValidateIncomeValueDate(swapEvent.unwindData, td); } var flowList = FindFlowEventsByEventId(swapEvent.id); + NormalizeDeliveryPrices(flowList); string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费; int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate); if (swapEvent.unwindData.SwapMarginAmount != 0) @@ -1798,6 +1833,7 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("未找到交易信息"); } + ValidateDeliveryPrices(unwindData); if (eventType == (int)SwapEventTypeEnum.互换) { NormalizeIncomeUnwindDate(unwindData); diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 6965a924..58509c63 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -37,6 +37,8 @@ namespace YLErp.Modules.SwapModule /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) protected virtual void PersistEodSwapPosition(eod_swap_position position) { + position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); position.PosiNotionalValue = Math.Round(position.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (position.id == 0) { @@ -181,10 +183,10 @@ namespace YLErp.Modules.SwapModule { var normalizedPrice = BondPriceConverter.ToStorage(price); Log.Error($"互换债券日终价格按展示态返回,已转换为存储态: UnderlyingCode={code}, ValueDate={settleDate:yyyy-MM-dd}, PosiGrossPrice={posiGrossPrice}, SourcePrice={price}, NormalizedPrice={normalizedPrice}"); - return normalizedPrice; + return Math.Round(normalizedPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } - return price; + return Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } /// 获取标的缓存数据(生产: DataCacheProvider;测试: 返回内存对象) @@ -1473,7 +1475,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.ContractSize = eventFlow.ContractSize; newEodPayPosition.CountRatio = eventFlow.CountRatio; newEodPayPosition.PosiNetPrice = netPrice; - newEodPayPosition.PosiGrossPrice = grossPrice; + newEodPayPosition.PosiGrossPrice = Math.Round(grossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); newEodPayPosition.PosiNetFeePrice = netFeePrice; newEodPayPosition.PosiNetNoFeePrice = netNoFeePrice; newEodPayPosition.PosiQuantity = payQty; @@ -1772,7 +1774,7 @@ namespace YLErp.Modules.SwapModule posiQty = 0; } curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty); - curretEod.PosiGrossPrice = Math.Round(curretEod.PosiGrossPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + curretEod.PosiGrossPrice = Math.Round(curretEod.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty); curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty); @@ -1898,14 +1900,14 @@ namespace YLErp.Modules.SwapModule } if (data.IsBond()) { - return BondPrice(data, settleDate, out vobp); + return Math.Round(BondPrice(data, settleDate, out vobp), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } var price = data.Price ?? 0; if (EodPriceQueryService.TryGetEodPrice(settleDate, code, out var eodPrice)) { price = eodPrice.GetPrice(SettlementTypeEnum.ClosePrice); } - return Convert.ToDecimal(price); + return Math.Round(Convert.ToDecimal(price), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } /// /// 获取债券收盘价格 diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs index 39eb73e4..9fafcdf7 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs @@ -61,6 +61,10 @@ namespace YLErp.Modules.SwapModule { foreach (var evt in events) { + if (!string.IsNullOrEmpty(evt.UnderlyingCode)) + { + evt.TradingAmountAvg = Math.Round(evt.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + } DbContext.swap_flow_event.Add(evt); } DbContext.SaveChanges(); @@ -321,7 +325,7 @@ namespace YLErp.Modules.SwapModule DataState = 1, EventDate = flow_merge.OccurTime, UnwindDate = QdpCalendarHelper.GetNonHoliday(flow_merge.OccurTime.AddDays(1)), - TradingAmountAvg = TradingAmountAvg, + TradingAmountAvg = Math.Round(TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), TradingAmountFeeAvg = TradingAmountFeeAvg, TradingAmountNetFeeAvg = TradingAmountNetFeeAvg, TradingAmountNetAvg = flow_merge.TradingAmountNetAvg, @@ -375,7 +379,7 @@ namespace YLErp.Modules.SwapModule DataState = 1, EventDate = flow_merge.OccurTime, UnwindDate = QdpCalendarHelper.GetNonHoliday(flow_merge.OccurTime.AddDays(1)), - TradingAmountAvg = flow_merge.TradingAmountAvg, + TradingAmountAvg = Math.Round(flow_merge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), TradingAmountFeeAvg = flow_merge.TradingAmountFeeAvg, TradingFeePending = flow_merge.TradingFeePending, ClientId = flow_merge.ClientId @@ -409,7 +413,7 @@ namespace YLErp.Modules.SwapModule DataState = (int)SwapFlowDateStateEnum.完成, EventDate = td.TradeDate.Value, UnwindDate = td.StartDate.Value, - TradingAmountAvg = position.PosiGrossPrice, + TradingAmountAvg = Math.Round(position.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), TradingAmountFeeAvg = position.PosiNetPrice, TradingAmountNetFeeAvg = position.PosiNetFeePrice, TradingAmountNetAvg = position.PosiNetNoFeePrice, @@ -462,7 +466,7 @@ namespace YLErp.Modules.SwapModule DataState = 100, EventDate = td.TradeDate.Value, UnwindDate = QdpCalendarHelper.GetNonHoliday(td.TradeDate.Value.AddDays(1)), - TradingAmountAvg = flowMerge.TradingAmountAvg, + TradingAmountAvg = Math.Round(flowMerge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), TradingAmountFeeAvg = flowMerge.TradingAmountFeeAvg, TradingAmountNetFeeAvg = flowMerge.TradingAmountNetFeeAvg, TradingAmountNetAvg = flowMerge.TradingAmountNetAvg, diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs index fb65eff8..79b5ad5a 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs @@ -129,6 +129,7 @@ namespace YLErp.Modules.SwapModule if (swap_flow.TradingAmountNetFee.HasValue) swap_flow.TradingAmountNetFee = BondPriceConverter.ToStorage(swap_flow.TradingAmountNetFee.Value); } + swap_flow.TradingAmountAvg = Math.Round(swap_flow.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); if (!string.IsNullOrEmpty(clientName)) { var client = DataCacheProvider.GetClientDataSource().AsQueryable(x=>x.Name== clientName).FirstOrDefault(); diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs index 17b17901..3cb640b9 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs @@ -473,7 +473,7 @@ namespace YLErp.Modules.SwapModule swap_Flow.UnderlyingCode = req.UnderlyingCode; swap_Flow.BsType = req.BsType; swap_Flow.ContractSize = req.ContractSize; - swap_Flow.TradingAmountAvg = req.TradingAmountAvg; + swap_Flow.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swap_Flow.TradingAmountFeeAvg = TradeFeeHelper.CalcPriceWithFee(req.TradingFee,req.TradingAmountAvg,req.TradingQty,req.BsType); swap_Flow.ClientId = req.ClientId; swap_Flow.ytm = req.ytm; @@ -540,7 +540,7 @@ namespace YLErp.Modules.SwapModule swap_Flow.BsType = req.BsType; swap_Flow.DataState = (int)SwapFlowDateStateEnum.等待完成; swap_Flow.ContractSize = req.ContractSize; - swap_Flow.TradingAmountAvg = req.TradingAmountAvg; + swap_Flow.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swap_Flow.TradingAmountFeeAvg = req.TradingAmountFeeAvg; UpdateDbOption(swap_Flow); if (req.id == 0) @@ -695,7 +695,7 @@ namespace YLErp.Modules.SwapModule TradingFee = gourpItem.Sum(s => s.TradingFee), DataState = (int)SwapFlowDateStateEnum.等待完成, TradingAmountFeeAvg = gourpItem.Average(s => s.TradingAmountFeeAvg), - TradingAmountAvg = gourpItem.Average(s => s.TradingAmountAvg), + TradingAmountAvg = Math.Round(gourpItem.Average(s => s.TradingAmountAvg), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), ContractSize = swapflow.ContractSize }; UpdateDbOption(swap_flow_summary); @@ -747,6 +747,11 @@ namespace YLErp.Modules.SwapModule private void CheckValid(swap_flow req) { CheckRequired(req); + var roundedPrice = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + if (req.TradingAmountAvg != roundedPrice) + { + throw new ServiceException($"成交全价最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数"); + } var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(req.UnderlyingCode); if (underlying == null) { @@ -764,6 +769,7 @@ namespace YLErp.Modules.SwapModule // 数量×100(万手→手),与价格维度无关,保留常量 req.TradingQty *= ConsGlobal.bondShowPriceMultiple; } + req.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); } diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs index 75af7ea2..1acab84e 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs @@ -154,7 +154,7 @@ namespace YLErp.Modules.SwapModule swapFlow.DataState = (int)SwapFlowDateStateEnum.等待完成; } // 债券报价(×100)转入库小数(×0.01),统一走 BondPriceConverter - swapFlow.TradingAmountAvg = BondPriceConverter.ToStorage(item.deal_full_price ?? 0); + swapFlow.TradingAmountAvg = Math.Round(BondPriceConverter.ToStorage(item.deal_full_price ?? 0), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swapFlow.TradingAmountFeeAvg = BondPriceConverter.ToStorage(item.deal_full_price_include_fee ?? 0); swapFlow.TradingAmount = swapFlow.TradingQty * swapFlow.ContractSize * swapFlow.TradingAmountAvg; swapFlow.ClientId = Convert.ToInt32(item.client_id ?? 0); @@ -451,7 +451,7 @@ namespace YLErp.Modules.SwapModule swap_flow_summary.FirstFlowTime = swapflow.OptTime; swap_flow_summary.SettleDate = gourpItem.Max(s => s.SettleDate); swap_flow_summary.TradingAmountAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.TradingAmountAvg * s.TradingQty) / swap_flow_summary.TradingQty; - swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountAvg : swap_flow_summary.TradingAmountAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountFeeAvg = Math.Round(swap_flow_summary.TradingAmountFeeAvg, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountNetAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.TradingAmountNet * s.TradingQty) / swap_flow_summary.TradingQty; @@ -508,6 +508,7 @@ namespace YLErp.Modules.SwapModule swap_flow_summary.SettleDate = gourpItem.Max(s => s.SettleDate); swap_flow_summary.TradingAmount = swap_flow_summary.TradingQty * swap_flow_summary.ContractSize; swap_flow_summary.TradingAmountAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.FullPrice * s.TradingQty) / swap_flow_summary.TradingQty; + swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountAvg : swap_flow_summary.TradingAmountAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountNetAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.NetPrice * s.TradingQty) / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountNetFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountNetAvg : swap_flow_summary.TradingAmountNetAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; @@ -1081,7 +1082,7 @@ namespace YLErp.Modules.SwapModule var ratio = flowMergeClone.BsType == 1 ? 1 : -1; var oriRatio = flowMergeClone.BsType == 1 ? -1 : 1; flowMergeClone.TradingFeePending = flowMergeClone.TradingQty / origin.TradingQty * origin.TradingFeePending; - flowMergeClone.TradingAmountAvg = origin.TradingAmountAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty; + flowMergeClone.TradingAmountAvg = Math.Round(origin.TradingAmountAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); flowMergeClone.TradingAmountNetAvg = origin.TradingAmountNetAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty; flowMergeClone.TradingAmountFeeAvg = flowMergeClone.TradingAmountAvg + ratio * flowMergeClone.TradingFeePending / flowMergeClone.TradingQty; diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 23483515..9f96a210 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -51,6 +51,17 @@ namespace YLErp.Modules.SwapModule { } + + private static decimal ValidateDeliveryPrice(decimal price, string fieldName) + { + var roundedPrice = Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + if (price != roundedPrice) + { + throw new ServiceException($"{fieldName}最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数"); + } + return roundedPrice; + } + #region 互换交易保存 /// /// 新版收益互换预付金校验 @@ -370,7 +381,7 @@ namespace YLErp.Modules.SwapModule CountRatio = underlying.CountRatio, ContractSize = Convert.ToDecimal(underlying.ContractSize), PosiNetPrice = flowMerge.TradingAmountFeeAvgAbs, - PosiGrossPrice = flowMerge.TradingAmountAvg, + PosiGrossPrice = Math.Round(flowMerge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), PosiNetFeePrice = flowMerge.TradingAmountNetFeeAvg ?? 0, PosiNetNoFeePrice = flowMerge.TradingAmountNetAvg ?? 0, PosiQuantity = flowMerge.TradingQtyAbs, @@ -1350,8 +1361,11 @@ namespace YLErp.Modules.SwapModule position.UnderlyingCode = swap.UnderlyingCode; position.UnderlyingInstrumentType = swap.UnderlyingInstrumentType; position.PosiDirection = swap.PosiDirection; - position.PosiGrossPrice = swap.PosiGrossPrice; - position.PosiNetPrice = swap.PosiQuantity == 0 ? 0 : (swap.PosiGrossPrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); + // position.PosiGrossPrice = string.IsNullOrEmpty(swap.UnderlyingCode) + // ? Math.Round(swap.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero) + // : ValidateDeliveryPrice(swap.PosiGrossPrice, "期初交割价"); + position.PosiGrossPrice = Math.Round(swap.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + position.PosiNetPrice = swap.PosiQuantity == 0 ? 0 : (position.PosiGrossPrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); position.PosiNetPrice = Math.Round(position.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice; position.PosiNetFeePrice = swap.PosiQuantity == 0 ? 0 : (swap.PosiNetNoFeePrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml index b02667f9..c86b8774 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml @@ -163,7 +163,7 @@ {{priceFormat(floatPosition.TradingAmountNetAvg > 0 ? floatPosition.TradingAmountNetAvg : floatPosition.PosiNetPrice)}} - + diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml index 1011c3c8..e1ecadcb 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml @@ -200,7 +200,7 @@ {{priceFormat(floatPosition.PosiGrossPrice)}} - + diff --git a/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml b/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml index 6bc78ac4..eb805b1c 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml @@ -243,7 +243,7 @@
- +
@*
diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index b8c7d03f..c62ed4c9 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -452,7 +452,7 @@ - + @@ -461,7 +461,7 @@ - + {{item.underlying!=null?item.underlying.QuoteUnitString:''}} diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js index 94481d55..3af1b627 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js @@ -1,5 +1,6 @@ //window.otcformat.options.disableGrouping = true; const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '' }); +const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '' }); const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); var clients = ylotc.clients; @@ -1198,6 +1199,7 @@ var vue = new Vue({ }, postSwapflow() { var thisObj = this; + thisObj.swapflow.TradingAmountAvg = _.round(Number(thisObj.swapflow.TradingAmountAvg), 9); main.post("/swaptrade2/SaveSwapflow", { req: thisObj.swapflow, step: thisObj.step }).done(function (resp) { if (resp.success) { getList(); diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 7acad7c4..0e6a610d 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -8,6 +8,7 @@ const inputFormatEqvNotional = Object.freeze({ precision: otcformat.trading.Stoc const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative: true }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true }); +const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true }); let ValueDate = model.ValueDate; let MaxIncomeValueDate = model.MaxIncomeValueDate ? model.MaxIncomeValueDate.substr(0, 10) : ValueDate; @@ -77,6 +78,9 @@ const vue = new Vue({ getPriceScale() { return SwapCalc.getPriceScale(this.multiplier); }, + getStorageDeliveryPrice() { + return _.round(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), 9); + }, initDeal() { var positions = model.FlowEvents.filter((item) => { return item.UnderlyingCode; @@ -161,7 +165,7 @@ const vue = new Vue({ { code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate }) .done(function (res) { res.obj = res.obj * thisObj.multiplier; - thisObj.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(res.obj); + thisObj.floatPosition.TradingAmountAvg = _.round(Number(res.obj), 9); thisObj.calcFloatClosePnl(); }); }, @@ -180,11 +184,11 @@ const vue = new Vue({ 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); - let scale = thisObj.getPriceScale(); + let deliveryPrice = thisObj.getStorageDeliveryPrice(); // 债券全价是单位价格,价差盈亏应按持仓数量×合约乘数计算; // CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。 let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1); - thisObj.floatPosition.MarkClosePnl = positionAmount * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiGrossPrice) * floatRatio; + thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio; thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红 // 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留) // 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游 @@ -205,13 +209,13 @@ const vue = new Vue({ thisObj.deal.SwapRealizedPnL = pnl; thisObj.deal.SwapMarginRebatePnl = 0; thisObj.deal.SwapMarginAmount = 0; - let scale = thisObj.getPriceScale(); - thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseNotionalValue) * scale; + let deliveryPrice = thisObj.getStorageDeliveryPrice(); + thisObj.floatPosition.TradingAmount = deliveryPrice * parseFloat(thisObj.deal.CloseNotionalValue); thisObj.floatPosition.CloseFee = TradingFee; if (thisObj.deal.CloseQty > 0) { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale + (TradingFee / thisObj.deal.CloseQty) * floatRatio; + thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * floatRatio; } else { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale; + thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice; } this.interestList.forEach(x => { //let interestRatio = x.InterestDirection == 1 ? 1 : -1; @@ -277,7 +281,7 @@ const vue = new Vue({ thisObj.floatPosition.EventDate = thisObj.deal.ValueDate; let floatPosition = _.cloneDeep(thisObj.floatPosition); floatPosition.Quantity = 0; - floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * thisObj.getPriceScale(); + floatPosition.TradingAmountAvg = thisObj.getStorageDeliveryPrice(); reqObj.FlowEvents.push(floatPosition); var postData = { unwindData: reqObj }; var msg = "确认提交收益结算?"; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js index dc726ce2..5c974e69 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js @@ -14,6 +14,8 @@ const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.trade const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '', percent: false }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent:true }); +const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: false }); +const inputFormatSwapBondDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: true }); const consUnderlyingFlagBase = (function () { let unSelFlag = tradeHelper.UnderlyingSelectFlag; @@ -277,7 +279,8 @@ const vue = new Vue({ //计算数量 if (this.paySwapList.length > 0) { var item = this.paySwapList[0]; - var notional = item.PosiGrossPrice * item.ContractSize; + var deliveryPrice = _.round(Number(item.PosiGrossPrice), 9); + var notional = deliveryPrice * item.ContractSize; item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision); this.calcNotional(); } @@ -358,7 +361,8 @@ const vue = new Vue({ } var national = payItem.PosiQuantity * payItem.ContractSize; // 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional - var stockEqvNotional = SwapCalc.calcStockEqvNotional(payItem.PosiGrossPrice, national);//名义本金=期初价格*数量*乘数 + var deliveryPrice = _.round(Number(payItem.PosiGrossPrice), 9); + var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数 this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional); payItem.PosiNotionalValue = this.trade.StockEqvNotional; } @@ -502,6 +506,7 @@ const vue = new Vue({ errorcount++; return false; } + x.PosiGrossPrice = _.round(Number(x.PosiGrossPrice), 9); thisObj.trade.swap_positions.push(x); }); } else { @@ -628,7 +633,7 @@ const vue = new Vue({ main.post("/pricing/AjaxGetUnderlyingPrice", { underlyingCode: underlyingCode, tradeDate: StartDate }) .done(function (resp) { item.PosiNetNoFeePrice = otcformat.trading.umprice(resp.obj.netPrice); - item.PosiGrossPrice = otcformat.trading.umprice(resp.obj.price); + item.PosiGrossPrice = _.round(Number(resp.obj.price), 9); thisObj.calcNotional(); }); }, diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index c7fd23ff..71553a15 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -7,6 +7,7 @@ const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.noti const inputFormatEqvNotional = Object.freeze({ precision: otcformat.trading.StockEqvNotional.precision, append: '', negative: true }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true }); +const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true }); let ValueDate = model.ValueDate; const vue = new Vue({ el: '#vueDiv', @@ -40,6 +41,9 @@ const vue = new Vue({ getPriceScale() { return this.multiplier == 100 ? 0.01 : 1; }, + getStorageDeliveryPrice() { + return _.round(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), 9); + }, initDeal() { var positions = model.FlowEvents.filter((item) => { return item.UnderlyingCode; @@ -84,7 +88,7 @@ const vue = new Vue({ this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount); //this.floatPosition.PosiNetPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiNetPrice); //this.floatPosition.PosiGrossPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiGrossPrice); - this.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(this.floatPosition.TradingAmountAvg); + this.floatPosition.TradingAmountAvg = _.round(Number(this.floatPosition.TradingAmountAvg), 9); this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee); this.floatPosition.TradingFeePending = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFeePending); this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2); @@ -212,7 +216,7 @@ const vue = new Vue({ { code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate }) .done(function (res) { res.obj = res.obj * thisObj.multiplier; - thisObj.floatPosition.TradingAmountAvg = otcformat.trading.tradeSinglePrice(res.obj); + thisObj.floatPosition.TradingAmountAvg = _.round(Number(res.obj), 9); thisObj.calcFloatClosePnl(); }); }, @@ -222,8 +226,8 @@ const vue = new Vue({ 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 scale = thisObj.getPriceScale(); - thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000; + let deliveryPrice = thisObj.getStorageDeliveryPrice(); + thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (deliveryPrice - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000; thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2));//MarkClosePnl 纯盯市不要计算交易费用和分红 thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl); thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + parseFloat(thisObj.floatPosition.DividendIn)).toFixed(2); @@ -253,13 +257,13 @@ const vue = new Vue({ thisObj.deal.SwapRealizedPnL = pnl; thisObj.deal.SwapMarginRebatePnl = 0; thisObj.deal.SwapMarginAmount = 0; - let scale = thisObj.getPriceScale(); - thisObj.floatPosition.TradingAmount = parseFloat(thisObj.floatPosition.TradingAmountAvg) * parseFloat(thisObj.deal.CloseQty) * scale; + let deliveryPrice = thisObj.getStorageDeliveryPrice(); + thisObj.floatPosition.TradingAmount = deliveryPrice * parseFloat(thisObj.deal.CloseQty); thisObj.floatPosition.CloseFee = TradingFee; if (thisObj.deal.CloseQty == 0) { thisObj.floatPosition.TradingAmountFeeAvg = 0; } else { - thisObj.floatPosition.TradingAmountFeeAvg = parseFloat(thisObj.floatPosition.TradingAmountAvg) * scale + (TradingFee / thisObj.deal.CloseQty) * ratio; + thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * ratio; } this.interestList.forEach(x => { /*let interestRatio = x.InterestDirection == 1 ? 1 : -1;*/ @@ -360,7 +364,7 @@ const vue = new Vue({ thisObj.floatPosition.EventDate = thisObj.deal.ValueDate; let floatPosition = _.cloneDeep(thisObj.floatPosition); floatPosition.Quantity = reqObj.CloseQty; - floatPosition.TradingAmountAvg = floatPosition.TradingAmountAvg * thisObj.getPriceScale(); + floatPosition.TradingAmountAvg = thisObj.getStorageDeliveryPrice(); reqObj.FlowEvents.push(floatPosition); var postData = { unwindData: reqObj }; var msg = "确认提交平仓?"; From bbf98ba61c74360fc23b9a7dcb972767fdcd64ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Wed, 22 Jul 2026 18:08:04 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(data):=20=E4=BF=AE=E6=AD=A3=E5=80=BA?= =?UTF-8?q?=E5=88=B8=E5=B8=B8=E9=87=8F=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Bonds 常量值从 "Bond" 更正为 "Bonds" --- Framework/YLErp.Core/ConsGlobal.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/YLErp.Core/ConsGlobal.cs b/Framework/YLErp.Core/ConsGlobal.cs index 049817e7..6469db05 100644 --- a/Framework/YLErp.Core/ConsGlobal.cs +++ b/Framework/YLErp.Core/ConsGlobal.cs @@ -121,7 +121,7 @@ namespace YLErp public const string TBonds = "TBonds"; public const string CreditBonds = "CreditBonds"; //信用债 public const string OtherBonds = "OtherBonds"; //其它债券 - public const string Bonds = "Bond"; //债券 + public const string Bonds = "Bonds"; //债券 public const string GoldFutures = "GoldFutures"; public const string TBFutures = "TBFutures"; public const string OtherFutures = "OtherFutures"; From 018136c615b3e8e03c345b53ae8edd4c30ad34e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 10:13:09 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E6=94=B6?= =?UTF-8?q?=E7=9B=8A=E4=BA=92=E6=8D=A2=E4=BB=B7=E6=A0=BC=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=92=8C=E5=88=9D=E5=A7=8B=E4=BF=9D=E8=AF=81=E9=87=91=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除债券价格显示态和存储态转换的错误判断逻辑 - 简化债券价格转换条件,只对债券标的进行存储态转换 - 添加初始保证金计算前的金额精度统一处理 - 重构初始保证金准备逻辑到独立方法中 - 修复因价格单位不一致导致的浮动损益计算错误 --- .../SwapModule/SwapEodPositionService.cs | 12 +++------ .../Modules/SwapModule/SwapTradeService.cs | 27 ++++++++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 58509c63..cf1b3440 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -162,11 +162,7 @@ namespace YLErp.Modules.SwapModule /// /// 获取用于互换浮动腿盯市的标的价格。 /// - /// 普通债券类收益互换的新录入页面将全价按小数保存,例如页面录入 20% 后 - /// PosiGrossPrice 为 0.2;而历史交易中仍可能存在直接保存为 20 的展示态价格。 - /// 中债估值正常经 EodPriceQueryService 转换后应为小数价格,但手工维护的历史 - /// 行情可能仍以展示态进入该服务,例如 2000 经一次转换后得到 20。若将 20 - /// 与 0.2 直接相减,会把 20% 的价格差误算成 1,980,000 的浮动损益。 + /// 普通债券类收益互换的新录入页面将全价按小数保存,例如页面录入 20% 后 PosiGrossPrice 为 0.2 /// /// 因此仅当交易期初价已经是小数口径、且当前债券价明显仍处于展示态时,再做 /// 一次展示态到存储态转换。期初价本身是历史展示态口径的存量交易保持原价格, @@ -176,10 +172,10 @@ namespace YLErp.Modules.SwapModule { var price = GetUnderlyingPrice(code, settleDate, out vobp); var underlying = GetUnderlyingData(code); - var usesStoragePrice = Math.Abs(posiGrossPrice) < 2m; - var usesDisplayPrice = Math.Abs(price) >= 10m; + // var usesStoragePrice = Math.Abs(posiGrossPrice) < 2m; + // var usesDisplayPrice = Math.Abs(price) >= 10m; - if (underlying?.IsBond() == true && usesStoragePrice && usesDisplayPrice) + if (underlying?.IsBond() == true) { var normalizedPrice = BondPriceConverter.ToStorage(price); Log.Error($"互换债券日终价格按展示态返回,已转换为存储态: UnderlyingCode={code}, ValueDate={settleDate:yyyy-MM-dd}, PosiGrossPrice={posiGrossPrice}, SourcePrice={price}, NormalizedPrice={normalizedPrice}"); diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 9f96a210..d48b339a 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -381,7 +381,10 @@ namespace YLErp.Modules.SwapModule CountRatio = underlying.CountRatio, ContractSize = Convert.ToDecimal(underlying.ContractSize), PosiNetPrice = flowMerge.TradingAmountFeeAvgAbs, - PosiGrossPrice = Math.Round(flowMerge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + PosiGrossPrice = Math.Round( + flowMerge.TradingAmountAvg, + underlying.IsBond() ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound, + MidpointRounding.AwayFromZero), PosiNetFeePrice = flowMerge.TradingAmountNetFeeAvg ?? 0, PosiNetNoFeePrice = flowMerge.TradingAmountNetAvg ?? 0, PosiQuantity = flowMerge.TradingQtyAbs, @@ -684,11 +687,7 @@ namespace YLErp.Modules.SwapModule private bool PrepareTrade(trade req, TradeSourceEnum dataSource, underlying_manager um) { bool tradeNumberGenerated = false; - req.InitialMargin = Convert.ToDouble(req.trade_Initial_Margin.MarginValue); - if (req.trade_Initial_Margin.MarginType == 0) - { - req.InitialMargin = req.StockEqvNotional == 0 ? 0 : Convert.ToDouble(req.trade_Initial_Margin.MarginValue) * req.StockEqvNotional; - }; + PrepareInitialMargin(req); var isAddNew = req.id == 0; if (isAddNew) { @@ -731,6 +730,17 @@ namespace YLErp.Modules.SwapModule return tradeNumberGenerated; } + + private static void PrepareInitialMargin(trade req) + { + // 初始预付金依赖最终入库的名义本金,须先统一金额精度,避免两者无法勾稽。 + req.StockEqvNotional = Math.Round(req.StockEqvNotional, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + req.InitialMargin = Convert.ToDouble(req.trade_Initial_Margin.MarginValue); + if (req.trade_Initial_Margin.MarginType == 0) + { + req.InitialMargin = req.StockEqvNotional == 0 ? 0 : Convert.ToDouble(req.trade_Initial_Margin.MarginValue) * req.StockEqvNotional; + } + } //准备单个交易 private trade PrepareSingleTrade(trade req, TradeSourceEnum dataSource, bool isAddNew, underlying_manager um) { @@ -1364,7 +1374,10 @@ namespace YLErp.Modules.SwapModule // position.PosiGrossPrice = string.IsNullOrEmpty(swap.UnderlyingCode) // ? Math.Round(swap.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero) // : ValidateDeliveryPrice(swap.PosiGrossPrice, "期初交割价"); - position.PosiGrossPrice = Math.Round(swap.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + var storagePriceRound = ConsGlobal.InstrumentType.IsBond(swap.UnderlyingInstrumentType) + ? ConsGlobal.PriceRound + : ConsGlobal.SwapDeliveryPriceRound; + position.PosiGrossPrice = Math.Round(swap.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.PosiNetPrice = swap.PosiQuantity == 0 ? 0 : (position.PosiGrossPrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); position.PosiNetPrice = Math.Round(position.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice; From bc961c3411889725007aad4703984d9b554beed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 11:06:50 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E4=BA=92?= =?UTF-8?q?=E6=8D=A2=E4=BA=A4=E6=98=93=E4=B8=AD=E5=80=BA=E5=88=B8=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E7=B2=BE=E5=BA=A6=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E6=89=A9=E5=85=85=E5=88=B011=E4=BD=8D=E8=AE=A1?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一债券价格四舍五入规则,使用AwayFromZero模式 - 为债券类型添加特殊的精度处理逻辑 - 修复前端价格显示精度计算问题 - 优化价格存储精度控制,区分债券和其他产品类型 - 修复初始化YTM和净价精度处理问题 - 更新价格验证逻辑以支持动态精度设置 --- .../RealTimeClientBanlanceService.cs | 2 +- .../Modules/SwapModule/SwapConsumerService.cs | 8 ++++- .../Modules/SwapModule/SwapDealService.cs | 23 +++++++++++-- .../SwapModule/SwapEodPositionService.cs | 34 +++++++++++++++---- .../SwapModule/SwapFlowEventService.cs | 34 ++++++++++++++++--- .../SwapModule/SwapFlowImportService.cs | 8 +++-- .../Modules/SwapModule/SwapFlowService.cs | 14 +++++--- .../SwapModule/SwapTradeAutoService.cs | 8 ++--- .../Modules/SwapModule/SwapTradeService.cs | 21 ++++++++---- YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml | 4 +-- .../Scripts/app/swaptrade/incomeSwapTrade.js | 3 +- .../Scripts/app/swaptrade/swapTradeEdit.js | 24 +++++++++---- .../Scripts/app/swaptrade/unwindSwapTrade.js | 3 +- 13 files changed, 142 insertions(+), 44 deletions(-) diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs index 2d16d4dd..aeedd67e 100644 --- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs +++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs @@ -1386,7 +1386,7 @@ namespace YLErp.BLL.Eod ClientId = item.client_id ?? 0, ClientName = item.client_name, TradingQty = (item.order_qty ?? 0) - (item.last_shares ?? 0), - TradingAmountAvg = BondPriceConverter.ToStorage(item.full_price ?? 0), + TradingAmountAvg = Math.Round(BondPriceConverter.ToStorage(item.full_price ?? 0), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero), TradingAmountFeeAvg = BondPriceConverter.ToStorage(item.full_price ?? 0), TradingFee = 0 }; diff --git a/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs b/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs index 10dc3845..e5504ce7 100644 --- a/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapConsumerService.cs @@ -137,7 +137,13 @@ namespace YLErp.Modules.SwapModule swapFlow.OptTime = result.OptTime; swapFlow.SettleDate = result.SettleDate; swapFlow.TradingAmount = result.TradingAmount; - swapFlow.TradingAmountAvg = Math.Round(result.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + var underlying = string.IsNullOrEmpty(result.UnderlyingCode) + ? null + : DataCacheProvider.GetUnderlyingDataSource().GetData(result.UnderlyingCode); + var storagePriceRound = underlying?.IsBond() == true + ? ConsGlobal.PriceRound + : ConsGlobal.SwapDeliveryPriceRound; + swapFlow.TradingAmountAvg = Math.Round(result.TradingAmountAvg, storagePriceRound, MidpointRounding.AwayFromZero); swapFlow.TradingAmountFeeAvg = result.TradingAmountFeeAvg; swapFlow.TradingAmountNet = result.TradingAmountNet; swapFlow.TradingAmountNetFee = result.TradingAmountNetFee; diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index d044e9a6..05182f5f 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -48,6 +48,20 @@ namespace YLErp.Modules.SwapModule unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); } + private static int GetStorageDeliveryPriceRound(swap_flow_event flowEvent) + { + if (ConsGlobal.InstrumentType.IsBond(flowEvent?.UnderlyingInstrumentType)) + { + return ConsGlobal.PriceRound; + } + if (string.IsNullOrEmpty(flowEvent?.UnderlyingCode)) + { + return ConsGlobal.SwapDeliveryPriceRound; + } + var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(flowEvent?.UnderlyingCode); + return underlying?.IsBond() == true ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound; + } + private static void ValidateDeliveryPrices(UnwindData unwindData) { if (unwindData.FlowEvents == null) @@ -56,7 +70,7 @@ namespace YLErp.Modules.SwapModule } foreach (var item in unwindData.FlowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode))) { - var roundedPrice = Math.Round(item.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + var roundedPrice = Math.Round(item.TradingAmountAvg, GetStorageDeliveryPriceRound(item), MidpointRounding.AwayFromZero); if (item.TradingAmountAvg != roundedPrice) { throw new ServiceException($"期末交割价最多保留{ConsGlobal.SwapDeliveryPriceRound}位小数"); @@ -73,7 +87,7 @@ namespace YLErp.Modules.SwapModule } foreach (var item in flowEvents.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode))) { - item.TradingAmountAvg = Math.Round(item.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + item.TradingAmountAvg = Math.Round(item.TradingAmountAvg, GetStorageDeliveryPriceRound(item), MidpointRounding.AwayFromZero); } } @@ -1310,7 +1324,6 @@ namespace YLErp.Modules.SwapModule /// public void AuotoSwapUnwind(int tradeid, decimal unwindPrice, decimal unwindPriceFee, decimal unwindNetFee, decimal unwindNet, DateTime valueDate, decimal unwindQty, decimal closeFee) { - unwindPrice = Math.Round(unwindPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); unwindPriceFee = decimal.Parse(unwindPriceFee.ToString("F10")); var td = DbContext.trade.Find(tradeid); var positions = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid); @@ -1319,6 +1332,10 @@ namespace YLErp.Modules.SwapModule var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == td.id); td.trade_extend = tradeExtend; var position = positions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial).FirstOrDefault(); + var storagePriceRound = ConsGlobal.InstrumentType.IsBond(position?.UnderlyingInstrumentType) + ? ConsGlobal.PriceRound + : ConsGlobal.SwapDeliveryPriceRound; + unwindPrice = Math.Round(unwindPrice, storagePriceRound, MidpointRounding.AwayFromZero); var preDealDate = GetPreDealDate(td.id, dealDate, eventTypes); swap_flow_event floatEvent = new swap_flow_event(); UnwindData unwindData = new UnwindData(); diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index cf1b3440..2266fcc5 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -32,13 +32,29 @@ namespace YLErp.Modules.SwapModule } + private int GetStorageDeliveryPriceRound(string underlyingInstrumentType, string underlyingCode) + { + if (ConsGlobal.InstrumentType.IsBond(underlyingInstrumentType)) + { + return ConsGlobal.PriceRound; + } + if (string.IsNullOrEmpty(underlyingCode)) + { + return ConsGlobal.SwapDeliveryPriceRound; + } + return GetUnderlyingData(underlyingCode)?.IsBond() == true + ? ConsGlobal.PriceRound + : ConsGlobal.SwapDeliveryPriceRound; + } + #region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变 /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) protected virtual void PersistEodSwapPosition(eod_swap_position position) { - position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); - position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode); + position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero); + position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.PosiNotionalValue = Math.Round(position.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); if (position.id == 0) { @@ -179,7 +195,7 @@ namespace YLErp.Modules.SwapModule { var normalizedPrice = BondPriceConverter.ToStorage(price); Log.Error($"互换债券日终价格按展示态返回,已转换为存储态: UnderlyingCode={code}, ValueDate={settleDate:yyyy-MM-dd}, PosiGrossPrice={posiGrossPrice}, SourcePrice={price}, NormalizedPrice={normalizedPrice}"); - return Math.Round(normalizedPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + return Math.Round(normalizedPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } return Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); @@ -1471,7 +1487,10 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.ContractSize = eventFlow.ContractSize; newEodPayPosition.CountRatio = eventFlow.CountRatio; newEodPayPosition.PosiNetPrice = netPrice; - newEodPayPosition.PosiGrossPrice = Math.Round(grossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + newEodPayPosition.PosiGrossPrice = Math.Round( + grossPrice, + GetStorageDeliveryPriceRound(eventFlow.UnderlyingInstrumentType, eventFlow.UnderlyingCode), + MidpointRounding.AwayFromZero); newEodPayPosition.PosiNetFeePrice = netFeePrice; newEodPayPosition.PosiNetNoFeePrice = netNoFeePrice; newEodPayPosition.PosiQuantity = payQty; @@ -1770,7 +1789,10 @@ namespace YLErp.Modules.SwapModule posiQty = 0; } curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty); - curretEod.PosiGrossPrice = Math.Round(curretEod.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + curretEod.PosiGrossPrice = Math.Round( + curretEod.PosiGrossPrice, + GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode), + MidpointRounding.AwayFromZero); curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty); curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty); @@ -1896,7 +1918,7 @@ namespace YLErp.Modules.SwapModule } if (data.IsBond()) { - return Math.Round(BondPrice(data, settleDate, out vobp), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + return Math.Round(BondPrice(data, settleDate, out vobp), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } var price = data.Price ?? 0; if (EodPriceQueryService.TryGetEodPrice(settleDate, code, out var eodPrice)) diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs index 9fafcdf7..2b28cca0 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowEventService.cs @@ -51,6 +51,21 @@ namespace YLErp.Modules.SwapModule protected virtual underlying_manager GetUnderlying(string underlyingCode) => DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + private int GetStorageDeliveryPriceRound(string underlyingInstrumentType, string underlyingCode) + { + if (ConsGlobal.InstrumentType.IsBond(underlyingInstrumentType)) + { + return ConsGlobal.PriceRound; + } + if (string.IsNullOrEmpty(underlyingCode)) + { + return ConsGlobal.SwapDeliveryPriceRound; + } + return GetUnderlying(underlyingCode)?.IsBond() == true + ? ConsGlobal.PriceRound + : ConsGlobal.SwapDeliveryPriceRound; + } + protected virtual DateTime GetNextBusinessDay(DateTime date) => QdpCalendarHelper.GetNonHoliday(date); @@ -63,7 +78,10 @@ namespace YLErp.Modules.SwapModule { if (!string.IsNullOrEmpty(evt.UnderlyingCode)) { - evt.TradingAmountAvg = Math.Round(evt.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + evt.TradingAmountAvg = Math.Round( + evt.TradingAmountAvg, + GetStorageDeliveryPriceRound(evt.UnderlyingInstrumentType, evt.UnderlyingCode), + MidpointRounding.AwayFromZero); } DbContext.swap_flow_event.Add(evt); } @@ -325,7 +343,7 @@ namespace YLErp.Modules.SwapModule DataState = 1, EventDate = flow_merge.OccurTime, UnwindDate = QdpCalendarHelper.GetNonHoliday(flow_merge.OccurTime.AddDays(1)), - TradingAmountAvg = Math.Round(TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + TradingAmountAvg = Math.Round(TradingAmountAvg, GetStorageDeliveryPriceRound(underlyingInstrumentType, flow_merge.UnderlyingCode), MidpointRounding.AwayFromZero), TradingAmountFeeAvg = TradingAmountFeeAvg, TradingAmountNetFeeAvg = TradingAmountNetFeeAvg, TradingAmountNetAvg = flow_merge.TradingAmountNetAvg, @@ -379,7 +397,10 @@ namespace YLErp.Modules.SwapModule DataState = 1, EventDate = flow_merge.OccurTime, UnwindDate = QdpCalendarHelper.GetNonHoliday(flow_merge.OccurTime.AddDays(1)), - TradingAmountAvg = Math.Round(flow_merge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + TradingAmountAvg = Math.Round( + flow_merge.TradingAmountAvg, + GetStorageDeliveryPriceRound(null, flow_merge.UnderlyingCode), + MidpointRounding.AwayFromZero), TradingAmountFeeAvg = flow_merge.TradingAmountFeeAvg, TradingFeePending = flow_merge.TradingFeePending, ClientId = flow_merge.ClientId @@ -413,7 +434,10 @@ namespace YLErp.Modules.SwapModule DataState = (int)SwapFlowDateStateEnum.完成, EventDate = td.TradeDate.Value, UnwindDate = td.StartDate.Value, - TradingAmountAvg = Math.Round(position.PosiGrossPrice, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + TradingAmountAvg = Math.Round( + position.PosiGrossPrice, + GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode), + MidpointRounding.AwayFromZero), TradingAmountFeeAvg = position.PosiNetPrice, TradingAmountNetFeeAvg = position.PosiNetFeePrice, TradingAmountNetAvg = position.PosiNetNoFeePrice, @@ -466,7 +490,7 @@ namespace YLErp.Modules.SwapModule DataState = 100, EventDate = td.TradeDate.Value, UnwindDate = QdpCalendarHelper.GetNonHoliday(td.TradeDate.Value.AddDays(1)), - TradingAmountAvg = Math.Round(flowMerge.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + TradingAmountAvg = Math.Round(flowMerge.TradingAmountAvg, GetStorageDeliveryPriceRound(underlyingInstrumentType, flowMerge.UnderlyingCode), MidpointRounding.AwayFromZero), TradingAmountFeeAvg = flowMerge.TradingAmountFeeAvg, TradingAmountNetFeeAvg = flowMerge.TradingAmountNetFeeAvg, TradingAmountNetAvg = flowMerge.TradingAmountNetAvg, diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs index 79b5ad5a..3ee3e891 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowImportService.cs @@ -118,7 +118,8 @@ namespace YLErp.Modules.SwapModule swap_flow.ytm = reader.GetDecimalOrPercent("成交收益率",false,true) ?? 0; swap_flow.TradingAmountNet = reader.GetDecimal("成交净价") ?? 0; swap_flow.TradingAmountNetFee = TradeFeeHelper.CalcPriceWithFee(swap_flow.TradingFee, swap_flow.TradingAmountNet??0, swap_flow.TradingQty, swap_flow.BsType); - if (underlying != null && underlying.IsBond()) + var isBond = underlying != null && underlying.IsBond(); + if (isBond) { // 成交流水债券报价(×100形式)转入库小数(×0.01),统一走 BondPriceConverter swap_flow.TradingAmountAvg = BondPriceConverter.ToStorage(swap_flow.TradingAmountAvg); @@ -129,7 +130,10 @@ namespace YLErp.Modules.SwapModule if (swap_flow.TradingAmountNetFee.HasValue) swap_flow.TradingAmountNetFee = BondPriceConverter.ToStorage(swap_flow.TradingAmountNetFee.Value); } - swap_flow.TradingAmountAvg = Math.Round(swap_flow.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swap_flow.TradingAmountAvg = Math.Round( + swap_flow.TradingAmountAvg, + isBond ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound, + MidpointRounding.AwayFromZero); if (!string.IsNullOrEmpty(clientName)) { var client = DataCacheProvider.GetClientDataSource().AsQueryable(x=>x.Name== clientName).FirstOrDefault(); diff --git a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs index 3cb640b9..430a5e53 100644 --- a/YLErpDAL/Modules/SwapModule/SwapFlowService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapFlowService.cs @@ -473,7 +473,7 @@ namespace YLErp.Modules.SwapModule swap_Flow.UnderlyingCode = req.UnderlyingCode; swap_Flow.BsType = req.BsType; swap_Flow.ContractSize = req.ContractSize; - swap_Flow.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swap_Flow.TradingAmountAvg = req.TradingAmountAvg; swap_Flow.TradingAmountFeeAvg = TradeFeeHelper.CalcPriceWithFee(req.TradingFee,req.TradingAmountAvg,req.TradingQty,req.BsType); swap_Flow.ClientId = req.ClientId; swap_Flow.ytm = req.ytm; @@ -540,7 +540,7 @@ namespace YLErp.Modules.SwapModule swap_Flow.BsType = req.BsType; swap_Flow.DataState = (int)SwapFlowDateStateEnum.等待完成; swap_Flow.ContractSize = req.ContractSize; - swap_Flow.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swap_Flow.TradingAmountAvg = req.TradingAmountAvg; swap_Flow.TradingAmountFeeAvg = req.TradingAmountFeeAvg; UpdateDbOption(swap_Flow); if (req.id == 0) @@ -695,7 +695,7 @@ namespace YLErp.Modules.SwapModule TradingFee = gourpItem.Sum(s => s.TradingFee), DataState = (int)SwapFlowDateStateEnum.等待完成, TradingAmountFeeAvg = gourpItem.Average(s => s.TradingAmountFeeAvg), - TradingAmountAvg = Math.Round(gourpItem.Average(s => s.TradingAmountAvg), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero), + TradingAmountAvg = Math.Round(gourpItem.Average(s => s.TradingAmountAvg), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero), ContractSize = swapflow.ContractSize }; UpdateDbOption(swap_flow_summary); @@ -757,7 +757,8 @@ namespace YLErp.Modules.SwapModule { throw new ServiceException("没有找到标的信息:" + req.UnderlyingCode); } - if (underlying != null && underlying.IsBond()) + var isBond = underlying.IsBond(); + if (isBond) { // 债券报价(×100)转入库小数(×0.01),价格字段统一走 BondPriceConverter req.TradingAmountAvg = BondPriceConverter.ToStorage(req.TradingAmountAvg); @@ -769,7 +770,10 @@ namespace YLErp.Modules.SwapModule // 数量×100(万手→手),与价格维度无关,保留常量 req.TradingQty *= ConsGlobal.bondShowPriceMultiple; } - req.TradingAmountAvg = Math.Round(req.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + req.TradingAmountAvg = Math.Round( + req.TradingAmountAvg, + isBond ? ConsGlobal.PriceRound : ConsGlobal.SwapDeliveryPriceRound, + MidpointRounding.AwayFromZero); } diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs index 1acab84e..410112e4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeAutoService.cs @@ -154,7 +154,7 @@ namespace YLErp.Modules.SwapModule swapFlow.DataState = (int)SwapFlowDateStateEnum.等待完成; } // 债券报价(×100)转入库小数(×0.01),统一走 BondPriceConverter - swapFlow.TradingAmountAvg = Math.Round(BondPriceConverter.ToStorage(item.deal_full_price ?? 0), ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swapFlow.TradingAmountAvg = Math.Round(BondPriceConverter.ToStorage(item.deal_full_price ?? 0), ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); swapFlow.TradingAmountFeeAvg = BondPriceConverter.ToStorage(item.deal_full_price_include_fee ?? 0); swapFlow.TradingAmount = swapFlow.TradingQty * swapFlow.ContractSize * swapFlow.TradingAmountAvg; swapFlow.ClientId = Convert.ToInt32(item.client_id ?? 0); @@ -451,7 +451,7 @@ namespace YLErp.Modules.SwapModule swap_flow_summary.FirstFlowTime = swapflow.OptTime; swap_flow_summary.SettleDate = gourpItem.Max(s => s.SettleDate); swap_flow_summary.TradingAmountAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.TradingAmountAvg * s.TradingQty) / swap_flow_summary.TradingQty; - swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountAvg : swap_flow_summary.TradingAmountAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountFeeAvg = Math.Round(swap_flow_summary.TradingAmountFeeAvg, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountNetAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.TradingAmountNet * s.TradingQty) / swap_flow_summary.TradingQty; @@ -508,7 +508,7 @@ namespace YLErp.Modules.SwapModule swap_flow_summary.SettleDate = gourpItem.Max(s => s.SettleDate); swap_flow_summary.TradingAmount = swap_flow_summary.TradingQty * swap_flow_summary.ContractSize; swap_flow_summary.TradingAmountAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.FullPrice * s.TradingQty) / swap_flow_summary.TradingQty; - swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + swap_flow_summary.TradingAmountAvg = Math.Round(swap_flow_summary.TradingAmountAvg, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); swap_flow_summary.TradingAmountFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountAvg : swap_flow_summary.TradingAmountAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountNetAvg = swap_flow_summary.TradingQty == 0 ? 0 : gourpItem.Sum(s => s.NetPrice * s.TradingQty) / swap_flow_summary.TradingQty; swap_flow_summary.TradingAmountNetFeeAvg = swap_flow_summary.TradingQty == 0 ? swap_flow_summary.TradingAmountNetAvg : swap_flow_summary.TradingAmountNetAvg + swap_flow_summary.TradingFeePending * tradeSide / swap_flow_summary.TradingQty; @@ -1082,7 +1082,7 @@ namespace YLErp.Modules.SwapModule var ratio = flowMergeClone.BsType == 1 ? 1 : -1; var oriRatio = flowMergeClone.BsType == 1 ? -1 : 1; flowMergeClone.TradingFeePending = flowMergeClone.TradingQty / origin.TradingQty * origin.TradingFeePending; - flowMergeClone.TradingAmountAvg = Math.Round(origin.TradingAmountAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); + flowMergeClone.TradingAmountAvg = Math.Round(origin.TradingAmountAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); flowMergeClone.TradingAmountNetAvg = origin.TradingAmountNetAvg + oriRatio * origin.TradingFeePending * 2 / origin.TradingQty; flowMergeClone.TradingAmountFeeAvg = flowMergeClone.TradingAmountAvg + ratio * flowMergeClone.TradingFeePending / flowMergeClone.TradingQty; diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index d48b339a..f8e5156e 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -62,6 +62,13 @@ namespace YLErp.Modules.SwapModule return roundedPrice; } + private static decimal? RoundSwapBondNetPriceAndYtm(decimal? value) + { + return value.HasValue + ? Math.Round(value.Value, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero) + : null; + } + #region 互换交易保存 /// /// 新版收益互换预付金校验 @@ -361,7 +368,7 @@ namespace YLErp.Modules.SwapModule td.ValidState = "Valid"; td.TradeSource = "系统交易"; td.TradeStatus = ConsTrade.确认成交; - td.InitYtm = flowMerge.InitYtm ?? 0; + td.InitYtm = RoundSwapBondNetPriceAndYtm(flowMerge.InitYtm) ?? 0; return td; } /// @@ -400,7 +407,7 @@ namespace YLErp.Modules.SwapModule OptId = UserInfo.UserId, OptName = UserInfo.UserName, UnderlyingInstrumentType = underlying.UnderlyingInstrumentType, - InitYtm = flowMerge.InitYtm + InitYtm = RoundSwapBondNetPriceAndYtm(flowMerge.InitYtm) }; td.swap_positions.Add(floatPosition); swap_position interestPosition = new swap_position() @@ -593,7 +600,7 @@ namespace YLErp.Modules.SwapModule dbTrade.trade_extend = req.trade_extend; dbTrade.swap_positions = req.swap_positions; dbTrade.MetaDic = req.MetaDic; - dbTrade.InitYtm = req.swap_positions.FirstOrDefault(p => p.InitYtm != null)?.InitYtm; + dbTrade.InitYtm = RoundSwapBondNetPriceAndYtm(req.swap_positions.FirstOrDefault(p => p.InitYtm != null)?.InitYtm); InnerSaveTrade(false, dbTrade, changsStr, changeConfirmStatus); return dbTrade; @@ -1380,7 +1387,7 @@ namespace YLErp.Modules.SwapModule position.PosiGrossPrice = Math.Round(swap.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.PosiNetPrice = swap.PosiQuantity == 0 ? 0 : (position.PosiGrossPrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio); position.PosiNetPrice = Math.Round(position.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); - position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice; + position.PosiNetNoFeePrice = RoundSwapBondNetPriceAndYtm(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 = Math.Round(swap.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); @@ -1405,10 +1412,10 @@ namespace YLErp.Modules.SwapModule position.FloatRateUnderlyingCode = swap.FloatRateUnderlyingCode; position.interest_rest_days = swap.interest_rest_days; position.interest_rule = swap.interest_rule; - position.InitYtm = swap.InitYtm; - if (swap.InitYtm != null && swap.InitYtm > 0) + position.InitYtm = RoundSwapBondNetPriceAndYtm(swap.InitYtm); + if (position.InitYtm != null && position.InitYtm > 0) { - td.InitYtm = swap.InitYtm; + td.InitYtm = position.InitYtm; } if (position.id == 0) diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml index c62ed4c9..8d8b244b 100644 --- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml +++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml @@ -455,10 +455,10 @@ - + - + diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 0e6a610d..a0057f28 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -79,7 +79,8 @@ const vue = new Vue({ return SwapCalc.getPriceScale(this.multiplier); }, getStorageDeliveryPrice() { - return _.round(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), 9); + const precision = inputFormatSwapDeliveryPrice.precision + (this.multiplier === 100 ? 2 : 0); + return SwapCalc.roundHalfAwayFromZero(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), precision); }, initDeal() { var positions = model.FlowEvents.filter((item) => { diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js index 5c974e69..6c9fcdf1 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js @@ -13,9 +13,10 @@ const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premium const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, negative: true, append: '' }); const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '', percent: false }); const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' }); -const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent:true }); const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: false }); const inputFormatSwapBondDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: true }); +const inputFormatSwapBondNetPriceAndYtm = Object.freeze({ precision: 9, negative: true, append: '', percent: true }); +const swapBondStoragePricePrecision = inputFormatSwapBondDeliveryPrice.precision + 2; const consUnderlyingFlagBase = (function () { let unSelFlag = tradeHelper.UnderlyingSelectFlag; @@ -221,6 +222,15 @@ const vue = new Vue({ }); }, methods: { + roundStorageDeliveryPrice(item, price) { + const precision = tradeHelper.IsBond(item && item.UnderlyingInstrumentType) + ? swapBondStoragePricePrecision + : inputFormatSwapDeliveryPrice.precision; + return _.round(Number(price), precision); + }, + roundStorageBondNetPriceAndYtm(value) { + return value == null ? value : _.round(Number(value), swapBondStoragePricePrecision); + }, getPosiPriceFormatKey(item, field) { const index = item && item.index != null ? item.index : ''; const isBond = tradeHelper.IsBond(item && item.UnderlyingInstrumentType); @@ -279,7 +289,7 @@ const vue = new Vue({ //计算数量 if (this.paySwapList.length > 0) { var item = this.paySwapList[0]; - var deliveryPrice = _.round(Number(item.PosiGrossPrice), 9); + var deliveryPrice = this.roundStorageDeliveryPrice(item, item.PosiGrossPrice); var notional = deliveryPrice * item.ContractSize; item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision); this.calcNotional(); @@ -361,7 +371,7 @@ const vue = new Vue({ } var national = payItem.PosiQuantity * payItem.ContractSize; // 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional - var deliveryPrice = _.round(Number(payItem.PosiGrossPrice), 9); + var deliveryPrice = this.roundStorageDeliveryPrice(payItem, payItem.PosiGrossPrice); var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数 this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional); payItem.PosiNotionalValue = this.trade.StockEqvNotional; @@ -506,7 +516,9 @@ const vue = new Vue({ errorcount++; return false; } - x.PosiGrossPrice = _.round(Number(x.PosiGrossPrice), 9); + x.PosiGrossPrice = thisObj.roundStorageDeliveryPrice(x, x.PosiGrossPrice); + x.PosiNetNoFeePrice = thisObj.roundStorageBondNetPriceAndYtm(x.PosiNetNoFeePrice); + x.InitYtm = x.InitYtm == null ? null : thisObj.roundStorageBondNetPriceAndYtm(x.InitYtm); thisObj.trade.swap_positions.push(x); }); } else { @@ -632,8 +644,8 @@ const vue = new Vue({ var thisObj = this; main.post("/pricing/AjaxGetUnderlyingPrice", { underlyingCode: underlyingCode, tradeDate: StartDate }) .done(function (resp) { - item.PosiNetNoFeePrice = otcformat.trading.umprice(resp.obj.netPrice); - item.PosiGrossPrice = _.round(Number(resp.obj.price), 9); + item.PosiNetNoFeePrice = thisObj.roundStorageBondNetPriceAndYtm(resp.obj.netPrice); + item.PosiGrossPrice = thisObj.roundStorageDeliveryPrice(item, resp.obj.price); thisObj.calcNotional(); }); }, diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 71553a15..3838b7ea 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -42,7 +42,8 @@ const vue = new Vue({ return this.multiplier == 100 ? 0.01 : 1; }, getStorageDeliveryPrice() { - return _.round(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), 9); + const precision = inputFormatSwapDeliveryPrice.precision + (this.multiplier === 100 ? 2 : 0); + return SwapCalc.roundHalfAwayFromZero(Number(this.floatPosition.TradingAmountAvg) * this.getPriceScale(), precision); }, initDeal() { var positions = model.FlowEvents.filter((item) => { From a91010589af99ff140eceb47b8c5468d1769c0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 14:16:50 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20Bonds=E6=9E=9A=E4=B8=BE=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=E5=80=BC=E6=94=B9=E5=9B=9EBond?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Framework/YLErp.Core/ConsGlobal.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/YLErp.Core/ConsGlobal.cs b/Framework/YLErp.Core/ConsGlobal.cs index 6469db05..049817e7 100644 --- a/Framework/YLErp.Core/ConsGlobal.cs +++ b/Framework/YLErp.Core/ConsGlobal.cs @@ -121,7 +121,7 @@ namespace YLErp public const string TBonds = "TBonds"; public const string CreditBonds = "CreditBonds"; //信用债 public const string OtherBonds = "OtherBonds"; //其它债券 - public const string Bonds = "Bonds"; //债券 + public const string Bonds = "Bond"; //债券 public const string GoldFutures = "GoldFutures"; public const string TBFutures = "TBFutures"; public const string OtherFutures = "OtherFutures"; From 32a3a45c85e87fdb68e18dc8f4ed3ff2aacb6513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 14:17:26 +0800 Subject: [PATCH 7/9] =?UTF-8?q?refactor(swap):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=80=BA=E5=88=B8=E6=97=A5=E7=BB=88=E4=BB=B7=E6=A0=BC=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 GetSwapValuationPrice 方法中的 posiGrossPrice 参数 - 删除债券价格转换相关的注释代码和日志记录 - 简化债券价格处理逻辑,直接使用原始价格进行四舍五入 - 更新所有调用 GetSwapValuationPrice 的地方以匹配新方法签名 - 移除多余的价格转换和展示态判断逻辑 --- .../SwapModule/SwapEodPositionService.cs | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 2266fcc5..59aa0707 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -175,27 +175,15 @@ namespace YLErp.Modules.SwapModule return UnderlyingCodePrice(code, settleDate, out vobp); } - /// - /// 获取用于互换浮动腿盯市的标的价格。 - /// - /// 普通债券类收益互换的新录入页面将全价按小数保存,例如页面录入 20% 后 PosiGrossPrice 为 0.2 - /// - /// 因此仅当交易期初价已经是小数口径、且当前债券价明显仍处于展示态时,再做 - /// 一次展示态到存储态转换。期初价本身是历史展示态口径的存量交易保持原价格, - /// 避免修改日终估值链路后改变其既有损益。 - /// - private decimal GetSwapValuationPrice(string code, decimal posiGrossPrice, DateTime settleDate, out decimal vobp) + /// 获取用于互换浮动腿盯市的标的价格。 + private decimal GetSwapValuationPrice(string code, DateTime settleDate, out decimal vobp) { var price = GetUnderlyingPrice(code, settleDate, out vobp); var underlying = GetUnderlyingData(code); - // var usesStoragePrice = Math.Abs(posiGrossPrice) < 2m; - // var usesDisplayPrice = Math.Abs(price) >= 10m; if (underlying?.IsBond() == true) { - var normalizedPrice = BondPriceConverter.ToStorage(price); - Log.Error($"互换债券日终价格按展示态返回,已转换为存储态: UnderlyingCode={code}, ValueDate={settleDate:yyyy-MM-dd}, PosiGrossPrice={posiGrossPrice}, SourcePrice={price}, NormalizedPrice={normalizedPrice}"); - return Math.Round(normalizedPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); + return Math.Round(price, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); } return Math.Round(price, ConsGlobal.SwapDeliveryPriceRound, MidpointRounding.AwayFromZero); @@ -1575,7 +1563,7 @@ namespace YLErp.Modules.SwapModule int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; - var price = GetSwapValuationPrice(eod.UnderlyingCode, eod.PosiGrossPrice, dealDate, out decimal vobp); + var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); decimal tax = um.ValueAddedTax ?? 0; if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0) @@ -1667,7 +1655,7 @@ namespace YLErp.Modules.SwapModule var dealDate = curretEod.ValueDate; int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; - var price = GetSwapValuationPrice(eod.UnderlyingCode, eod.PosiGrossPrice, dealDate, out decimal vobp); + var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp); var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents); var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice; decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio); @@ -1856,7 +1844,7 @@ namespace YLErp.Modules.SwapModule curretEod.ContractSize = position.ContractSize; curretEod.CountRatio = position.CountRatio; curretEod.PosiTradingFee = position.PosiTradingFee; - curretEod.UnderlyingPrice = GetSwapValuationPrice(position.UnderlyingCode, position.PosiGrossPrice, dealDate, out decimal vobp); + curretEod.UnderlyingPrice = GetSwapValuationPrice(position.UnderlyingCode, dealDate, out decimal vobp); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, position); curretEod.dv01 = Dv01Helper.CalcDv01(curretEod.UnderlyingCode, curretEod.PosiQuantity, curretEod.PosiDirection, curretEod.PositionType, vobp); //if (settleDate == td.TradeDate) From 7475bddc4483306ab5c4c80ad1c5e0aa1d0af3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 16:23:52 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix(tradeHelper):=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=8E=E7=AB=AFBonds->Bond=20=E9=87=87?= =?UTF-8?q?=E7=94=A8=E5=89=8D=E7=AB=AF=E5=85=BC=E5=AE=B9=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=80=BA=E5=88=B8=E7=B1=BB=E5=9E=8B=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了 Bond 类型到 IsBond 函数的判断条件中 - 确保 Bond 类型能够被正确识别为债券产品 --- YLErpWeb/wwwroot/Scripts/app/tradeHelper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js b/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js index 75701fcb..fa58d6ff 100644 --- a/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js +++ b/YLErpWeb/wwwroot/Scripts/app/tradeHelper.js @@ -276,6 +276,7 @@ tradeHelper.IsBond = function (instType) { switch (instType) { case "Bonds": + case "Bond": case "TBonds": case "CreditBonds": case "OtherBonds": From c4379d4b907224633705e59ce534c6dc80bb9466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Thu, 23 Jul 2026 17:49:02 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E6=AD=A3=E6=97=A5?= =?UTF-8?q?=E7=BB=88=E4=BF=9D=E8=AF=81=E9=87=91=E6=9C=AC=E9=87=91=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除保证金本金的1/10缩放处理,改为直接使用实际本金数值 - 修正了保证金本金计算中的除以10逻辑,确保显示正确的本金金额 - 保持利息计算使用原始本金累积值,避免破坏保证金利息金额准确性 - 更新注释说明,明确当前风险页按日终保证金 --- YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 59aa0707..6975a5ee 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -2327,24 +2327,23 @@ namespace YLErp.Modules.SwapModule item.PeriodPaymentValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL; } // eod_swap 的保证金本金来自 trade_span;缺少 span 数据时会被保存为 0。 - // 本风险页改按日终保证金腿展示,且该页面保证金本金采用原始本金的 1/10 口径。 - // 利息仍使用原始本金累积值,不能同步缩放,否则会破坏保证金利息金额。 + // 本风险页改按日终保证金腿的实际本金展示。 item.position.InitMarginGain = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.InterestDirection == (int)SwapDirectionEnum.收取) - .Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m; + .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.InitMarginLoss = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.InterestDirection == (int)SwapDirectionEnum.支付) - .Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m; + .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.PostionMarginGain = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.InterestDirection == (int)SwapDirectionEnum.收取) - .Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m; + .Sum(x => Math.Abs(x.InterestPrincipalFix)); item.position.PostionMarginLoss = marginLegs .Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.InterestDirection == (int)SwapDirectionEnum.支付) - .Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m; + .Sum(x => Math.Abs(x.InterestPrincipalFix)); // 保证金本金方向与我方的利息现金流方向相反:原始“收取”保证金 // 表示我方占用客户资金,应向客户支付利息;支付金额按负数展示。