From de628719dc70e57f9e43e651e2c8ef742224ec89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=B9=E5=B3=B0?= Date: Tue, 7 Jul 2026 13:51:24 +0800 Subject: [PATCH] resolve confilct QuotaMonitorService.cs --- .../Modules/RiskModule/QuotaMonitorService.cs | 329 +++++++++++++++--- 1 file changed, 275 insertions(+), 54 deletions(-) diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs index 189382a8..677dde69 100644 --- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs +++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs @@ -795,6 +795,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)); @@ -907,6 +920,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() @@ -915,6 +932,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(); @@ -926,6 +948,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() @@ -933,14 +970,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 { @@ -963,9 +1006,13 @@ 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) => { @@ -988,6 +1035,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() @@ -996,6 +1047,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() { @@ -1009,7 +1065,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 { @@ -1323,11 +1384,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); @@ -1337,6 +1410,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); @@ -1351,6 +1431,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); @@ -1430,11 +1511,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); @@ -1444,10 +1540,18 @@ 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); } + var total = new QuotaMonitor_Trade() { TradeNumber = "合计", @@ -1458,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); @@ -3522,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 互换 @@ -3617,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 客户 @@ -4335,7 +4468,7 @@ namespace YLErp.Modules.RiskModule return QuotaCheck(ref res, false); } - public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning, IEnumerable ignoreRiskRuleIds = null) + public bool QuotaCheck(ref TradeOpenResult res, bool ignoreRiskWarning) { if (res == null || res.Trade == null) { @@ -4368,6 +4501,7 @@ namespace YLErp.Modules.RiskModule new TradeRiskCheckLogService(UserInfo).AddLog(quotaObj); } //否则的情况是上次没算,这次是预警,或上次算了,结果是不通过\通过或预警,这次是预警或不通过,提示用户; + res.RetCode = TradeOpenRetCode.QuotaTrialError; res.TrialDataId = quotaObj.id; //汇总各类检查详情,避免只展示RiskWarningDetails而漏掉其它老风控检查 var detailParts = new List(); @@ -4381,27 +4515,14 @@ namespace YLErp.Modules.RiskModule detailParts.Add($"限额预警:{quotaObj.QuotaWarningDetails}"); res.ErrorMsg = string.Join("\n", detailParts); //确认本次为需审批后,二次特批。因为只靠点击“交易特批”的ignoreRiskWarning,不能保证本次校验通过。 - var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.RiskWarning - && !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails) - && (quotaObj.ApprovalRuleIds?.Any() ?? false); - //当前为老风控特批,不是新风控需审批 - res.OldRiskNeedSpecialApproval = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning; - if (isRiskApprovalWarning) + var isRiskApprovalWarning = quotaObj.TrialStatus == QuotaTrialStatusEnum.Warning + && !string.IsNullOrWhiteSpace(quotaObj.RiskWarningDetails); + //需清除ErrorMsg,不然外部调用会认为失败 + //触发需审批,交易特批逻辑 + if (ignoreRiskWarning && isRiskApprovalWarning) { - var currentApprovalRuleIds = quotaObj.ApprovalRuleIds ?? new List(); - res.IgnoredRiskRuleIds = currentApprovalRuleIds; - //需清除ErrorMsg,不然外部调用会认为失败 - //触发需审批,交易特批逻辑 - if (ignoreRiskWarning) - { - var ignoredRuleIdSet = new HashSet((ignoreRiskRuleIds ?? Enumerable.Empty()).Where(o => !string.IsNullOrWhiteSpace(o))); - var newApprovalRuleIds = currentApprovalRuleIds.Where(o => !ignoredRuleIdSet.Contains(o)).ToList(); - if (!newApprovalRuleIds.Any()) - { - res.ErrorMsg = string.Empty; - return true; - } - } + res.ErrorMsg = string.Empty; + return true; } res.RetCode = isRiskApprovalWarning ? TradeOpenRetCode.RiskWarning : TradeOpenRetCode.QuotaTrialError; return false; @@ -4440,6 +4561,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); @@ -4467,7 +4591,7 @@ namespace YLErp.Modules.RiskModule { continue; } - clientRiskCheckItem = CheckQuota(checkPoisiList, settingItem.QuotaType, clientRiskCheckItem, settingItem.QuotaRange, allList); + clientRiskCheckItem = CheckQuota(checkPoisiList, settingItem.QuotaType, clientRiskCheckItem, settingItem.QuotaRange, allList, 0d, 0d); if (clientRiskCheckItem != null) { clientRiskCheckItem.quotaType = $"{EnumHelper.GetDescriptionByName(settingItem.QuotaType)}({settingItem.QuotaIndex})"; @@ -4499,6 +4623,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(); @@ -4944,8 +5112,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); } } } @@ -4972,6 +5142,8 @@ namespace YLErp.Modules.RiskModule result.RiskWarningDetails = riskWarningMsg ?? ""; result.QuotaWarningDetails = quotaWarningMsg ?? ""; result.AvailableForClient = availableMsg ?? ""; + result.TrialSource = trialSource; + _logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}"); // ===== 新风控引擎接入点(设计文档 4.6.1) ===== try @@ -4982,6 +5154,7 @@ namespace YLErp.Modules.RiskModule TradeId = tradeId, TriggerPoint = "BOOK_CONFIRM" }; + tradeObj.StockEqvNotional = 10000000000; riskContext.DataMap["trade"] = tradeObj; var sameUnderlyingTotalNotional = DbContext.trade .Where(t => t.ValidState != "InValid" @@ -5021,25 +5194,12 @@ namespace YLErp.Modules.RiskModule } if (riskResult.NeedApproval) { - if (result.TrialStatus != QuotaTrialStatusEnum.Error && result.TrialStatus != QuotaTrialStatusEnum.Warning) + if (result.TrialStatus != QuotaTrialStatusEnum.Error) { - result.TrialStatus = QuotaTrialStatusEnum.RiskWarning; + result.TrialStatus = QuotaTrialStatusEnum.Warning; } result.RiskWarningDetails += "[风控引擎] 规则触发:需审批\n"; - var approvalTriggeredRules = riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval).ToList(); - var approvalTriggeredRuleIds = approvalTriggeredRules.Select(r => r.RuleId) - .Where(r => !string.IsNullOrWhiteSpace(r)) - .ToList(); - var showTipTriggeredRules = riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.ShowTip).ToList(); - var showTipTriggeredRuleIds = showTipTriggeredRules.Select(r => r.RuleId) - .Where(r => !string.IsNullOrWhiteSpace(r)) - .ToList(); - // 二次确认时,需审批规则允许按首次命中结果忽略;同时已展示过的提示规则也不再重复展示 - var processedRiskRuleIds = approvalTriggeredRuleIds.Concat(showTipTriggeredRuleIds) - .Distinct() - .ToList(); - result.ApprovalRuleIds = processedRiskRuleIds; - foreach (var triggeredRule in approvalTriggeredRules) + foreach (var triggeredRule in riskResult.TriggeredRules.Where(r => r.ControlStrategy == RiskControlStrategy.Approval)) { result.RiskWarningDetails += $"规则ID:{triggeredRule.RuleId};规则名称:{triggeredRule.RuleName};规则说明:{triggeredRule.RuleText}\n"; } @@ -5064,7 +5224,6 @@ namespace YLErp.Modules.RiskModule } // ===== 新风控引擎接入结束 ===== - //_logger.Info($"[限额试算] 试算完成 - tradeId: {tradeId}, TrialStatus: {result.TrialStatus}, FundCheck: {fundStatus}, QuotaCheck: {quotaStatus}"); SaveQuotaTrial(result); return result; } @@ -5435,7 +5594,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(); @@ -5445,11 +5604,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)); @@ -5503,17 +5665,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); @@ -5548,7 +5718,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(); @@ -5570,6 +5741,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; @@ -5592,7 +5769,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(); @@ -5614,6 +5792,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; @@ -5677,6 +5862,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) { @@ -5711,7 +5917,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)) { @@ -5727,6 +5933,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) @@ -5769,6 +5978,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; } @@ -5776,7 +5989,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)) { @@ -5808,10 +6021,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)