using System; using System.Collections.Generic; using System.Linq; using YLErp.BLL; namespace YLErp.Modules.RiskEngine { /// /// 风控行情偏离类变量辅助方法,统一封装债券中债估值偏离和非债券行情价格偏离的取数、计算和命中明细生成逻辑。 /// public static class RiskMarketDeviationHelper { /// /// 计算当前交易所有浮动支付端的债券类净价偏离值,返回最大偏离值用于规则比较。 /// /// 数据库上下文。 /// 当前交易ID。 /// 包含最大净价偏离值和逐笔偏离明细的变量返回值。 public static RiskVariableValueDetail GetBondNetPriceDeviation(YLContext dbContext, int tradeId) { return GetBondValuationDeviation( dbContext, tradeId, "债券类净价偏离", "期初交割净价", "中债估值净价", p => p.PosiNetNoFeePrice, v => v.net_price); } /// /// 计算当前交易所有浮动支付端的债券类收益率偏离值,返回最大偏离值用于规则比较。 /// /// 数据库上下文。 /// 当前交易ID。 /// 包含最大收益率偏离值和逐笔偏离明细的变量返回值。 public static RiskVariableValueDetail GetBondYieldDeviation(YLContext dbContext, int tradeId) { return GetBondValuationDeviation( dbContext, tradeId, "债券类收益率偏离", "期初成交收益率", "中债估值收益率", p => p.InitYtm, v => v.yield); } /// /// 计算当前交易所有浮动支付端的非债券类价格偏离值,返回最大偏离值用于规则比较。 /// /// 数据库上下文。 /// 当前交易ID。 /// 包含最大价格偏离值和逐笔偏离明细的变量返回值。 public static RiskVariableValueDetail GetNonBondPriceDeviation(YLContext dbContext, int tradeId) { if (dbContext == null) throw new ArgumentNullException(nameof(dbContext)); var tradeDate = GetTradeDate(dbContext, tradeId); var floatingPositions = GetFloatingPaymentPositions(dbContext, tradeId) .Select(p => new { p.id, p.UnderlyingCode, p.PosiGrossPrice }) .ToList(); if (!floatingPositions.Any()) throw new Exception("浮动支付端记录不存在"); var underlyingCodes = floatingPositions .Select(p => p.UnderlyingCode) .Distinct() .ToList(); // 非债券类取交易日前最近一条行情,不使用银行间日历,也不要求行情日等于上一银行间交易日。 var eodRows = dbContext.eod_commodity_future_price .Where(e => underlyingCodes.Contains(e.UnderlyingCode) && e.ValueDate < tradeDate) .Select(e => new { e.id, e.UnderlyingCode, e.ValueDate, e.ClosePrice }) .ToList(); // 先按标的批量查出行情,再在内存中分组取最近日,避免每条浮动支付端单独访问数据库。 var eodByUnderlyingCode = eodRows .GroupBy(e => e.UnderlyingCode) .ToDictionary( g => g.Key, g => g.OrderByDescending(e => e.ValueDate).ThenBy(e => e.id).First()); var valuationItems = floatingPositions .Select(p => new { Position = p, Eod = eodByUnderlyingCode.ContainsKey(p.UnderlyingCode) ? eodByUnderlyingCode[p.UnderlyingCode] : null }) .ToList(); var missingEodItems = valuationItems .Where(x => x.Eod == null) .Select(x => $"记录ID {x.Position.id},标的{x.Position.UnderlyingCode}") .ToList(); if (missingEodItems.Any()) throw new Exception($"未找到交易日前行情收盘价:" + string.Join(";", missingEodItems)); var diffItems = valuationItems .Select(x => new { PositionId = x.Position.id, UnderlyingCode = x.Position.UnderlyingCode, PositionPrice = x.Position.PosiGrossPrice * 100m, MarketDate = x.Eod.ValueDate, MarketPrice = Convert.ToDecimal(x.Eod.ClosePrice), DiffAbs = Math.Abs(x.Position.PosiGrossPrice * 100m - Convert.ToDecimal(x.Eod.ClosePrice)) }) .ToList(); return BuildDeviationDetail( diffItems.Select(x => new DeviationItem { PositionId = x.PositionId, UnderlyingCode = x.UnderlyingCode, PositionValue = x.PositionPrice, MarketDate = x.MarketDate, MarketValue = x.MarketPrice, DiffAbs = x.DiffAbs }).ToList(), "非债券类价格偏离", "期初标的价格", "上一行情收盘价"); } /// /// 债券类中债估值偏离的公共计算入口,净价偏离和收益率偏离仅通过字段选择器区分取值字段。 /// /// 数据库上下文。 /// 当前交易ID。 /// 偏离规则名称,用于生成命中明细。 /// 交易侧取值名称,用于生成命中明细。 /// 市场估值取值名称,用于生成命中明细。 /// 交易侧字段选择器。 /// 中债估值字段选择器。 /// 包含最大偏离值和逐笔偏离明细的变量返回值。 private static RiskVariableValueDetail GetBondValuationDeviation( YLContext dbContext, int tradeId, string deviationName, string positionValueName, string marketValueName, Func positionValueSelector, Func valuationValueSelector) { if (dbContext == null) throw new ArgumentNullException(nameof(dbContext)); var tradeDate = GetTradeDate(dbContext, tradeId); var previousTradingDay = RiskCalendarHelper.GetPreviousInterbankTradingDay(dbContext, tradeDate); var nextTradingDate = previousTradingDay.AddDays(1); var floatingPositions = GetFloatingPaymentPositions(dbContext, tradeId).ToList(); if (!floatingPositions.Any()) throw new Exception("浮动支付端记录不存在"); var positionItems = floatingPositions .Select(p => new { p.id, p.UnderlyingCode, PositionValue = positionValueSelector(p) }) .ToList(); var missingPositionValueIds = positionItems .Where(p => !p.PositionValue.HasValue) .Select(p => p.id.ToString()) .ToList(); if (missingPositionValueIds.Any()) throw new Exception($"浮动支付端{positionValueName}为空,记录ID:" + string.Join("、", missingPositionValueIds)); var underlyingCodes = positionItems .Select(p => p.UnderlyingCode) .Distinct() .ToList(); // 债券类必须严格匹配上一银行间交易日当天的中债估值,不能简单取交易日前最近估值日。 var valuationRows = dbContext.china_bond_valuation .Where(v => underlyingCodes.Contains(v.bond_id) && v.valuation_date >= previousTradingDay && v.valuation_date < nextTradingDate) .ToList() .Select(v => new { v.id, v.bond_id, v.valuation_date, v.credibility, ValuationValue = valuationValueSelector(v) }) .Where(v => v.ValuationValue.HasValue) .ToList(); // 同一标的同一估值日可能有多条来源,按可信度优先,ID兜底稳定排序。 var valuationByBondId = valuationRows .GroupBy(v => v.bond_id) .ToDictionary( g => g.Key, g => g.OrderBy(v => v.credibility).ThenBy(v => v.id).First()); var valuationItems = positionItems .Select(p => new { Position = p, Valuation = valuationByBondId.ContainsKey(p.UnderlyingCode) ? valuationByBondId[p.UnderlyingCode] : null }) .ToList(); var missingValuationItems = valuationItems .Where(x => x.Valuation == null) .Select(x => $"记录ID {x.Position.id},标的{x.Position.UnderlyingCode}") .ToList(); if (missingValuationItems.Any()) throw new Exception($"未找到上一银行间交易日{previousTradingDay:yyyy-MM-dd}的{marketValueName}:" + string.Join(";", missingValuationItems)); var diffItems = valuationItems .Select(x => new DeviationItem { PositionId = x.Position.id, UnderlyingCode = x.Position.UnderlyingCode, PositionValue = x.Position.PositionValue.Value * 100m, MarketDate = x.Valuation.valuation_date, MarketValue = x.Valuation.ValuationValue.Value, DiffAbs = Math.Abs(x.Position.PositionValue.Value * 100m - x.Valuation.ValuationValue.Value) }) .ToList(); return BuildDeviationDetail(diffItems, deviationName, positionValueName, marketValueName); } /// /// 获取当前交易的交易日,所有行情偏离规则都以交易日作为市场数据取数基准。 /// /// 数据库上下文。 /// 当前交易ID。 /// 交易日日期部分。 private static DateTime GetTradeDate(YLContext dbContext, int tradeId) { var tradeDate = dbContext.trade .Where(t => t.id == tradeId) .Select(t => t.TradeDate) .FirstOrDefault(); if (!tradeDate.HasValue) throw new Exception("交易日为空"); return tradeDate.Value.Date; } /// /// 获取当前交易下全部浮动支付端记录,行情偏离类规则需要遍历同一TradeId下所有浮动支付端。 /// /// 数据库上下文。 /// 当前交易ID。 /// 浮动支付端记录查询对象。 private static IQueryable GetFloatingPaymentPositions(YLContext dbContext, int tradeId) { return dbContext.swap_position .Where(p => p.SwapTradeId == tradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 2 && !string.IsNullOrEmpty(p.UnderlyingCode)); } /// /// 统一生成行情偏离类变量返回值,变量值取最大偏离值,命中说明保留逐笔偏离明细。 /// /// 逐笔偏离结果。 /// 偏离规则名称。 /// 交易侧取值名称。 /// 市场侧取值名称。 /// 包含最大偏离值和逐笔偏离明细的变量返回值。 private static RiskVariableValueDetail BuildDeviationDetail( List diffItems, string deviationName, string positionValueName, string marketValueName) { var maxDiffItem = diffItems .OrderByDescending(x => x.DiffAbs) .ThenBy(x => x.PositionId) .First(); var deviatedItems = diffItems .Where(x => x.DiffAbs > 0m) .OrderByDescending(x => x.DiffAbs) .ThenBy(x => x.PositionId) .Select(x => $"记录ID {x.PositionId},标的{x.UnderlyingCode}:{positionValueName}{FormatDecimal(x.PositionValue)},{x.MarketDate:yyyy-MM-dd}{marketValueName}{FormatDecimal(x.MarketValue)},偏离{FormatDecimal(x.DiffAbs)}") .ToList(); string diffMessage = deviatedItems.Any() ? $"存在{deviationName}的浮动支付端记录:" + string.Join(";", deviatedItems) : $"未发现{deviationName}记录"; return new RiskVariableValueDetail(maxDiffItem.DiffAbs, diffMessage); } /// /// 格式化风控命中说明中的数值,避免展示过长小数。 /// /// 待格式化数值。 /// 最多9位小数的展示文本。 private static string FormatDecimal(decimal value) { return value.ToString("0.#########"); } /// /// 行情偏离计算的中间结果模型,用于把债券和非债券两类计算结果统一交给明细构建逻辑。 /// private class DeviationItem { public long PositionId { get; set; } public string UnderlyingCode { get; set; } public decimal PositionValue { get; set; } public DateTime MarketDate { get; set; } public decimal MarketValue { get; set; } public decimal DiffAbs { get; set; } } } }