From 20ea93d89ce231a336f44c73b601c282f61640d3 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 13:57:42 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=92=E6=8D=A2?= =?UTF-8?q?=E6=94=B6=E7=9B=8A=E7=BB=93=E7=AE=97=E6=9C=9F=E6=9C=AB=E4=BB=B7?= =?UTF-8?q?=E8=A2=AB=E9=94=99=E8=AF=AF=E7=BC=A9=E6=94=BE=E5=8F=8A=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a00fe3cc合并etf分支时,误把initDeal中TradingAmountAvg的×100缩放去掉, 导致期末标的价格停留在相对价态(1.02),提交时又被getPriceScale()÷100, 最终swap_flow_event.TradingAmountAvg入库为0.0102,且盈亏计算错误。 对齐平仓页(unwindSwapTrade.js)的正确实现: - initDeal: TradingAmountAvg = initPosiNetPrice × multiplier 转为界面百分比态 - SwapIncome表格期末价列: 不再经过priceFormat(避免二次×100),直接显示界面态值 --- YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml | 2 +- YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml index 7ff430fd..d86d8631 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml @@ -154,7 +154,7 @@ {{priceFormat(floatPosition.PosiGrossPrice)}} {{priceFormat(floatPosition.TradingAmountNetAvg)}} - {{priceFormat(floatPosition.TradingAmountAvg)}} + {{floatPosition.TradingAmountAvg}} {{floatPosition.Quantity}} diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 9cdd68fa..2293b45b 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -43,7 +43,8 @@ const vue = new Vue({ this.floatPosition = positions[0]; this.initPosiNetPrice = this.floatPosition.PosiNetPrice; // 互换标的价格固定为期初净价,与平仓不同不需要用户填写 - this.floatPosition.TradingAmountAvg = this.initPosiNetPrice; + // 期初净价入库为相对价(如1.02),需转换为界面百分比形态(102),与平仓页保持一致 + this.floatPosition.TradingAmountAvg = this.initPosiNetPrice * this.multiplier; this.interestList = model.FlowEvents.filter((item) => { return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9; }); From 6d17e95197340846ab6d0090de0dfad845957e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 14:33:01 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E6=94=B6=E7=9B=98=20=E5=88=86=E7=BA=A2?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=E4=B8=AD=E7=AE=97=E4=B8=8A?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E7=9A=84=E5=B7=B2=E5=AE=9E=E7=8E=B0=E7=9A=84?= =?UTF-8?q?=E5=88=86=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SwapModule/SwapEodPositionService.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 98691d38..54d26754 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1,6 +1,7 @@ using BaseOUDAL; using Newtonsoft.Json; using System.Linq.Expressions; +using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; using YLErp.Helpers; @@ -1484,12 +1485,16 @@ namespace YLErp.Modules.SwapModule curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend; // 分红与互换解耦:持仓>0时从起始日重算待实现分红,不再受互换事件影响 + // 修改,互换事件会影响待实现的分红的,现在要算上 if (curretEod.PosiQuantity > 0) { - decimal tax = um.ValueAddedTax ?? 0; - BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); - decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); - curretEod.PosiDividendSum = Math.Round(totalPayment / (1 + tax) * (1 - tax), 2); + //decimal tax = um.ValueAddedTax ?? 0; + //BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); + //decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, td.., shortRatio, directionRatio); + //var totalInterest = totalPayment / (1 + tax) * (1 - tax); + var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); + + curretEod.PosiDividendSum = eod.PosiDividendSum - consumedDividend - curretEod.TdCloseDividend; } else { @@ -1513,6 +1518,21 @@ namespace YLErp.Modules.SwapModule } return curretEod; } + + private decimal CalcConsumedDividend(eod_swap_position curretEod, List events) + { + decimal consumedDividend = 0; + + List swapEventTypes = new List() { (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 }; + //这里要剔除掉平仓产生的分红 + consumedDividend = events + .Where(x => x.SwapTradeId == curretEod.SwapTradeId + && swapEventTypes.Contains(x.EventType) + && x.DataState == (int)SwapFlowDateStateEnum.完成) + .Sum(s => s.DividendIn); + return consumedDividend; + } + /// /// 根据开平仓事件算价格及后付费用 /// @@ -1645,6 +1665,7 @@ namespace YLErp.Modules.SwapModule BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); decimal payment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, settleDate, curretEod.PosiQuantity, shortRatio, directionRatio); payment = Math.Round(payment / (1 + tax) * (1 - tax), 2); + //var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); 首日应该没有分红 curretEod.TdPosiDividend = payment; curretEod.PosiDividendSum = payment; } From bc6fed554d8eb02d1f5183faa42496abeb9de2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 14:33:50 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E5=B9=B3=E4=BB=93=E5=92=8C=E5=88=86?= =?UTF-8?q?=E7=BA=A2=E4=B8=AD=EF=BC=8C=E5=90=8E=E7=BB=AD=E5=B9=B3=E4=BB=93?= =?UTF-8?q?=E5=88=86=E7=BA=A2=E8=AE=A1=E7=AE=97=E4=B8=8D=E5=AF=B9=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 21243e1b..573e75ff 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -280,10 +280,13 @@ const vue = new Vue({ let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1; var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate } main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) { - let totalDividend = parseFloat(thisObj.deal.CloseQty) * resp.obj.totalInterest * ratio * floatRatio; let consumedDividend = parseFloat(resp.obj.consumedDividend ?? 0); + let consumedDividendInterest = consumedDividend / parseFloat(thisObj.deal.NotionalQty) + // 这里计算已经分红的利息,从totalInterest里扣除 + let totalDividend = parseFloat(thisObj.deal.CloseQty) * (resp.obj.totalInterest - consumedDividendInterest) * ratio * floatRatio; // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0 - thisObj.floatPosition.DividendIn = Math.abs(consumedDividend) > 0 ? parseFloat((totalDividend - consumedDividend).toFixed(2)) : parseFloat(totalDividend.toFixed(2)); + // 现在可能做了纯分红的互换结算,所以不能直接归0 + thisObj.floatPosition.DividendIn = parseFloat(totalDividend.toFixed(2)); var posiQty = parseFloat(thisObj.floatPosition.Quantity) - parseFloat(thisObj.deal.CloseQty); thisObj.floatPosition.DividendPending = parseFloat((posiQty * resp.obj.totalInterest * ratio * floatRatio).toFixed(2)); thisObj.calcFloatClosePnl(); From ca68178bf8febefaebb48b36d64ed552ff2a9aa0 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 14:24:30 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=92=E6=8D=A2?= =?UTF-8?q?=E6=94=B6=E7=9B=8A=E7=BB=93=E7=AE=97=E6=9C=9F=E5=88=9D=E5=87=80?= =?UTF-8?q?=E4=BB=B7=E6=98=BE=E7=A4=BA0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 期初净价列取TradingAmountNetAvg字段,后端InitIncome赋值为 position.PosiNetNoFeePrice(可空)。部分交易录入时未填该字段(值为0), 导致期初净价显示为0(如trade 1892/1859)。 改为:TradingAmountNetAvg为0时回退使用PosiNetPrice(期初净价,始终有值)。 仅前端兜底,不改动后端及平仓页。 --- YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml index d86d8631..17e8aeb2 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml @@ -152,7 +152,7 @@ {{floatPosition.UnderlyingCode}} {{priceFormat(floatPosition.PosiGrossPrice)}} - {{priceFormat(floatPosition.TradingAmountNetAvg)}} + {{priceFormat(floatPosition.TradingAmountNetAvg > 0 ? floatPosition.TradingAmountNetAvg : floatPosition.PosiNetPrice)}} {{floatPosition.TradingAmountAvg}} From 276c1c2961fba0f56f33b9bfd63f84225f6e96ce Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 14:31:52 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=92=E6=8D=A2?= =?UTF-8?q?=E6=94=B6=E7=9B=8A=E7=BB=93=E7=AE=97=E6=9C=9F=E6=9C=AB=E4=BB=B7?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E7=BC=96=E8=BE=91=E5=8F=8A=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ab09cbc9为支持自动互换期间付息,误将期末标的价格改为纯文本不可编辑, 并删除了changeUnderlyingPrice/refreshUnderlyingPrice方法。开发误解了需求: 自动互换默认填上次互换价是后端逻辑,手动互换时期末价应可编辑+刷新(与平仓页一致)。 对齐平仓页(SwapUnwind)恢复: - incomeSwapTrade.js: 恢复changeUnderlyingPrice、refreshUnderlyingPrice方法 - SwapIncome.cshtml: 期末价列恢复vue-number-input输入框+刷新图标 - SwapDealService.cs/InitIncome: 补充注释说明TradingAmountNetAvg字段 虽名为期末语义但此处装入期初净价,避免后续混淆 --- YLErpDAL/Modules/SwapModule/SwapDealService.cs | 1 + YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml | 6 +++++- .../Scripts/app/swaptrade/incomeSwapTrade.js | 14 +++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index afd9aa15..10a1af7d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -276,6 +276,7 @@ namespace YLErp.Modules.SwapModule floatEvent.PayDirection = position.PosiDirection; floatEvent.PosiGrossPrice = position.PosiGrossPrice; floatEvent.PosiNetPrice = position.PosiNetPrice; + // 注意:TradingAmountNetAvg 字段名为"成交净价(期末语义)",但收益结算/平仓初始化时装入的是期初净价(PosiNetNoFeePrice),前端展示期初净价时取此字段 floatEvent.TradingAmountNetAvg = position.PosiNetNoFeePrice; floatEvent.TradingAmountNetFeeAvg = position.PosiNetFeePrice; floatEvent.PositionType = position.PositionType; diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml index 17e8aeb2..ca19bcc7 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml @@ -152,9 +152,13 @@ {{floatPosition.UnderlyingCode}} {{priceFormat(floatPosition.PosiGrossPrice)}} + {{priceFormat(floatPosition.TradingAmountNetAvg > 0 ? floatPosition.TradingAmountNetAvg : floatPosition.PosiNetPrice)}} - {{floatPosition.TradingAmountAvg}} + + + + {{floatPosition.Quantity}} diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js index 2293b45b..311f90ef 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js @@ -97,11 +97,23 @@ const vue = new Vue({ } if (!isUseApproval) { this.getInterestList(); - //this.refreshUnderlyingPrice(); } else { this.dataFormat(); } }, + changeUnderlyingPrice() {//修改标的价格 + this.calcFloatClosePnl(); + }, + refreshUnderlyingPrice() {//刷新标的价格 + var thisObj = this; + main.post("/underlying_manager/GetUnderlyingPriceByCode", + { 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.calcFloatClosePnl(); + }); + }, changeTradingFee() {//修改交易费用 this.calcFloatClosePnl(); }, From aadba136c8a548a5713619a98dba98548d3e7a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 15:26:40 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E5=B9=B3=E4=BB=93=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E4=BA=92=E6=8D=A2=E5=88=86=E7=BA=A2=E5=88=A9=E6=81=AF=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=96=B9=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 573e75ff..41188b2d 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -281,7 +281,7 @@ const vue = new Vue({ var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate } main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) { let consumedDividend = parseFloat(resp.obj.consumedDividend ?? 0); - let consumedDividendInterest = consumedDividend / parseFloat(thisObj.deal.NotionalQty) + let consumedDividendInterest = -1 * consumedDividend / parseFloat(thisObj.deal.NotionalQty)//调整一下方向 // 这里计算已经分红的利息,从totalInterest里扣除 let totalDividend = parseFloat(thisObj.deal.CloseQty) * (resp.obj.totalInterest - consumedDividendInterest) * ratio * floatRatio; // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0 From 25626b845ed7f310d6a54b1b891722e560d351f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 15:38:48 +0800 Subject: [PATCH 07/12] =?UTF-8?q?PosiDividendSum=20=E6=89=A3=E9=99=A4?= =?UTF-8?q?=E4=BA=92=E6=8D=A2=E5=88=86=E7=BA=A2=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E6=96=B9=E5=90=91=E6=9C=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 54d26754..a00e05fb 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1492,7 +1492,7 @@ namespace YLErp.Modules.SwapModule //BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); //decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, td.., shortRatio, directionRatio); //var totalInterest = totalPayment / (1 + tax) * (1 - tax); - var consumedDividend = CalcConsumedDividend(curretEod, unwindEvents); + var consumedDividend = - 1 * CalcConsumedDividend(curretEod, unwindEvents); curretEod.PosiDividendSum = eod.PosiDividendSum - consumedDividend - curretEod.TdCloseDividend; } From 90b669222228ec910b7a519a7e609e9226eeab79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 17:55:41 +0800 Subject: [PATCH 08/12] =?UTF-8?q?#EQD-6290=20=E6=97=A5=E7=BB=88=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E9=A3=8E=E9=99=A9-=E4=BA=92=E6=8D=A2=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E4=BB=B7=E5=80=BC=E5=92=8C=E5=B7=B2=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=9B=88=E4=BA=8F=E4=B8=8D=E6=AD=A3=E7=A1=AE=20;=20=E6=9C=AA?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9A=84=E5=88=86=E7=BA=A2=E5=88=B0=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E6=9C=AA=E5=8E=BB=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + EOF | 0 .../SwapModule/SwapEodPositionService.cs | 27 ++-- YLErpWeb/App_Data/Config/otcformat.js | 125 +++++++++++++++++- 4 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 EOF diff --git a/.gitignore b/.gitignore index 0f2a2af1..0edc3f1b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.suo *.user *.sln.docstates +*.lscache # Build results [Dd]ebug/ diff --git a/EOF b/EOF new file mode 100644 index 00000000..e69de29b diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index a00e05fb..e5792425 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1,5 +1,6 @@ using BaseOUDAL; using Newtonsoft.Json; +using NPOI.POIFS.Properties; using System.Linq.Expressions; using YLErp.DBModels; using YLErp.DBModels.Consts; @@ -471,12 +472,13 @@ namespace YLErp.Modules.SwapModule eodPosi.TdCloseDividend += eodPosi.PosiDividendSum; eodPosi.TdPosiDividend = 0; eodPosi.RealizedDividend += eodPosi.PosiDividendSum; - eodPosi.PosiDividendSum = 0; - eodPosi.PosiProfitSum -= eodPosi.TdCloseDividend; //互换持仓价值要去掉已实现的 eodPosi.SwapPositionValue -= eodPosi.PosiDividendSum; //已实现盈亏要加上已实现的 eodPosi.RealizedPnl += eodPosi.PosiDividendSum; + eodPosi.PosiDividendSum = 0; + eodPosi.PosiProfitSum -= eodPosi.TdCloseDividend; + } UnwindData unwindData = new UnwindData(); @@ -1460,6 +1462,12 @@ namespace YLErp.Modules.SwapModule int shortRatio = eod.PositionType == (int)PositionTypeFlag.Long ? 1 : -1; int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; var price = UnderlyingCodePrice(eod.UnderlyingCode, dealDate, out decimal vobp); + var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents); + var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice; + BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); + decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio); + decimal tax = um.ValueAddedTax ?? 0; + decimal totalInterest = totalPayment / (1 + tax) * (1 - tax); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition); curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); curretEod.UnderlyingPrice = price; @@ -1467,21 +1475,20 @@ namespace YLErp.Modules.SwapModule curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio; curretEod.TdPosiDividend = 0; // 分红与互换无关,只要持仓>0且起始日早于当前日,正常计算当日分红 + // 修改,互换事件会影响待实现的分红的,现在要算上 if (valueDate > td.StartDate.Value && (curretEod.PosiQuantity > 0)) { - decimal tax = um.ValueAddedTax ?? 0; - BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); decimal payment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, eod.ValueDate, valueDate, curretEod.PosiQuantity, shortRatio, directionRatio); curretEod.TdPosiDividend = Math.Round(payment / (1 + tax) * (1 - tax), 2); } curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; - + // 当日浮动端平仓盈亏·分红(仅来自平仓事件 和 互换 中已实现的分红) + curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn); curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; curretEod.RealizedPnl = eod.RealizedPnl + curretEod.TdCloseMtmPnl; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var closeQty = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList().Sum(s => s.Quantity); - // 当日浮动端平仓盈亏·分红(仅来自平仓事件中已实现的分红) - curretEod.TdCloseDividend = unwindEvents.Sum(e => e.DividendIn); + curretEod.RealizedDividend = curretEod.RealizedDividend + curretEod.TdCloseDividend; // 分红与互换解耦:持仓>0时从起始日重算待实现分红,不再受互换事件影响 @@ -1490,11 +1497,9 @@ namespace YLErp.Modules.SwapModule { //decimal tax = um.ValueAddedTax ?? 0; //BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); - //decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, td.., shortRatio, directionRatio); + //decimal totalPayment = bondPaymentService.CalcPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio); //var totalInterest = totalPayment / (1 + tax) * (1 - tax); - var consumedDividend = - 1 * CalcConsumedDividend(curretEod, unwindEvents); - - curretEod.PosiDividendSum = eod.PosiDividendSum - consumedDividend - curretEod.TdCloseDividend; + curretEod.PosiDividendSum = Math.Round(totalInterest,2) - curretEod.RealizedDividend; } else { diff --git a/YLErpWeb/App_Data/Config/otcformat.js b/YLErpWeb/App_Data/Config/otcformat.js index 92b1f226..d4c79353 100644 --- a/YLErpWeb/App_Data/Config/otcformat.js +++ b/YLErpWeb/App_Data/Config/otcformat.js @@ -1,2 +1,125 @@ var main = main || {}; -main.formatOptions = { "trading": { "umprice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "umpriceP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "umpricePR": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradeSinglePrice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "premiumRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "premiumRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradePrice": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "StockEqvNotional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notionalP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "volatility": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "volatilityP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "greek": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "marginRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "marginRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 } } }; \ No newline at end of file +main.formatOptions={ + "trading": { + "umprice": { + "precision": 9, + "grouping": true, + "rounded": true, + "percent": false, + "minDecimals": 9, + "maxDecimals": 0 + }, + "umpriceP": { + "percent": true, + "precision": 2, + "grouping": false, + "rounded": true, + "minDecimals": 2, + "maxDecimals": 0 + }, + "umpricePR": { + "precision": 4, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 4, + "maxDecimals": 2 + }, + "tradeSinglePrice": { + "precision": 2, + "grouping": true, + "rounded": true, + "percent": false, + "minDecimals": 2, + "maxDecimals": 0 + }, + "premiumRateP": { + "percent": true, + "precision": 2, + "grouping": false, + "rounded": true, + "minDecimals": 2, + "maxDecimals": 0 + }, + "premiumRate": { + "precision": 4, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 4, + "maxDecimals": 2 + }, + "tradePrice": { + "precision": 2, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 2, + "maxDecimals": 0 + }, + "StockEqvNotional": { + "precision": 2, + "grouping": true, + "rounded": true, + "percent": false, + "minDecimals": 2, + "maxDecimals": 0 + }, + "notional": { + "precision": 2, + "grouping": true, + "rounded": true, + "percent": false, + "minDecimals": 2, + "maxDecimals": 0 + }, + "notionalP": { + "percent": true, + "precision": 2, + "grouping": false, + "rounded": true, + "minDecimals": 2, + "maxDecimals": 0 + }, + "volatility": { + "precision": 4, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 4, + "maxDecimals": 2 + }, + "volatilityP": { + "percent": true, + "precision": 2, + "grouping": false, + "rounded": true, + "minDecimals": 2, + "maxDecimals": 0 + }, + "greek": { + "precision": 2, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 2, + "maxDecimals": 0 + }, + "marginRateP": { + "percent": true, + "precision": 2, + "grouping": false, + "rounded": true, + "minDecimals": 2, + "maxDecimals": 0 + }, + "marginRate": { + "precision": 4, + "grouping": false, + "rounded": true, + "percent": false, + "minDecimals": 4, + "maxDecimals": 2 + } + } +}; \ No newline at end of file From 0ab6091962cd875083f3dcbee6a9f178feb1c11d Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 18:08:39 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=86=E7=BA=A2?= =?UTF-8?q?=E5=9E=8B=E8=87=AA=E5=8A=A8=E4=BA=92=E6=8D=A2=E5=9C=A8=E4=BA=92?= =?UTF-8?q?=E6=8D=A2Tab=E6=98=BE=E7=A4=BA=E6=9C=9F=E5=88=9D/=E6=9C=9F?= =?UTF-8?q?=E6=9C=AB=E4=BB=B7=E6=9C=AA=C3=97100=E5=8F=8A=E6=9C=9F=E5=88=9D?= =?UTF-8?q?=E5=87=80=E4=BB=B7=E4=B8=BA0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DealDividends生成分红型自动互换事件时,漏赋了UnderlyingInstrumentType和 TradingAmountNetAvg两个字段,导致互换Tab(TradeView)显示异常: 1. UnderlyingInstrumentType缺失:TradeView第654行IsBond判断为false, multiplier=1,走了非债券分支(不×100),使期初/期末价显示为相对价(1.00) 而非百分比(100%)。补赋eodPosi.UnderlyingInstrumentType后, 与手动互换一致走债券分支(×100)。 2. TradingAmountNetAvg缺失:TradeView第993行显示期初净价取该字段, 缺失导致显示0。补赋eodPosi.PosiNetNoFeePrice修复。 入库的TradingAmountAvg(相对价)本来就正确,仅显示层判断缺字段。 仅改动DealDividends,不影响平仓/手动互换/显示层逻辑。 --- YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index e5792425..a33f9e92 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -450,12 +450,14 @@ namespace YLErp.Modules.SwapModule PayDate = dividendPayDate, PositionId = eodPosi.PositionId, UnderlyingCode = eodPosi.UnderlyingCode, + UnderlyingInstrumentType = eodPosi.UnderlyingInstrumentType, PayDirection = eodPosi.PosiDirection, PositionType = eodPosi.PositionType, PositionQty = eodPosi.PosiQuantity, Quantity = 0, ContractSize = eodPosi.ContractSize, TradingAmountAvg = eodPosi.PosiNetPrice, + TradingAmountNetAvg = eodPosi.PosiNetNoFeePrice, PosiGrossPrice = eodPosi.PosiGrossPrice, PosiNetPrice = eodPosi.PosiNetPrice, MarkClosePnl = eodPosi.PosiDividendSum, From 818619bc70e2f7e90b56ac34169572b45332140a Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 19:15:27 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E8=A1=A5=E5=85=85DV01=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9A=84=E6=B3=A8=E9=87=8A:=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E8=BE=93=E5=87=BA=E5=8D=95=E4=BD=8D=E4=B8=BA=E4=B8=87?= =?UTF-8?q?=E5=85=83=E5=8F=8A=E5=85=A5=E5=8F=82=E5=8D=95=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dv01Helper.CalcDv01 与业界标准一致,但输出单位是万元而非元,且 PosiQuantity须为元面值单位。原注释未说明这些口径,易导致误读。 补充: - 类注释:写明公式等价业界标准DV01(元)/10000,输出万元 - posiQuantity入参:标注须为元面值单位 - vobp入参:标注为每100元面值1bp的价格变动 - 返回值:标注单位为万元 - 券面总额计算行:标注交易数量单位为元面值 仅注释改动,不改逻辑。 --- YLErpDAL/Helpers/Dv01Helper.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/YLErpDAL/Helpers/Dv01Helper.cs b/YLErpDAL/Helpers/Dv01Helper.cs index 70c0b370..408bdd2c 100644 --- a/YLErpDAL/Helpers/Dv01Helper.cs +++ b/YLErpDAL/Helpers/Dv01Helper.cs @@ -7,7 +7,10 @@ namespace YLErp.Helpers { /// /// DV01计算工具类 - /// DV01 = 方向 * (券面总额(万元) / 单位面值) * 中债基点价值 + /// 公式: DV01(万元) = 方向 * (持仓面值(万元) / 单位面值) * 中债基点价值 + /// 等价业界标准: DV01(元) = 持仓面值(元) / 100 * vobp,本方法输出为该值的 1/10000(即万元)。 + /// 注意: PosiQuantity 入参须为"元面值"单位(由 PosiNotionalValue/PosiGrossPrice 反推); + /// 单位面值默认 100,取自 UnderlyingBond.Price;vobp 为"每100元面值1bp的价格变动"。 /// public static class Dv01Helper { @@ -15,11 +18,11 @@ namespace YLErp.Helpers /// 计算DV01 /// /// 标的代码 - /// 持仓数量 + /// 持仓数量,须为"元面值"单位(如1000000=100万元面值) /// 收支方向: 1=收取, 2=支付 /// 多空方向: 1=多头, 2=空头 - /// 中债基点价值 - /// DV01值 + /// 中债基点价值(每100元面值,收益率变动1bp的价格变动) + /// DV01值,单位为"万元" public static decimal CalcDv01(string underlyingCode, decimal posiQuantity, int posiDirection, int positionType, decimal vobp) { if (vobp == 0 || posiQuantity == 0) return 0; @@ -32,7 +35,7 @@ namespace YLErp.Helpers int direction = (posiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1) * (positionType == (int)PositionTypeFlag.Long ? 1 : -1); - // 券面总额(万元) = 交易数量 / 10000 + // 券面总额(万元) = 交易数量(元面值) / 10000 double faceValueWan = Convert.ToDouble(posiQuantity) / 10000d; // 单位面值, 默认100 From 697c7babf2c01b5de2febb5f67b0c501af1b9c12 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 25 Jun 2026 19:15:54 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E6=A1=A3:?= =?UTF-8?q?=E4=BA=92=E6=8D=A2=E4=BA=A4=E6=98=93=E4=BB=B7=E6=A0=BC=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=AD=98=E5=82=A8=E4=B8=8E=E6=98=BE=E7=A4=BA=E8=A7=84?= =?UTF-8?q?=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 汇总互换价格显示Bug排查结论,供后续维护参考: - 核心规则:债券类入库相对价(÷100)、展示×100;非债券类原样 - ÷100机制:由FastVue.parseNumber实现(percent:true时number/100) - 关键字段映射:swap_position/swap_flow_event/eod_swap_position (标注TradingAmountNetAvg名像期末实装期初、PosiGrossPrice在 event表为NotMapped等易混点) - 互换事件生成路径:手动/自动互换-分红型/利息端 - 已修复Bug与教训(4个) - 附录A:排查工具箱SQL(5个,期初持仓/事件入库值/分红数据源/ 中债估值/脏数据修复) - 附录B:字段命名与数据流向速查表 --- 项目文档/互换交易价格字段存储与显示规范.md | 308 +++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 项目文档/互换交易价格字段存储与显示规范.md diff --git a/项目文档/互换交易价格字段存储与显示规范.md b/项目文档/互换交易价格字段存储与显示规范.md new file mode 100644 index 00000000..aab923ef --- /dev/null +++ b/项目文档/互换交易价格字段存储与显示规范.md @@ -0,0 +1,308 @@ +# 互换交易价格字段存储与显示规范 + +> 本文档总结互换交易(收益互换)中"标的价格"类字段在**录入、入库、展示**三个环节的处理规则,以及债券类与普通(非债券)类的差异。 +> 该规则是 2026-06 一系列价格显示 Bug 排查后总结的结论,供后续开发维护参考,避免再次出现"多除/少乘 100"类问题。 + +--- + +## 一、核心规则速查 + +| 标的类型 | 录入存库 | 入库基准 | 展示规则 | +|---------|---------|---------|---------| +| **普通债券类收益互换**(债券标的,如 180205.IB) | 用户输入值 ÷ 100 存库(**相对价**) | 相对价,如 `1.02`(对应输入 102) | 相对价 × 100 展示,如 `102.00000%` | +| **普通收益互换**(非债券标的,如 ETF、商品) | 用户输入值原样存库 | 绝对价,如 `100`(对应输入 100) | 原样展示,如 `100.00000` | + +**一句话**:债券类的价格,库里存"相对价(÷100)",界面显示时"×100 还原"。普通非债券类输入什么就存什么、显示什么。 + +--- + +## 二、为什么债券类要 ÷100 / ×100 + +债券价格习惯用"面值的百分比"表达(全价 102 元 = 面值 100 的 102%)。 +为了与其他利率/收益率类字段在库里保持"小数形式(1.02)"统一,录入时由 `vue-number-input` 组件自动 ÷100 转成相对价存库;展示时再 ×100 还原成用户习惯的百分比形式。 + +--- + +## 三、关键字段与所在表 + +互换涉及的价格字段命名容易混淆(有的字段名是"期末语义"但实际装的是"期初值"),现梳理如下: + +### swap_position 表(期初持仓,`IsInitial=1`) + +| 字段 | 含义 | 备注 | +|------|------|------| +| `PosiGrossPrice` | 期初全价(不含费) | 相对价基准(债券为 1.02 形式) | +| `PosiNetPrice` | 期初净价(含费) | 相对价基准 | +| `PosiNetNoFeePrice` | 成交净价(不含费),**可空** | 收益结算页"期初净价"列取此字段;为空时需回退到 `PosiNetPrice` | +| `PosiNetFeePrice` | 成交净价(含费),**可空** | | + +### swap_flow_event 表(互换/平仓/自动互换事件) + +| 字段 | 含义 | 是否入库 | 备注 | +|------|------|---------|------| +| `TradingAmountAvg` | 成交均价(期末全价) | ✅ 入库 | 债券类存相对价 | +| `TradingAmountNetAvg` | 成交净价(不含费) | ✅ 入库(可空) | 字段名像"期末",但收益结算初始化时装的是**期初净价** | +| `TradingAmountNetFeeAvg` | 成交净价(含费) | ✅ 入库 | | +| `TradingAmount` | 成交金额 | ✅ 入库 | = `TradingAmountAvg × Quantity × ContractSize`(后端计算) | +| `PosiGrossPrice` | 期初全价 | ❌ `[NotMapped]` 不入库 | 运行时从 `swap_position` 拷贝 | +| `PosiNetPrice` | 期初净价 | ❌ `[NotMapped]` 不入库 | 运行时从 `swap_position` 拷贝 | +| `UnderlyingInstrumentType` | 标的类型 | ✅ 入库 | **展示时判断是否债券(决定 ×100)的关键字段,生成事件时必须赋值** | + +> ⚠️ `PosiGrossPrice` / `PosiNetPrice` 在 `swap_flow_event` 里是 `[NotMapped]`,**数据库查不到这两列**,只在运行时从 `swap_position` 拷贝。所以"期初价"展示的实际数据源是 `swap_position`,不是 `swap_flow_event`。 + +### eod_swap_position 表(日终持仓) + +`PosiDividendSum`(浮动端未实现分红收益)≠ 0 时,会触发分红型自动互换(`DealDividends`)。 + +--- + +## 四、各环节的代码实现位置 + +### 1. 录入(TradeEdit.cshtml + swapTradeEdit.js) + +债券类与普通类用**不同的输入格式器**(只是配置开关),真正的 ÷100 转换在 `FastVue.parseNumber` 里执行: + +```html + + + + + +``` + +格式器定义(swapTradeEdit.js,仅配置开关): +- `inputFormatMarginRateNoPercent`:`percent: true`(注意:名字里的 "NoPercent" 指"显示时不带%号",而非"不做换算",容易误解) +- `inputFormatTradeSinglePrice`:`percent: false` + +**÷100 的真正实现**(`statics/bundles/bundle.js` 的 `FastVue.parseNumber`): +```js +FastVue.parseNumber = function (val, percent) { + // ... + let isPercent = val.endsWith('%') || percent == true; // format.percent=true 时 + let number = new Number(val.replace(/%+$/, '')); + return isPercent ? number / 100 : number; // ← ÷100 在这里执行 +}; +``` +即:组件本身基于第三方库(打包进 bundle.js),但 `percent:true` 时 `number / 100` 的换算是项目自定义的 `FastVue.parseNumber` 实现的,不是第三方库原生行为。 + +### 2. 展示(TradeView.cshtml) + +互换 Tab(浮动收益端)对期初/期末价的处理,**依赖 `UnderlyingInstrumentType` 判断是否债券**: + +```cshtml +@{ + // 关键: multiplier 由标的类型决定, 债券=100, 非债券=1 + var multiplier = ConsGlobal.InstrumentType.IsBond(closeFloat.UnderlyingInstrumentType) ? 100 : 1; +} +@if (isBond) { + @((closeFloat.PosiGrossPrice * multiplier).OtcFormat(OtcFormatFlag.umprice)) + @((closeFloat.TradingAmountAvg * multiplier).OtcFormat(OtcFormatFlag.umprice)) +} else { + @(closeFloat.PosiGrossPrice.OtcFormat(OtcFormatFlag.umprice)) + @(closeFloat.TradingAmountAvg.OtcFormat(OtcFormatFlag.umprice)) +} +``` + +> 🔑 **`UnderlyingInstrumentType` 是展示层判断的关键**。所有生成 `swap_flow_event` 的地方都必须赋值这个字段,否则 `IsBond` 判断失败,债券标的会走非债券分支(不 ×100),导致显示成相对价(1.00 而非 100.00)。 + +### 3. 收益结算页(SwapIncome.cshtml + incomeSwapTrade.js) + +前端 `priceFormat()` 同样 `× multiplier`: + +```js +priceFormat(price) { + price = price * this.multiplier; // 债券类 ×100 + return otcformat.trading.umprice(price); +} +``` + +--- + +## 五、互换事件的生成路径与价格赋值 + +互换 Tab 会按时间序列展示所有互换事件(EventType=互换/自动互换),手动与自动用**同一套渲染代码**,所以入库基准必须一致。 + +### 手动互换(EventType=3) +- 入口:`SwapDealService.SwapUnwind` / `InitIncome` +- 价格赋值:`floatEvent.TradingAmountAvg = unwindPrice`(前端用户输入,已 ÷100) +- **已赋值 `UnderlyingInstrumentType`**(来自 position) + +### 自动互换-分红型(EventType=4)⚠️ 重点 +- 入口:`SwapEodPositionService.DealDividends`(`PosiDividendSum != 0` 触发) +- 价格赋值:`TradingAmountAvg = eodPosi.PosiNetPrice`(相对价,来自 eod 持仓) +- **必须赋值 `UnderlyingInstrumentType` 和 `TradingAmountNetAvg`**(曾经遗漏导致 Bug) + +### 自动互换-利息端(EventType=4) +- 入口:`DealAutoInterests` → `SaveAutoEodWithCloseInterestPosition` +- `UnderlyingCode` 为空(利息腿),不出现在互换 Tab 浮动端 + +--- + +## 六、已修复的 Bug 与教训 + +| Bug | 根因 | 修复提交 | +|-----|------|---------| +| 收益结算期末价缩放/显示错误 | `incomeSwapTrade.js` initDeal 合并冲突丢 ×multiplier | 已修 | +| 收益结算期初净价显示 0 | `PosiNetNoFeePrice` 可空,为 0 时未回退 `PosiNetPrice` | 已修 | +| 收益结算期末价不可编辑/无刷新图标 | `ab09cbc9` 误删输入框与方法 | 已修 | +| **分红型自动互换显示未 ×100** | `DealDividends` 漏赋 `UnderlyingInstrumentType` | 已修(d9c5ce4c)| +| **分红型自动互换期初净价显示 0** | `DealDividends` 漏赋 `TradingAmountNetAvg` | 已修(d9c5ce4c)| + +### 教训 + +1. **生成 `swap_flow_event` 时必须赋全字段**,尤其是 `UnderlyingInstrumentType`——它是展示层判断债券/非债券(决定 ×100)的关键,遗漏会导致显示数量级错误。 +2. **手动互换与自动互换共用同一套展示代码**,入库基准(相对价)必须保持一致。 +3. **字段命名误导**:`TradingAmountNetAvg` 名字像"期末",但收益结算初始化时装的是期初净价,改代码时务必核对实际数据流向。 +4. **可空字段要兜底**:`PosiNetNoFeePrice`、`TradingAmountNetAvg` 可空,展示时为 0/空要有回退值。 + +--- + +## 七、历史脏数据说明 + +排查过程中发现,测试环境里同一笔手动互换的 `swap_flow_event` 可能存在多条记录,`TradingAmountAvg` 取值不一致(有的相对价如 1.0011,有的被 ÷100 如 0.010011),成交金额相应相差 100 倍。这是早期代码版本更迭残留的脏数据。 + +判断脏数据的依据:成交金额 `TradingAmount = TradingAmountAvg × Quantity × ContractSize`(后端 `SwapDealService:1103` 计算)。若同一交易的两条互换记录成交金额相差正好 100 倍,则其中一条的 `TradingAmountAvg` 被多 ÷100。 + +这类历史数据问题用 SQL 单独修复,**不应在代码里迁就脏数据而保留错误的逻辑**。排查与修复 SQL 见文末附录。 + +--- + +## 附录 A:排查工具箱 SQL + +> 以下 SQL 是 2026-06 价格显示 Bug 排查过程中验证有效的查询,数据库为 MySQL,库名 `glms_yltrs_ylcms`(ylcms 连接)。后续遇到类似"显示数量级不对"问题可直接复用。 + +### A.1 查互换交易期初持仓的 4 个价格字段(确认入库基准) + +```sql +SELECT + t.TradeNumber AS trade_no, + t.StructureType AS 结构类型, + sp.UnderlyingCode AS 标的代码, + sp.PosiGrossPrice AS 期初全价, + sp.PosiNetPrice AS 期初净价, + sp.PosiNetNoFeePrice AS 期初净价不含费, + sp.PosiNetFeePrice AS 期初净价含费 +FROM trade t +JOIN swap_position sp ON sp.SwapTradeId = t.id + AND sp.IsInitial = 1 + AND sp.UnderlyingCode IS NOT NULL -- 过滤利息腿/预付金腿 +WHERE t.TradeType = '收益互换' + AND t.StructureType IN ('普通债券类收益互换', '普通收益互换') + AND t.ValidState = 'valid' +ORDER BY t.id DESC +LIMIT 20; +``` + +### A.2 查互换/自动互换事件入库值(区分手动/自动、用成交金额反推正确基准) + +⚠️ `swap_flow_event` 表的 `PosiGrossPrice`/`PosiNetPrice` 是 `[NotMapped]`,**数据库查不到这两列**;只能查真正入库的 `TradingAmountAvg`、`TradingAmountNetAvg`、`TradingAmount`。 + +```sql +SELECT + t.TradeNumber AS trade_no, + fe.id AS 事件ID, + fe.EventType AS 事件类型, + CASE fe.EventType WHEN 3 THEN '手动互换' WHEN 4 THEN '自动互换' END AS 类型, + fe.EventReason AS 事件原因, + fe.EventDate AS 业务日期, + fe.UnderlyingCode AS 标的, + fe.PayDirection AS 收支方向, + fe.PositionType AS 多空方向, + fe.UnderlyingInstrumentType AS 标的类型_判断债券用, + fe.TradingAmountAvg AS 期末全价_入库, + fe.TradingAmountNetAvg AS 期末净价_入库, + fe.TradingAmount AS 成交金额, + fe.DividendIn AS 分红金额 +FROM trade t +JOIN swap_flow_event fe ON fe.SwapTradeId = t.id +WHERE t.id IN (/*填trade id*/) + AND t.ValidState = 'valid' + AND fe.EventType IN (3, 4) + AND fe.UnderlyingCode IS NOT NULL -- 只要浮动端事件,排除利息腿 +ORDER BY t.id, fe.EventDate DESC, fe.id DESC +LIMIT 30; +``` + +**判断要点**: +- `UnderlyingInstrumentType` 为空 → 该事件的展示会走"非债券分支(不×100)",债券标的显示会错(这就是自动互换的 Bug 根因)。 +- 同一交易同一腿出现 `TradingAmountAvg` 相差 100 倍的两条记录 → 其中一条是被多 ÷100 的脏数据。 + +### A.3 查分红型自动互换的数据源(eod_swap_position,确认 PosiDividendSum 触发条件) + +`PosiDividendSum != 0` 时触发 `DealDividends` 生成自动互换事件。注意测试数据可能为 0 查不到,需去掉该条件排查。 + +```sql +SELECT + esp.SwapTradeId AS trade_id, + esp.ValueDate AS 持仓日期, + esp.UnderlyingCode AS 标的代码, + esp.UnderlyingInstrumentType AS 标的类型, + esp.PosiDividendSum AS 浮动端未实现分红_触发条件, + esp.TdPosiDividend AS 当日浮动端分红, + esp.PosiNetPrice AS 持仓净价_自动互换TradingAmountAvg来源, + esp.PosiGrossPrice AS 持仓全价, + esp.PosiNetNoFeePrice AS 持仓净价不含费_期初净价来源 +FROM eod_swap_position esp +WHERE esp.SwapTradeId IN (/*填trade id*/) +ORDER BY esp.ValueDate DESC +LIMIT 20; +``` + +### A.4 查中债估值数据源(确认录入时默认读取的价格) + +簿记交易时标的默认价格来自 `china_bond_valuation`(中债估值)表。⚠️ **该表的字段命名与业务常用名差异很大**,特别注意:债券代码列是 `bond_id`(不是 UnderlyingCode),价格列是 `dirty_price_close`(日终估值全价)、`net_price`(估价净值)、`yield`(收益率%)、`vobp`(基点价值)。 + +```sql +SELECT + cbv.bond_id AS 债券代码, + cbv.valuation_date AS 估值日期, + cbv.dirty_price_close AS 日终估值全价, -- 录入默认读取的"全价"来源 + cbv.net_price AS 估价净值, + cbv.yield AS 估价收益率_pct, + cbv.vobp AS 估价基点价值, -- DV01 计算用此字段 + cbv.acc_rint_close AS 日终应计利息, + cbv.credibility AS 可信度 -- GetBondPrice 按可信度降序取最新 +FROM china_bond_valuation cbv +WHERE cbv.bond_id = '180205.IB' +ORDER BY cbv.credibility DESC, cbv.valuation_date DESC +LIMIT 10; +``` + +> 后端 `EodPriceQueryService.GetBondPrice` 取数逻辑:按 `bond_id` 匹配,`valuation_date <= 指定日`,按 `credibility` 降序、`valuation_date` 降序取第一条。返回的 `ClosePrice = dirty_price_close × bondPriceMultiple`(默认乘数通常为 1)。测试环境可能被维护成离谱值(如全价 1999)。 + +### A.5 历史脏数据修复(手动互换被多 ÷100 的记录) + +⚠️ **执行前务必先用 A.2 核对,并备份**。以下仅作为模板,需根据实际范围调整 WHERE: + +```sql +-- 备份 +CREATE TABLE swap_flow_event_bak_yyyymmdd AS +SELECT * FROM swap_flow_event WHERE /*同下方WHERE条件*/; + +-- 修复: 被多÷100的TradingAmountAvg ×100还原, 成交金额同步×100 +UPDATE swap_flow_event fe +JOIN trade t ON t.id = fe.SwapTradeId +SET fe.TradingAmountAvg = fe.TradingAmountAvg * 100, + fe.TradingAmount = fe.TradingAmount * 100 +WHERE t.StructureType = '普通债券类收益互换' + AND fe.EventType = 3 -- 手动互换 + AND fe.UnderlyingCode IS NOT NULL + AND fe.TradingAmountAvg > 0 + AND fe.TradingAmountAvg < 0.5; -- 相对价合理范围判断(债券相对价通常0.5~2) +``` + +--- + +## 附录 B:字段命名与数据流向速查 + +容易混淆的字段对应关系(排查时务必核对实际数据流向,不要被字段名误导): + +| 界面列(表头) | 取值字段 | 字段所在表 | 实际数据来源 | 备注 | +|---|---|---|---|---| +| 期初标的交割全价% | `PosiGrossPrice` | swap_position(入库)/ swap_flow_event([NotMapped]运行时拷贝) | `swap_position.PosiGrossPrice` | 相对价,展示×100 | +| 期初标的交割净价% | `TradingAmountNetAvg` | swap_flow_event(入库) | `swap_position.PosiNetNoFeePrice`(收益结算初始化时) | **字段名像期末,实装期初值**;可空需兜底 | +| 期末标的交割全价% | `TradingAmountAvg` | swap_flow_event(入库) | 手动互换=用户输入;自动互换=`eod.PosiNetPrice` | 相对价,展示×100 | +| 标的类型判断 | `UnderlyingInstrumentType` | swap_flow_event(入库) | 生成事件时赋值 | **展示层判断债券(×100)的关键,必须赋值** | +| 成交金额 | `TradingAmount` | swap_flow_event(入库) | `TradingAmountAvg × Quantity × ContractSize` | 反推正确基准的依据 | + +**关键提醒**:`swap_flow_event.PosiGrossPrice` 和 `PosiNetPrice` 是 `[NotMapped]`,数据库**没有这两列**,查库会报 `Unknown column`。它们只在运行时从 `swap_position` 拷贝过来用于展示。 From 2140a97f533353c075a84393871e406329275aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 25 Jun 2026 19:44:00 +0800 Subject: [PATCH 12/12] =?UTF-8?q?BugFix=20=E5=88=B0=E6=9C=9F=E5=88=86?= =?UTF-8?q?=E7=BA=A2=E7=9A=84=EF=BC=8C=E5=B7=B2=E5=AE=9E=E7=8E=B0=E5=92=8C?= =?UTF-8?q?=E4=BA=92=E5=8A=A8=E7=9A=84=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index e5792425..a8d8b8bb 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1505,6 +1505,12 @@ namespace YLErp.Modules.SwapModule { curretEod.PosiDividendSum = 0; } + if (curretEod.PosiDividendSum == 0)//这里处理一下已实现盈亏和互换价值,其他在处理分红的时候会处理 + { + curretEod.RealizedPnl += curretEod.TdCloseDividend; + curretEod.SwapPositionValue -= curretEod.TdCloseDividend; + + } curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; if (curretEod.PosiStatus == 1) {