Files
zszq-trs/YLErpDAL/Modules/RiskEngine/Helper/RiskMarketDeviationHelper.cs
T

341 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using YLErp.BLL;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控行情偏离类变量辅助方法,统一封装债券中债估值偏离和非债券行情价格偏离的取数、计算和命中明细生成逻辑。
/// </summary>
public static class RiskMarketDeviationHelper
{
/// <summary>
/// 计算当前交易所有浮动支付端的债券类净价偏离值,返回最大偏离值用于规则比较。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <returns>包含最大净价偏离值和逐笔偏离明细的变量返回值。</returns>
public static RiskVariableValueDetail GetBondNetPriceDeviation(YLContext dbContext, int tradeId)
{
return GetBondValuationDeviation(
dbContext,
tradeId,
"债券类净价偏离",
"期初交割净价",
"中债估值净价",
p => p.PosiNetNoFeePrice,
v => v.net_price);
}
/// <summary>
/// 计算当前交易所有浮动支付端的债券类收益率偏离值,返回最大偏离值用于规则比较。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <returns>包含最大收益率偏离值和逐笔偏离明细的变量返回值。</returns>
public static RiskVariableValueDetail GetBondYieldDeviation(YLContext dbContext, int tradeId)
{
return GetBondValuationDeviation(
dbContext,
tradeId,
"债券类收益率偏离",
"期初成交收益率",
"中债估值收益率",
p => p.InitYtm,
v => v.yield);
}
/// <summary>
/// 计算当前交易所有浮动支付端的非债券类价格偏离值,返回最大偏离值用于规则比较。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <returns>包含最大价格偏离值和逐笔偏离明细的变量返回值。</returns>
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(),
"非债券类价格偏离",
"期初标的价格",
"上一行情收盘价");
}
/// <summary>
/// 债券类中债估值偏离的公共计算入口,净价偏离和收益率偏离仅通过字段选择器区分取值字段。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <param name="deviationName">偏离规则名称,用于生成命中明细。</param>
/// <param name="positionValueName">交易侧取值名称,用于生成命中明细。</param>
/// <param name="marketValueName">市场估值取值名称,用于生成命中明细。</param>
/// <param name="positionValueSelector">交易侧字段选择器。</param>
/// <param name="valuationValueSelector">中债估值字段选择器。</param>
/// <returns>包含最大偏离值和逐笔偏离明细的变量返回值。</returns>
private static RiskVariableValueDetail GetBondValuationDeviation(
YLContext dbContext,
int tradeId,
string deviationName,
string positionValueName,
string marketValueName,
Func<YLErp.DBModels.swap_position, decimal?> positionValueSelector,
Func<YLErp.DBModels.ChinaBondValuation, decimal?> 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);
}
/// <summary>
/// 获取当前交易的交易日,所有行情偏离规则都以交易日作为市场数据取数基准。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <returns>交易日日期部分。</returns>
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;
}
/// <summary>
/// 获取当前交易下全部浮动支付端记录,行情偏离类规则需要遍历同一TradeId下所有浮动支付端。
/// </summary>
/// <param name="dbContext">数据库上下文。</param>
/// <param name="tradeId">当前交易ID。</param>
/// <returns>浮动支付端记录查询对象。</returns>
private static IQueryable<YLErp.DBModels.swap_position> 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));
}
/// <summary>
/// 统一生成行情偏离类变量返回值,变量值取最大偏离值,命中说明保留逐笔偏离明细。
/// </summary>
/// <param name="diffItems">逐笔偏离结果。</param>
/// <param name="deviationName">偏离规则名称。</param>
/// <param name="positionValueName">交易侧取值名称。</param>
/// <param name="marketValueName">市场侧取值名称。</param>
/// <returns>包含最大偏离值和逐笔偏离明细的变量返回值。</returns>
private static RiskVariableValueDetail BuildDeviationDetail(
List<DeviationItem> 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);
}
/// <summary>
/// 格式化风控命中说明中的数值,避免展示过长小数。
/// </summary>
/// <param name="value">待格式化数值。</param>
/// <returns>最多9位小数的展示文本。</returns>
private static string FormatDecimal(decimal value)
{
return value.ToString("0.#########");
}
/// <summary>
/// 行情偏离计算的中间结果模型,用于把债券和非债券两类计算结果统一交给明细构建逻辑。
/// </summary>
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; }
}
}
}