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 { /// /// 风控规则专用日历辅助类。 /// 用于按规则对应的市场日历确认“上一收盘日”,避免简单按行情表倒序取最近日期导致口径偏差。 /// public static class RiskCalendarHelper { /// /// 按当前风控执行使用的数据库上下文保存日历缓存,同一次风控检查内复用,数据库上下文释放后不阻止缓存被回收。 /// private static readonly ConditionalWeakTable>> HolidayCaches = new ConditionalWeakTable>>(); /// /// 获取指定日期的上一银行间交易日。 /// /// 当前风控执行使用的数据库上下文。 /// 当前交易日或业务基准日。 /// 严格早于入参日期的上一银行间交易日。 /// 数据库上下文为空。 /// 缺少银行间日历、日历内容异常或在保护范围内找不到上一交易日。 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}交易日"); } /// /// 获取指定年份的银行间非交易日集合。 /// /// 当前风控执行使用的数据库上下文。 /// 日历年份。 /// 单次风控检查内按市场和年份共享的非交易日缓存。 /// 格式为 yyyy,MM,dd 的非交易日集合。 private static HashSet GetHolidays(YLContext dbContext, int year, string country, string calendarName, Dictionary> 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 holidayList; try { holidayList = JsonConvert.DeserializeObject>(calendar.HolidayJson); } catch (Exception ex) { throw new Exception($"{year}年{calendarName}日历HolidayJson解析失败", ex); } holidays = new HashSet(holidayList ?? new List()); holidayCache[cacheKey] = holidays; return holidays; } } }