From 3520f242539aa6bd2f23214a79a9bab81710da35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=B9=E6=B5=B7?= Date: Tue, 23 Jun 2026 10:48:51 +0800 Subject: [PATCH] =?UTF-8?q?#EQD-5994=20=E5=9B=BD=E8=81=94=E6=B0=91?= =?UTF-8?q?=E7=94=9F-=E3=80=90=E9=A3=8E=E9=99=A9=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=80=91TRS=E5=90=88=E7=BA=A6DV=E8=AE=A1=E7=AE=97=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YLErp.Core/Commons/RiskCfgRedisKey.cs | 17 +- Framework/YLErp.Core/DBModels/EodSwap.cs | 5 + .../YLErp.Core/DBModels/EodSwapPosition.cs | 5 + Framework/YLErp.Core/DBModels/QuotaMonitor.cs | 40 +++ .../YLErp.Core/DBModels/UnderlyingBond.cs | 6 +- .../YLErp.Core/Enums/QuotaMonitorEnums.cs | 2 +- YLErpDAL/Helpers/Dv01Helper.cs | 51 ++++ .../Modules/RiskModule/QuotaMonitorService.cs | 279 ++++++++++++++++-- .../SwapModule/SwapEodPositionService.cs | 5 + .../wwwroot/Scripts/app/risk/quotaMonitor.js | 74 ++++- .../wwwroot/Scripts/app/risk/quotaSetting.js | 1 + .../Scripts/app/swaptrade/EodPositionRisks.js | 7 + 12 files changed, 470 insertions(+), 22 deletions(-) create mode 100644 YLErpDAL/Helpers/Dv01Helper.cs diff --git a/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs b/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs index aff0be48..36090934 100644 --- a/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs +++ b/Framework/YLErp.Core/Commons/RiskCfgRedisKey.cs @@ -32,12 +32,19 @@ namespace YLErp.Commons /// 交易维度-价格偏离度 /// private static string TRADE_PRICE_RATE = "risk:cfg:trade:price_rate"; - - + /// + /// 交易维度-DV + /// + private static string TRADE_DV = "risk:cfg:trade:dv"; /// /// 客户维度-名义本金 /// private static string CLIENT_PRINCIPAL = "risk:cfg:client:principal"; + /// + /// 全局维度-DV + /// + + private static string GLOBAL_DV = "risk:cfg:global:dv"; @@ -45,6 +52,8 @@ namespace YLErp.Commons { switch (quoteType) { + case QuotaTypeEnum.GLOBAL_ALL: + return GLOBAL_DV; case QuotaTypeEnum.GLOBAL_SWAP: return SWAP_PRINCIPAL; case QuotaTypeEnum.UNDERLYING: @@ -54,6 +63,10 @@ namespace YLErp.Commons } return ASSET_ROLL; case QuotaTypeEnum.TRADE: + if ("DV".Equals(quoteIndex)) + { + return TRADE_DV; + } return TRADE_PRICE_RATE; case QuotaTypeEnum.CLIENT: return CLIENT_PRINCIPAL; diff --git a/Framework/YLErp.Core/DBModels/EodSwap.cs b/Framework/YLErp.Core/DBModels/EodSwap.cs index 61f2289a..4d6294a0 100644 --- a/Framework/YLErp.Core/DBModels/EodSwap.cs +++ b/Framework/YLErp.Core/DBModels/EodSwap.cs @@ -148,6 +148,11 @@ namespace YLErp.DBModels [DataChange] public decimal TdCloseQty { get; set; } /// + /// 基点价值DV01 + /// + [DataChange] + public decimal? dv01 { get; set; } + /// /// 平仓起始日期 /// [NotMapped] diff --git a/Framework/YLErp.Core/DBModels/EodSwapPosition.cs b/Framework/YLErp.Core/DBModels/EodSwapPosition.cs index 1a2944e9..f4c8f745 100644 --- a/Framework/YLErp.Core/DBModels/EodSwapPosition.cs +++ b/Framework/YLErp.Core/DBModels/EodSwapPosition.cs @@ -433,6 +433,11 @@ namespace YLErp.DBModels /// public int? interest_rule { get; set; } /// + /// 基点价值DV01 + /// + [DataChange] + public decimal? dv01 { get; set; } + /// /// 持仓编码 /// [NotMapped] diff --git a/Framework/YLErp.Core/DBModels/QuotaMonitor.cs b/Framework/YLErp.Core/DBModels/QuotaMonitor.cs index 43fdddce..3de7acf1 100644 --- a/Framework/YLErp.Core/DBModels/QuotaMonitor.cs +++ b/Framework/YLErp.Core/DBModels/QuotaMonitor.cs @@ -891,6 +891,26 @@ namespace YLErp.DBModels /// CCR /// public double? Quota_CCR { get; set; } = double.NaN; + /// + /// DV + /// + public double? DV { get; set; } + /// + /// DV限额 + /// + public double? Quota_DV_Upper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_Lower { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wUpper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wLower { get; set; } = double.NaN; } /// @@ -1257,5 +1277,25 @@ namespace YLErp.DBModels public double? Quota_ThisYearTotalPnl_Lower { get; set; } = double.NaN; public double? Quota_ThisYearTotalPnl_wUpper { get; set; } = double.NaN; public double? Quota_ThisYearTotalPnl_wLower { get; set; } = double.NaN; + /// + /// DV + /// + public double? DV { get; set; } + /// + /// DV限额 + /// + public double? Quota_DV_Upper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_Lower { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wUpper { get; set; } = double.NaN; + /// + /// DV限额 + /// + public double? Quota_DV_wLower { get; set; } = double.NaN; } } diff --git a/Framework/YLErp.Core/DBModels/UnderlyingBond.cs b/Framework/YLErp.Core/DBModels/UnderlyingBond.cs index 5797f652..535e8746 100644 --- a/Framework/YLErp.Core/DBModels/UnderlyingBond.cs +++ b/Framework/YLErp.Core/DBModels/UnderlyingBond.cs @@ -80,5 +80,9 @@ /// 债券期限(利率债) /// public string BondTerm { get; set; } - } + /// + /// 债券单位面值,默认100 + /// + public decimal? Price { get; set; } + } } diff --git a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs index 18486318..18980071 100644 --- a/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs +++ b/Framework/YLErp.Core/Enums/QuotaMonitorEnums.cs @@ -111,7 +111,7 @@ namespace YLErp.Enums { return new List() { - //new SelectItem() {Text="全局",Value="16" }, + new SelectItem() {Text="全局",Value="16" }, //new SelectItem() {Text="场外业务",Value="0" }, //new SelectItem() {Text="场外期权",Value="1" }, new SelectItem() {Text="互换",Value="2" }, diff --git a/YLErpDAL/Helpers/Dv01Helper.cs b/YLErpDAL/Helpers/Dv01Helper.cs new file mode 100644 index 00000000..70c0b370 --- /dev/null +++ b/YLErpDAL/Helpers/Dv01Helper.cs @@ -0,0 +1,51 @@ +using System; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Modules; + +namespace YLErp.Helpers +{ + /// + /// DV01计算工具类 + /// DV01 = 方向 * (券面总额(万元) / 单位面值) * 中债基点价值 + /// + public static class Dv01Helper + { + /// + /// 计算DV01 + /// + /// 标的代码 + /// 持仓数量 + /// 收支方向: 1=收取, 2=支付 + /// 多空方向: 1=多头, 2=空头 + /// 中债基点价值 + /// DV01值 + public static decimal CalcDv01(string underlyingCode, decimal posiQuantity, int posiDirection, int positionType, decimal vobp) + { + if (vobp == 0 || posiQuantity == 0) return 0; + + var um = DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); + if (um == null || !um.IsBond()) return 0; + + // 方向: 支付多头=-1, 收取多头=1, 支付空头=1, 收取空头=-1 + // = (PosiDirection==收取?1:-1) * (PositionType==多头?1:-1) + int direction = (posiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1) + * (positionType == (int)PositionTypeFlag.Long ? 1 : -1); + + // 券面总额(万元) = 交易数量 / 10000 + double faceValueWan = Convert.ToDouble(posiQuantity) / 10000d; + + // 单位面值, 默认100 + decimal unitFaceValue = 100m; + if (!string.IsNullOrEmpty(um.ExJson)) + { + var bond = JsonHelper.Deserialize(um.ExJson); + if (bond?.Price != null && bond.Price != 0) + unitFaceValue = bond.Price.Value; + } + + decimal result = Convert.ToDecimal(direction * (faceValueWan / Convert.ToDouble(unitFaceValue)) * Convert.ToDouble(vobp)); + return Math.Round(result, 4); + } + } +} diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs index 7c43ed2c..3247dd67 100644 --- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs +++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs @@ -794,6 +794,19 @@ namespace YLErp.Modules.RiskModule join p in DbContext.swap_position on t.id equals p.SwapTradeId where !p.IsInitial && p.PosiDirection > 0 && !p.Invalid select p; + // 计算互换DV汇总(按标的缓存bondPrice) + var swapPositions = posiQuery.ToList(); + var swapDV = 0d; + var swapBPCache = swapPositions.Select(p => p.UnderlyingCode).Distinct() + .ToDictionary(code => code, code => EodPriceQueryService.GetBondPrice(valuedateBLL.ValueDate, code)); + foreach (var pos in swapPositions) + { + if (!swapBPCache.TryGetValue(pos.UnderlyingCode, out var bondPrice) || bondPrice == null) continue; + var vobp = bondPrice.Vobp ?? 0; + if (vobp == 0) continue; + swapDV += Convert.ToDouble(Dv01Helper.CalcDv01(pos.UnderlyingCode, pos.PosiQuantity, pos.PosiDirection, pos.PositionType, vobp)); + } + var setValue = new Action>((obj, settings) => { obj.StockEqvNotional = Convert.ToDouble(posiQuery.Sum(s => s.PosiNotionalValue)); @@ -906,6 +919,10 @@ namespace YLErp.Modules.RiskModule var allSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.GLOBAL_ALL); allSetting = MargeQuotaSetting(allSetting, 0, 0); + var globalDvSetting = allSetting.Where(O => O.IsValid && O.Status == QuotaSettingApprovalStatus.Valid && O.QuotaRange == 0 && O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); + var tradeSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.TRADE); + tradeSetting = MargeQuotaSetting(tradeSetting, 0, 0); + var tradeDvSetting = tradeSetting.Where(O => O.IsValid && O.Status == QuotaSettingApprovalStatus.Valid && O.QuotaRange == 0 && O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); var swapSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.GLOBAL_SWAP); swapSetting = MargeQuotaSetting(swapSetting, 0, 0); var swap = new QuotaMonitor_Global() @@ -914,6 +931,11 @@ namespace YLErp.Modules.RiskModule BusinessType = "互换" }; setValue(swap, swapSetting); + swap.DV = swapDV; + swap.Quota_DV_Upper = tradeDvSetting?.QuotaUpperLimit ?? double.NaN; + swap.Quota_DV_Lower = tradeDvSetting?.QuotaLowerLimit ?? double.NaN; + swap.Quota_DV_wUpper = tradeDvSetting?.WarningUpperLimit ?? double.NaN; + swap.Quota_DV_wLower = tradeDvSetting?.WarningLowerLimit ?? double.NaN; var positionList = new List>(); var underly = GetTradePositionPnl(); @@ -925,6 +947,21 @@ namespace YLErp.Modules.RiskModule positionList.AddRange(swapPositionList); } var checkPosiList = GetCheckPosiList(); + // 计算checkPosiList中所有持仓的DV合计,用于算未簿记DV(按标的缓存bondPrice) + var totalCheckPosiDV = 0d; + if (checkPosiList.Any()) + { + var ckBPCache = checkPosiList.Select(p => p.UnderlyingCode).Distinct() + .ToDictionary(code => code, code => EodPriceQueryService.GetBondPrice(valuedateBLL.ValueDate, code)); + foreach (var pos in checkPosiList) + { + if (!ckBPCache.TryGetValue(pos.UnderlyingCode, out var bp) || bp == null) continue; + var vobp = bp.Vobp ?? 0; + if (vobp == 0) continue; + int positionType = pos.Side == 0 ? (int)PositionTypeFlag.Long : (int)PositionTypeFlag.Short; + totalCheckPosiDV += Convert.ToDouble(Dv01Helper.CalcDv01(pos.UnderlyingCode, pos.Qty, pos.Direction, positionType, vobp)); + } + } var posiStockEqvNotional = checkPosiList.Sum(s => s.Pv); var delta = checkPosiList.Sum(s => s.Delta); var unTrade = new QuotaMonitor_Global() @@ -932,14 +969,20 @@ namespace YLErp.Modules.RiskModule ParentKey = "场外", BusinessType = "未簿记合约", StockEqvNotional = Convert.ToDouble(posiStockEqvNotional) - swap.StockEqvNotional.Normalize(), - DeltaExposure=(double)delta- (swap.DeltaExposure??0) + DeltaExposure=(double)delta- (swap.DeltaExposure??0), + DV = totalCheckPosiDV - (swap.DV ?? 0), }; var all = new QuotaMonitor_Global() { BusinessType = "全局", StockEqvNotional = Convert.ToDouble(posiStockEqvNotional), PositionPnl = underly.PositionPnl + swap.PositionPnl, - DeltaExposure=(double)delta+ underly.DeltaExposure + DeltaExposure=(double)delta+ underly.DeltaExposure, + DV = (swap.DV ?? 0) + (underly.DV ?? 0) + (unTrade.DV ?? 0), + Quota_DV_Upper = globalDvSetting?.QuotaUpperLimit ?? double.NaN, + Quota_DV_Lower = globalDvSetting?.QuotaLowerLimit ?? double.NaN, + Quota_DV_wUpper = globalDvSetting?.WarningUpperLimit ?? double.NaN, + Quota_DV_wLower = globalDvSetting?.WarningLowerLimit ?? double.NaN, }; var list = new List { @@ -962,10 +1005,14 @@ namespace YLErp.Modules.RiskModule /// public List QueryEodGlobalFromCalc(QuotaMonitorReq req) { - var posiQuery = DbContext.eod_swap_position.Where(O =>O.ValueDate==req.ValueDate&&O.PosiQuantity>0); + var maxDate = DbContext.eod_swap_position.Where(O => O.PosiQuantity > 0).Max(O => (DateTime?)O.ValueDate) ?? req.ValueDate; + var posiQuery = DbContext.eod_swap_position.Where(O =>O.ValueDate==maxDate&&O.PosiQuantity>0); req.PvPercent = 1 - req.PvPercent; - + + // 互换DV汇总:直接取eod_swap_position中已计算好的dv01 + var eodSwapDV = Convert.ToDouble(posiQuery.Sum(p => p.dv01)); + var setValue = new Action>((obj, settings) => { obj.StockEqvNotional = Convert.ToDouble(posiQuery.Sum(s => s.PosiNotionalValue)); @@ -987,6 +1034,10 @@ namespace YLErp.Modules.RiskModule var allSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.GLOBAL_ALL); allSetting = MargeQuotaSetting(allSetting, 0, 0); + var globalDvSetting = allSetting.Where(O => O.IsValid && O.Status == QuotaSettingApprovalStatus.Valid && O.QuotaRange == 0 && O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); + var tradeSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.TRADE); + tradeSetting = MargeQuotaSetting(tradeSetting, 0, 0); + var tradeDvSetting = tradeSetting.Where(O => O.IsValid && O.Status == QuotaSettingApprovalStatus.Valid && O.QuotaRange == 0 && O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); var swapSetting = QueryCurrentQuotaSetting(QuotaTypeEnum.GLOBAL_SWAP); swapSetting = MargeQuotaSetting(swapSetting, 0, 0); var swap = new QuotaMonitor_Global() @@ -995,6 +1046,11 @@ namespace YLErp.Modules.RiskModule BusinessType = "互换", }; setValue(swap, swapSetting); + swap.DV = eodSwapDV; + swap.Quota_DV_Upper = tradeDvSetting?.QuotaUpperLimit ?? double.NaN; + swap.Quota_DV_Lower = tradeDvSetting?.QuotaLowerLimit ?? double.NaN; + swap.Quota_DV_wUpper = tradeDvSetting?.WarningUpperLimit ?? double.NaN; + swap.Quota_DV_wLower = tradeDvSetting?.WarningLowerLimit ?? double.NaN; var underly = GetEodTradePositionPnl(req.ValueDate); var unTrade = new QuotaMonitor_Global() { @@ -1008,7 +1064,12 @@ namespace YLErp.Modules.RiskModule BusinessType = "全局", StockEqvNotional = swap.StockEqvNotional, PositionPnl = underly.PositionPnl + swap.PositionPnl, - DeltaExposure = swap.DeltaExposure+ underly.DeltaExposure + DeltaExposure = swap.DeltaExposure+ underly.DeltaExposure, + DV = (swap.DV ?? 0) + (underly.DV ?? 0) + (unTrade.DV ?? 0), + Quota_DV_Upper = globalDvSetting?.QuotaUpperLimit ?? double.NaN, + Quota_DV_Lower = globalDvSetting?.QuotaLowerLimit ?? double.NaN, + Quota_DV_wUpper = globalDvSetting?.WarningUpperLimit ?? double.NaN, + Quota_DV_wLower = globalDvSetting?.WarningLowerLimit ?? double.NaN, }; var list = new List { @@ -1322,11 +1383,23 @@ namespace YLErp.Modules.RiskModule obj.Quota_SwapPercent_Lower = swapPercentSetting?.QuotaLowerLimit ?? double.NaN; obj.Quota_SwapPercent_wUpper = swapPercentSetting?.WarningUpperLimit ?? double.NaN; obj.Quota_SwapPercent_wLower = swapPercentSetting?.WarningLowerLimit ?? double.NaN; + var dvSetting = temp.Where(O => O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); + obj.Quota_DV_Upper = dvSetting?.QuotaUpperLimit ?? double.NaN; + obj.Quota_DV_Lower = dvSetting?.QuotaLowerLimit ?? double.NaN; + obj.Quota_DV_wUpper = dvSetting?.WarningUpperLimit ?? double.NaN; + obj.Quota_DV_wLower = dvSetting?.WarningLowerLimit ?? double.NaN; }); var setting = QueryCurrentQuotaSetting(QuotaTypeEnum.TRADE); setting = MargeQuotaSetting(setting, 0, 0); var ccrDict = new Dictionary(); + + // 批量获取swap_position方向/多空/数量,用于DV计算 + var tradeIds = list.Select(O => O.trade.id).Distinct().ToList(); + var swapPositionDict = DbContext.swap_position + .Where(p => tradeIds.Contains(p.SwapTradeId) && !p.IsInitial && p.PosiDirection > 0 && !p.Invalid) + .ToDictionary(p => p.SwapTradeId); + foreach (var item in list) { setValue(item, setting); @@ -1336,6 +1409,13 @@ namespace YLErp.Modules.RiskModule var vobp = bondPrice == null ? 0 : Convert.ToDouble(bondPrice.Vobp); var pricePercent = basePrice == 0 ? 0 : Math.Abs((sportPrice / basePrice) - 1); item.Quota_SwapPercent = pricePercent; + + // 计算DV + if (swapPositionDict.TryGetValue(item.trade.id, out var swapPos) && bondPrice != null && vobp != 0) + { + item.DV = Convert.ToDouble(Dv01Helper.CalcDv01(item.trade.UnderlyingCode, swapPos.PosiQuantity, swapPos.PosiDirection, swapPos.PositionType, Convert.ToDecimal(vobp))); + } + var obj = new QuotaMonitor_Trade(); ObjectHelper.MapValues(obj, item); result.Add(obj); @@ -1350,6 +1430,7 @@ namespace YLErp.Modules.RiskModule VegaCash = result.Sum(O => O.VegaCash), Quota_CCR = result.Sum(O => O.Quota_CCR), PnL = result.Sum(O => O.PnL), + DV = result.Sum(O => O.DV), }; result.Add(total); @@ -1429,11 +1510,26 @@ namespace YLErp.Modules.RiskModule obj.Quota_SwapPercent_Lower = swapPercentSetting?.QuotaLowerLimit ?? double.NaN; obj.Quota_SwapPercent_wUpper = swapPercentSetting?.WarningUpperLimit ?? double.NaN; obj.Quota_SwapPercent_wLower = swapPercentSetting?.WarningLowerLimit ?? double.NaN; + var dvSetting = temp.Where(O => O.QuotaIndex == "DV").FirstOrDefault()?.Clone(); + obj.Quota_DV_Upper = dvSetting?.QuotaUpperLimit ?? double.NaN; + obj.Quota_DV_Lower = dvSetting?.QuotaLowerLimit ?? double.NaN; + obj.Quota_DV_wUpper = dvSetting?.WarningUpperLimit ?? double.NaN; + obj.Quota_DV_wLower = dvSetting?.WarningLowerLimit ?? double.NaN; }); var setting = QueryCurrentQuotaSetting(QuotaTypeEnum.TRADE); setting = MargeQuotaSetting(setting, 0, 0); var ccrDict = new Dictionary(); + + // 批量获取eod_swap_position方向/多空/数量,用于DV计算和dv01回写 + var tradeIds = list.Select(O => O.trade.id).Distinct().ToList(); + var eodSwapPosDict = DbContext.eod_swap_position + .Where(p => p.ValueDate == req.ValueDate && tradeIds.Contains(p.SwapTradeId) && p.PosiQuantity > 0) + .ToDictionary(p => p.SwapTradeId); + var eodSwapDict = DbContext.eod_swap + .Where(p => p.ValueDate == req.ValueDate && tradeIds.Contains(p.SwapTradeId)) + .ToDictionary(p => p.SwapTradeId); + foreach (var item in list) { setValue(item, setting); @@ -1443,10 +1539,19 @@ namespace YLErp.Modules.RiskModule var vobp = bondPrice == null ? 0 : Convert.ToDouble(bondPrice.Vobp); var pricePercent = basePrice == 0 ? 0 : Math.Abs((sportPrice / basePrice) - 1); item.Quota_SwapPercent = pricePercent; + + // 计算DV,并回写dv01 + if (eodSwapPosDict.TryGetValue(item.trade.id, out var eodSwapPos)) + { + item.DV = Convert.ToDouble(eodSwapPos.dv01); + } + var obj = new QuotaMonitor_Trade(); ObjectHelper.MapValues(obj, item); result.Add(obj); } + DbContext.SaveChanges(); + var total = new QuotaMonitor_Trade() { TradeNumber = "合计", @@ -1457,6 +1562,7 @@ namespace YLErp.Modules.RiskModule VegaCash = result.Sum(O => O.VegaCash), Quota_CCR = result.Sum(O => O.Quota_CCR), PnL = result.Sum(O => O.PnL), + DV = result.Sum(O => O.DV), }; result.Add(total); @@ -3521,6 +3627,20 @@ namespace YLErp.Modules.RiskModule singleUnderlyingRate = valuedateBLL.SystemDate.SingleUnderlyingRate; _quotaSettings = new List(); #region 整体业务 + //全局-DV + _quotaSettings.Add(new QuotaSetting() + { + QuotaType = QuotaTypeEnum.GLOBAL_ALL, + QuotaRange = 0, + QuotaIndex = "DV", + QuotaLowerLimit = null, + QuotaUpperLimit = null, + WarningLowerLimit = null, + WarningUpperLimit = null, + Percent = false, + IsValid = true, + Status = QuotaSettingApprovalStatus.Valid, + }); #endregion #region 互换 @@ -3616,6 +3736,20 @@ namespace YLErp.Modules.RiskModule IsValid = true, Status = QuotaSettingApprovalStatus.Valid, }); + //交易-DV + _quotaSettings.Add(new QuotaSetting() + { + QuotaType = QuotaTypeEnum.TRADE, + QuotaRange = 0, + QuotaIndex = "DV", + QuotaLowerLimit = null, + QuotaUpperLimit = null, + WarningLowerLimit = null, + WarningUpperLimit = null, + Percent = false, + IsValid = true, + Status = QuotaSettingApprovalStatus.Valid, + }); #endregion #region 客户 @@ -4394,6 +4528,9 @@ namespace YLErp.Modules.RiskModule var ums = GetUnderlyings(umCodes); var checkPoisiList = BuildCheckQuotaMoitorModels(clientPositions, ums, dealDate); + // 仅在限额设置中有DV指标时才计算DV + var (totalDv, currentDv) = precheckQuotaSettingList.Any(s => s.QuotaIndex == "DV") + ? CalcRiskCheckDv(checkPoisiList, dealDate) : (0d, 0d); var underlyingPositions = checkPoisiList.Where(x => x.UnderlyingCode == clientRiskCheckReq.securityId).ToList(); var sameQty = underlyingPositions.Where(x=>x.ClientId== clientRiskCheckReq.clientId && x.Side== clientRiskCheckReq.side).Sum(s => s.Qty); var qty = underlyingPositions.Where(x => x.ClientId == clientRiskCheckReq.clientId && x.Side != clientRiskCheckReq.side).Sum(s => s.Qty); @@ -4421,7 +4558,7 @@ namespace YLErp.Modules.RiskModule { continue; } - clientRiskCheckItem = CheckQuota(checkPoisiList, settingItem.QuotaType, clientRiskCheckItem, settingItem.QuotaRange, allList); + clientRiskCheckItem = CheckQuota(checkPoisiList, settingItem.QuotaType, clientRiskCheckItem, settingItem.QuotaRange, allList, totalDv, currentDv); if (clientRiskCheckItem != null) { clientRiskCheckItem.quotaType = $"{EnumHelper.GetDescriptionByName(settingItem.QuotaType)}({settingItem.QuotaIndex})"; @@ -4453,6 +4590,50 @@ namespace YLErp.Modules.RiskModule .Where(x => umCodes.Contains(x.UnderlyingCode)) .ToList(); } + /// + /// 计算风控检查用的DV(全局DV + 当前单笔DV) + /// + private (double totalDv, double currentDv) CalcRiskCheckDv(List checkPoisiList, DateTime dealDate) + { + var totalDv = 0d; + var currentDv = 0d; + // 按标的缓存bondPrice + var bpCache = checkPoisiList.Select(p => p.UnderlyingCode).Distinct() + .ToDictionary(code => code, code => EodPriceQueryService.GetBondPrice(dealDate, code)); + // 全部持仓DV + totalDv = checkPoisiList.Sum(pos => + { + if (!bpCache.TryGetValue(pos.UnderlyingCode, out var bp) || bp == null) return 0d; + var vobp = bp.Vobp ?? 0; + if (vobp == 0) return 0d; + int posType = pos.Side == 0 ? (int)PositionTypeFlag.Long : (int)PositionTypeFlag.Short; + return Convert.ToDouble(Dv01Helper.CalcDv01(pos.UnderlyingCode, pos.Qty, pos.Direction, posType, vobp)); + }); + // 当前单笔DV + var cur = checkPoisiList.FirstOrDefault(x => x.Current); + if (cur != null && bpCache.TryGetValue(cur.UnderlyingCode, out var curBp) && curBp != null) + { + var vobp = curBp.Vobp ?? 0; + if (vobp != 0) + { + int pt = cur.Side == 0 ? (int)PositionTypeFlag.Long : (int)PositionTypeFlag.Short; + currentDv = Convert.ToDouble(Dv01Helper.CalcDv01(cur.UnderlyingCode, cur.Qty, cur.Direction, pt, vobp)); + } + } + // 标的交易DV + List tradeTypes = new List { "利率债", "信用债", "其它债券" }; + var tposis = DbContext.TradePosition.Where(x => tradeTypes.Contains(x.TradeType)).AsNoTracking().ToList(); + foreach (var code in tposis.Select(x => x.UnderlyingCode).Distinct().Where(c => !bpCache.ContainsKey(c))) + bpCache[code] = EodPriceQueryService.GetBondPrice(dealDate, code); + foreach (var item in tposis) + { + if (!bpCache.TryGetValue(item.UnderlyingCode, out var tpBp) || tpBp == null) continue; + var vobp = tpBp.Vobp ?? 0; + if (vobp == 0) continue; + totalDv += Convert.ToDouble(Dv01Helper.CalcDv01(item.UnderlyingCode, Convert.ToDecimal(Math.Abs(item.Position)), (int)SwapDirectionEnum.收取, (int)item.PositionType, vobp)); + } + return (totalDv, currentDv); + } private (List, List) GetClientPositionsAndOrders(BondOmsDBContext bondDb, long orderId) { var clientPositions = bondDb.client_position.AsNoTracking().ToList(); @@ -4898,8 +5079,10 @@ namespace YLErp.Modules.RiskModule checkQuotaMoitorModel.Current = checkQuotaMoitorModel.id == 0; checkPoisiList.Add(checkQuotaMoitorModel); } - quotaWarningStatus = CheckQuota(checkPoisiList, posiList, list, allList, true, out quotaWarningMsg); - quotaStatus = CheckQuota(checkPoisiList, posiList, list, allList, false, out quotaMsg); + // 预计算DV,避免warning/error两次检查各算一遍 + var (quotaTotalDv, quotaCurrentDv) = list.Any(s => s.QuotaIndex == "DV") ? CalcRiskCheckDv(checkPoisiList, tradeObj.TradeDate.Value) : (0d, 0d); + quotaWarningStatus = CheckQuota(checkPoisiList, posiList, list, allList, true, out quotaWarningMsg, quotaTotalDv, quotaCurrentDv); + quotaStatus = CheckQuota(checkPoisiList, posiList, list, allList, false, out quotaMsg, quotaTotalDv, quotaCurrentDv); } } } @@ -5298,7 +5481,7 @@ namespace YLErp.Modules.RiskModule /// 试算类型是预警还是警告 /// 试算结果描述 /// - private bool CheckQuota(List positionList, List posiList, List setting, List settingAll, bool warning, out string msg) + private bool CheckQuota(List positionList, List posiList, List setting, List settingAll, bool warning, out string msg, double totalDv = 0, double currentDv = 0) { msg = ""; var msgList = new List(); @@ -5308,11 +5491,14 @@ namespace YLErp.Modules.RiskModule { switch (item.Key.QuotaType) { + case QuotaTypeEnum.GLOBAL_ALL: + msgList.AddRange(checkGlobalDv(positionList, item.Value, warning, totalDv)); + break; case QuotaTypeEnum.GLOBAL_SWAP: msgList.AddRange(checkGlobal(positionList, posiList, "场外业务-互换", item.Value, warning)); break; case QuotaTypeEnum.TRADE: - msgList.AddRange(checkTrade(positionList, posiList, "单笔交易", item.Value, warning)); + msgList.AddRange(checkTrade(positionList, posiList, "单笔交易", item.Value, warning, currentDv)); break; case QuotaTypeEnum.UNDERLYING: msgList.AddRange(checkUnderlying(positionList, posiList, "标的资产", item.Value, settingAll, warning)); @@ -5366,17 +5552,25 @@ namespace YLErp.Modules.RiskModule /// /// 单笔名义本金 /// - private ClientRiskCheckItem CheckQuota(List positionList, QuotaTypeEnum quotaType, ClientRiskCheckItem checkItem, int quotaRange, List settingAll) + private ClientRiskCheckItem CheckQuota(List positionList, QuotaTypeEnum quotaType, ClientRiskCheckItem checkItem, int quotaRange, List settingAll, double totalDv = 0, double currentDv = 0) { var tag_prefix = ""; var positionListAll = positionList; switch (quotaType) { + case QuotaTypeEnum.GLOBAL_ALL: + if (checkItem.quotaType == "DV") + { + checkItem.currentValue = Math.Round(totalDv, 2); + if (!ValidateQuoteResult(checkItem)) return checkItem; + return null; + } + break; case QuotaTypeEnum.GLOBAL_SWAP: checkItem = checkGlobal(positionList, "场外业务-互换", checkItem, quotaType); break; case QuotaTypeEnum.TRADE: - checkItem = checkTrade(positionList, "单笔交易", checkItem); + checkItem = checkTrade(positionList, "单笔交易", checkItem, currentDv); break; case QuotaTypeEnum.UNDERLYING: checkItem = checkUnderlying(positionList, "标的资产", checkItem, quotaRange, settingAll); @@ -5411,7 +5605,8 @@ namespace YLErp.Modules.RiskModule BusinessType = "标的交易", PositionPnl=0, DeltaExposure=0, - StockEqvNotional=0 + StockEqvNotional=0, + DV=0 }; List tradetypes = new List { "利率债", "信用债", "其它债券" }; var tposis = DbContext.TradePosition.Where(x => tradetypes.Contains(x.TradeType)).AsNoTracking().ToList(); @@ -5433,6 +5628,12 @@ namespace YLErp.Modules.RiskModule { var bondPrice = EodPriceQueryService.GetBondPrice(valuedateBLL.ValueDate, item.UnderlyingCode); lastPrice = bondPrice != null ? bondPrice.ClosePrice : (um.Price ?? 0) * Convert.ToDouble(ConsGlobal.bondPriceMultiple); + // 计算标的交易DV + var vobp = bondPrice != null ? bondPrice.Vobp ?? 0 : 0; + if (vobp != 0) + { + quotaMonitor_Global.DV += Convert.ToDouble(Dv01Helper.CalcDv01(item.UnderlyingCode,Math.Abs(Convert.ToDecimal(item.Position)), (int)SwapDirectionEnum.收取, (int)item.PositionType, vobp)); + } } } quotaMonitor_Global.PositionPnl += lastPrice * item.Position - item.PositionCost; @@ -5455,7 +5656,8 @@ namespace YLErp.Modules.RiskModule BusinessType = "标的交易", PositionPnl = 0, DeltaExposure = 0, - StockEqvNotional = 0 + StockEqvNotional = 0, + DV = 0 }; List tradetypes = new List { "利率债", "信用债", "其它债券" }; var tposis = DbContext.eod_trade_position.Where(x => tradetypes.Contains(x.TradeType)&&x.ValueDate==valueDate).AsNoTracking().ToList(); @@ -5477,6 +5679,13 @@ namespace YLErp.Modules.RiskModule { var bondPrice = EodPriceQueryService.GetBondPrice(valuedateBLL.ValueDate, item.UnderlyingCode); lastPrice = bondPrice != null ? bondPrice.ClosePrice : (um.Price ?? 0) * Convert.ToDouble(ConsGlobal.bondPriceMultiple); + // 计算标的交易DV: Amount有符号(正=多,负=空) + var vobp = bondPrice != null ? bondPrice.Vobp ?? 0 : 0; + if (vobp != 0) + { + int positionType = item.Amount > 0 ? (int)PositionTypeFlag.Long : (int)PositionTypeFlag.Short; + quotaMonitor_Global.DV += Convert.ToDouble(Dv01Helper.CalcDv01(item.UnderlyingCode, Convert.ToDecimal(Math.Abs(item.Amount)), (int)SwapDirectionEnum.收取, positionType, vobp)); + } } } quotaMonitor_Global.PositionPnl += item.PositionPnL; @@ -5540,6 +5749,27 @@ namespace YLErp.Modules.RiskModule } return messageList; } + /// + /// 检验全局DV限额 + /// + private List checkGlobalDv(List positionList, QuotaSetting[] settings, bool warning, double totalDv = -1) + { + var messageList = new List(); + var dvSettings = settings.Where(s => s.QuotaIndex == "DV").ToArray(); + if (!dvSettings.Any()) return messageList; + // totalDv未预计算时(其他调用方),才在这里算 + if (totalDv < 0) + totalDv = CalcRiskCheckDv(positionList, valuedateBLL.ValueDate).totalDv; + foreach (var settingItem in dvSettings) + { + double? upperLimit = warning ? settingItem.WarningUpperLimit : settingItem.QuotaUpperLimit; + double? lowerLimit = warning ? settingItem.WarningLowerLimit : settingItem.QuotaLowerLimit; + if (upperLimit == null && lowerLimit == null) continue; + var tag = $"全局(DV)"; + messageList.Add(SetMsg(tag, totalDv, upperLimit, lowerLimit, settingItem.Percent, warning)); + } + return messageList; + } private ClientRiskCheckItem checkGlobal(List positionList, string tag_prefix, ClientRiskCheckItem checkItem, QuotaTypeEnum quotaType) { @@ -5574,7 +5804,7 @@ namespace YLErp.Modules.RiskModule } return null; } - private List checkTrade(List positionList, List posiList, string tag_prefix, QuotaSetting[] settings, bool warning) + private List checkTrade(List positionList, List posiList, string tag_prefix, QuotaSetting[] settings, bool warning, double currentDv = -1) { if (string.IsNullOrEmpty(tag_prefix)) { @@ -5590,6 +5820,9 @@ namespace YLErp.Modules.RiskModule double? upperLimit, lowerLimit, currentValue = null; CheckQuotaMoitorModel current = positionList.FirstOrDefault(x => x.Current); + // currentDv未预计算时(其他调用方),才在这里算 + if (currentDv < 0 && settings.Any(s => s.QuotaIndex == "DV")) + currentDv = CalcRiskCheckDv(positionList, valuedateBLL.ValueDate).currentDv; foreach (var settingItem in settings) { if (warning) @@ -5632,6 +5865,10 @@ namespace YLErp.Modules.RiskModule currentValue = Convert.ToDouble(current.Delta); messageList.Add(SetMsg(tag, currentValue, upperLimit, lowerLimit, settingItem.Percent, warning)); break; + case "DV": + currentValue = currentDv; + messageList.Add(SetMsg(tag, currentValue, upperLimit, lowerLimit, settingItem.Percent, warning)); + break; default: continue; } @@ -5639,7 +5876,7 @@ namespace YLErp.Modules.RiskModule } return messageList; } - private ClientRiskCheckItem checkTrade(List positionList, string tag_prefix, ClientRiskCheckItem checkItem) + private ClientRiskCheckItem checkTrade(List positionList, string tag_prefix, ClientRiskCheckItem checkItem, double currentDv = 0) { if (string.IsNullOrEmpty(tag_prefix)) { @@ -5671,10 +5908,18 @@ namespace YLErp.Modules.RiskModule return checkItem; } return null; + case "DV": + currentValue = currentDv; + break; default: return null; } - + checkItem.currentValue = Math.Round(currentValue ?? 0, 2); + if (!ValidateQuoteResult(checkItem)) + { + return checkItem; + } + return null; } private List checkUnderlying(List positionList, List posiList, string tag_prefix, QuotaSetting[] settings, List settingAll, bool warning) diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 6cfcbcb7..929e557b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System.Linq.Expressions; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; +using YLErp.Helpers; using YLErp.Model; using YLErp.Model.Enum; using YLErp.Models; @@ -1291,6 +1292,7 @@ namespace YLErp.Modules.SwapModule //浮动端估值用信息 newEodPayPosition.UnderlyingPrice = UnderlyingCodePrice(newEodPayPosition.UnderlyingCode, eventFlow.EventDate, out decimal vobp); + newEodPayPosition.dv01 = Dv01Helper.CalcDv01(newEodPayPosition.UnderlyingCode, newEodPayPosition.PosiQuantity, newEodPayPosition.PosiDirection, newEodPayPosition.PositionType, vobp); newEodPayPosition.UnderlyingMarketValue = newEodPayPosition.UnderlyingPrice * newEodPayPosition.PosiQuantity * newEodPayPosition.ContractSize * shortRatio; //当日已实现 newEodPayPosition.TdCloseQty = closeQty; @@ -1361,6 +1363,7 @@ namespace YLErp.Modules.SwapModule int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; var price = UnderlyingCodePrice(eod.UnderlyingCode, dealDate, out decimal vobp); + curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); decimal tax = um.ValueAddedTax ?? 0; BondPaymentService bondPaymentService = new BondPaymentService(UserInfo); if (valueDate > td.StartDate.Value && curretEod.PosiQuantity > 0) @@ -1442,6 +1445,7 @@ namespace YLErp.Modules.SwapModule int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1; var price = UnderlyingCodePrice(eod.UnderlyingCode, dealDate, out decimal vobp); SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition); + curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp); curretEod.UnderlyingPrice = price; curretEod.UnderlyingMarketValue = curretEod.UnderlyingPrice * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio; curretEod.PosiMtmPnL = (curretEod.UnderlyingPrice - curretEod.PosiGrossPrice) * curretEod.PosiQuantity * curretEod.ContractSize * shortRatio * directionRatio; @@ -1614,6 +1618,7 @@ namespace YLErp.Modules.SwapModule curretEod.PosiTradingFee = position.PosiTradingFee; curretEod.UnderlyingPrice = UnderlyingCodePrice(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) //{ // curretEod.UnderlyingPrice = curretEod.PosiGrossPrice; diff --git a/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js b/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js index bcb2bea8..4c159554 100644 --- a/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js +++ b/YLErpWeb/wwwroot/Scripts/app/risk/quotaMonitor.js @@ -1,4 +1,4 @@ -var varieties = [{ id: 0, Name: '全部' }].concat(ylotc.varieties); +var varieties = [{ id: 0, Name: '全部' }].concat(ylotc.varieties); var remarkDict = {}; var valueDateShowStatus = true; $(function () { @@ -1587,6 +1587,33 @@ var colModel_trade = [ var style = "style='" + checkQuota(rowObject, 'SwapPercent') + "'"; return style; } + }, { + name: 'Quota_DV_Upper', + label: 'DV限额', + index: 'Quota_DV_Upper', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + return formatQuotaAbs(rowObject, 'DV'); + } + }, { + name: 'DV', + label: 'DV', + index: 'DV', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + if (!cellvalue && cellvalue !== 0 || cellvalue == "NaN") return ""; + var abs = Math.abs(cellvalue); + if (abs < 0.0001) return "0"; + return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = "style='" + checkQuota(rowObject, 'DV') + "'"; + return style; + } } ]; @@ -1676,6 +1703,51 @@ var colModel_global = [ formatter: function (cellvalue, options, rowObject) { return !cellvalue || cellvalue == "NaN" ? "" : cellvalue; } + }, { + name: 'Quota_DV_Upper', + label: 'DV限额', + index: 'Quota_DV_Upper', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + return formatQuotaAbs(rowObject, 'DV'); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = ''; + if (!rowObject.ParentKey) { + style += 'font-weight: bold;' + if (rowObject.BusinessType == "合计") { + style += 'font-size: 1rem;' + } + } + return "style='" + style + "'"; + } + }, { + name: 'DV', + label: 'DV', + index: 'DV', + width: 120, + align: 'right', + sortable: false, + formatter: function (cellvalue, options, rowObject) { + if (rowObject.BusinessType == "场内业务") { + return '
'; + } + if (!cellvalue && cellvalue !== 0 || cellvalue == "NaN") return ""; + if (Math.abs(cellvalue) < 0.0001) return "0"; + return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); + }, + cellattr: function (cellvalue, options, rowObject) { + var style = checkQuota(rowObject, 'DV'); + if (!rowObject.ParentKey) { + style += 'font-weight: bold;' + if (rowObject.BusinessType == "合计") { + style += 'font-size: 1rem;' + } + } + return "style='" + style + "'"; + } } ]; diff --git a/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js b/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js index d0ad962e..543e8caa 100644 --- a/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js +++ b/YLErpWeb/wwwroot/Scripts/app/risk/quotaSetting.js @@ -430,6 +430,7 @@ var colModel = [ case "非权益类业务规模": case "本年累计盈亏": case "浮动盈亏": + case "DV": // DV是绝对值指标,不允许百分比模式 break; default: html += "onclick='changePercent(this)' "; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js index 3818a18f..9770c225 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/EodPositionRisks.js @@ -610,6 +610,13 @@ function colModelGridEodSwap() { width: 150, align: 'center', formatter: StockEqvNotionalFormat, + }, { + name: 'position.dv01', + label: 'DV', + index: 'position.dv01', + width: 120, + align: 'center', + formatter: StockEqvNotionalFormat, }, { name: 'SwapTradeTypeStr', label: '互换类型',