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

108 lines
5.2 KiB
C#

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using YLErp.BLL;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 风控规则专用日历辅助类。
/// 用于按规则对应的市场日历确认“上一收盘日”,避免简单按行情表倒序取最近日期导致口径偏差。
/// </summary>
public static class RiskCalendarHelper
{
/// <summary>
/// 按当前风控执行使用的数据库上下文保存日历缓存,同一次风控检查内复用,数据库上下文释放后不阻止缓存被回收。
/// </summary>
private static readonly ConditionalWeakTable<YLContext, Dictionary<string, HashSet<string>>> HolidayCaches =
new ConditionalWeakTable<YLContext, Dictionary<string, HashSet<string>>>();
/// <summary>
/// 获取指定日期的上一银行间交易日。
/// </summary>
/// <param name="dbContext">当前风控执行使用的数据库上下文。</param>
/// <param name="date">当前交易日或业务基准日。</param>
/// <returns>严格早于入参日期的上一银行间交易日。</returns>
/// <exception cref="ArgumentNullException">数据库上下文为空。</exception>
/// <exception cref="Exception">缺少银行间日历、日历内容异常或在保护范围内找不到上一交易日。</exception>
public static DateTime GetPreviousInterbankTradingDay(YLContext dbContext, DateTime date)
{
return GetPreviousTradingDay(dbContext, date, "IB", "银行间");
}
public static DateTime GetPreviousExchangeTradingDay(YLContext dbContext, DateTime date)
{
return GetPreviousTradingDay(dbContext, date, "CHN", "交易所");
}
private static DateTime GetPreviousTradingDay(YLContext dbContext, DateTime date, string country, string calendarName)
{
if (dbContext == null)
throw new ArgumentNullException(nameof(dbContext));
// 以本次风控检查的数据库上下文为缓存边界,避免不同规则重复读取和解析相同市场日历。
var holidayCache = HolidayCaches.GetOrCreateValue(dbContext);
var currentDate = date.Date.AddDays(-1);
// 最多向前查 370 天,既覆盖跨年和长假场景,也避免日历配置异常时出现无限循环。
for (var i = 0; i < 370; i++)
{
var holidays = GetHolidays(dbContext, currentDate.Year, country, calendarName, holidayCache);
var currentDateText = currentDate.ToString("yyyy,MM,dd", CultureInfo.InvariantCulture);
// calendar.HolidayJson 存的是非交易日;不在非交易日集合内,即认为是对应市场的交易日。
if (!holidays.Contains(currentDateText))
return currentDate;
currentDate = currentDate.AddDays(-1);
}
throw new Exception($"未找到{date:yyyy-MM-dd}的上一{calendarName}交易日");
}
/// <summary>
/// 获取指定年份的银行间非交易日集合。
/// </summary>
/// <param name="dbContext">当前风控执行使用的数据库上下文。</param>
/// <param name="year">日历年份。</param>
/// <param name="holidayCache">单次风控检查内按市场和年份共享的非交易日缓存。</param>
/// <returns>格式为 yyyy,MM,dd 的非交易日集合。</returns>
private static HashSet<string> GetHolidays(YLContext dbContext, int year, string country, string calendarName, Dictionary<string, HashSet<string>> holidayCache)
{
// 缓存需要同时区分市场和年份,避免银行间与交易所同一年日历相互串用。
var cacheKey = $"{country.ToUpperInvariant()}:{year}";
if (holidayCache.TryGetValue(cacheKey, out var holidays))
return holidays;
// 同一年可能存在多种市场日历,按规则对应的市场代码读取非交易日。
var calendar = dbContext.calendar
.Where(c => c.Year == year && (c.ValidState == null || c.ValidState != ConsGlobal.InValid))
.ToList()
.FirstOrDefault(c => string.Equals(c.Country, country, StringComparison.OrdinalIgnoreCase));
if (calendar == null)
throw new Exception($"未找到{year}年{calendarName}日历");
if (string.IsNullOrWhiteSpace(calendar.HolidayJson))
throw new Exception($"{year}年{calendarName}日历HolidayJson为空");
List<string> holidayList;
try
{
holidayList = JsonConvert.DeserializeObject<List<string>>(calendar.HolidayJson);
}
catch (Exception ex)
{
throw new Exception($"{year}年{calendarName}日历HolidayJson解析失败", ex);
}
holidays = new HashSet<string>(holidayList ?? new List<string>());
holidayCache[cacheKey] = holidays;
return holidays;
}
}
}