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 = "确认提交平仓?";