using System; using System.Collections.Generic; using System.Linq; using YLErp.BLL; using YLErp.DBModels; using YLErp.Enums; using YLErp.Helpers; using YLErp.Models; using YLErp.Modules.UnderlyingModule; namespace YLErp.Modules.MarginModule { /// /// 预付金模板V2 取数帮助类:按交易绑定的模板(trade_margin_template → margin_template_v2 → margin_template_detail) /// 取 初始预付金率x / 维持预付金率y,供交易确认书、预付金计算等消费点统一使用。 /// 匹配维度:生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。 /// public static class MarginTemplateV2RateHelper { private static readonly IYcLogger logger = LogFactory.GetLogger("预付金模板取数"); /// /// 取数结果。率为小数(如 0.1 表示 10%,与数据库一致),调用方直接用。 /// public class MarginRateResult { /// /// 命中的模板 /// public margin_template_v2 Template { get; set; } /// /// 命中的明细参数行(无预付金规则时为 null) /// public margin_template_detail Detail { get; set; } /// /// 初始预付金率 x(小数) /// public decimal? InitRate { get; set; } /// /// 维持预付金率 y(小数) /// public decimal? MaintainRate { get; set; } } /// /// 按交易绑定的模板取 初始/维持预付金率。 /// /// 交易ID /// 标的代码(用于计算利率债期限档) /// 标的资产类型(trade/swap_position 的 UnderlyingInstrumentType,如 TBonds) /// 业务日期 /// 命中返回结果;交易未绑定模板、模板无效、规则非 无预付金/区间追保结构、或明细无匹配行时返回 null(由调用方决定兜底) public static MarginRateResult GetTradeMarginRate(int tradeId, string underlyingCode, string underlyingInstrumentType, DateTime valueDate) { using (var db = new YLContext()) { return GetTradeMarginRate(tradeId, underlyingCode, underlyingInstrumentType, valueDate, db); } } /// /// 按交易绑定的模板取 初始/维持预付金率(调用方传入 DbContext,供批量场景复用连接)。 /// R1 三层级找到即停(2026-08-21 补齐客户/全局两级):交易绑定(自定义)→ 客户默认(client_margin_template, /// 按客户或客户等级)→ 全局默认(margin_template_v2 IsDefault&&!IsForClient,适用结构需含收益互换); /// 三级均未命中返回 null(由调用方兜底,此处留日志:视为无预付金要求)。 /// public static MarginRateResult GetTradeMarginRate(int tradeId, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db) { //R1 三层级找到即停:交易绑定(自定义)→ 客户默认 → 全局默认 var clientId = db.trade.AsNoTracking().Where(t => t.id == tradeId).Select(t => (int?)t.ClientId).FirstOrDefault() ?? 0; var template = ResolveTieredTemplate(tradeId, clientId, valueDate, db); if (template == null) { logger.Info($"【警告】预付金模板取数:交易{tradeId}(客户{clientId})三级(交易绑定/客户默认/全局默认)均未命中有效模板,视为无预付金要求"); return null; } return GetRateByTemplate(template, underlyingCode, underlyingInstrumentType, valueDate, db); } /// /// 已解析模板的取率(GetTradeMarginRate 去掉三层级解析的部分;引擎批量解析后逐交易调用,避免二次解析)。 /// 规则非 无预付金/区间追保结构 或明细无匹配行时返回 null(由调用方兜底)。 /// public static MarginRateResult GetRateByTemplate(margin_template_v2 template, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db) { //2.无预付金规则:率直接为 0 if (template.RuleType == (int)MarginRuleTypeEnum.无预付金) { return new MarginRateResult { Template = template, InitRate = 0m, MaintainRate = 0m }; } if (template.RuleType != (int)MarginRuleTypeEnum.区间追保结构) { //其他规则不在本帮助类支持范围,显式返回 null return null; } //3.区间追保结构:取 ValueDate 最新生效的一组明细(同一 ValueDate 下有多行参数组) var detailQuery = db.margin_template_detail.AsNoTracking() .Where(x => x.MarginTemplateId == template.id && x.ValueDate <= valueDate); if (!detailQuery.Any()) { return null; } var latestValueDate = detailQuery.Max(x => x.ValueDate); var details = detailQuery.Where(x => x.ValueDate == latestValueDate).ToList(); //4.利率债/分档ETF 期限档匹配:精确档 → "全部"(BondTerm 为空)兜底 var term = UnderlyingHelper.GetApplicableMarginTerm(underlyingCode, valueDate); var matched = details.Where(x => x.SpanConfig != null && x.SpanConfig.BondTerm == term).ToList(); if (!matched.Any()) { matched = details.Where(x => x.SpanConfig == null || string.IsNullOrEmpty(x.SpanConfig.BondTerm)).ToList(); } if (!matched.Any()) { return null; } //5.ETF 子类行优先(子类区分度高于期限):标的有 EtfSubType(基金类)时优先取 EtfKind=子类 的行—— //期限档匹配未命中子类行时再单独尝试"子类 + BondTerm 空"(子类不分档通配);无子类行维持原 matched(基金通配兜底) var underlyingCategory = GetUnderlyingCategory(underlyingCode, underlyingInstrumentType); if (underlyingCategory != null) { matched = PreferCategoryRows(matched, details, underlyingCategory); } if (template.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.CustomInstrumentType && Enum.TryParse(underlyingInstrumentType, out var instrumentFlag)) { var byInstrument = matched.Where(x => (x.UnderlyingType & instrumentFlag) > 0).ToList(); if (byInstrument.Any()) { matched = byInstrument; } else { var wildcard = matched.Where(x => x.UnderlyingType == UnderlyingTypeEnum.None || x.UnderlyingType == UnderlyingTypeEnum.All).ToList(); if (wildcard.Any()) { matched = wildcard; } } } var detail = matched.First(); return new MarginRateResult { Template = template, Detail = detail, InitRate = ToDecimalRate(detail.MarginRatio1), MaintainRate = ToDecimalRate(detail.MarginRatio2) }; } /// /// ETF 子类行优先匹配(纯函数,单测覆盖): /// termMatched 为期限档匹配结果(含精确档/BondTerm 空兜底),allDetails 为该模板当日全部明细行。 /// 优先取 termMatched 中 EtfKind=category 的行;没有则取"allDetails 中 EtfKind=category 且 BondTerm 空"的子类不分档行; /// 都没有时原样返回 termMatched(基金通配行兜底)。 /// public static List PreferCategoryRows(List termMatched, List allDetails, string category) { var byCategory = termMatched.Where(x => x.SpanConfig != null && x.SpanConfig.EtfKind == category).ToList(); if (byCategory.Any()) { return byCategory; } var categoryWildcard = allDetails .Where(x => x.SpanConfig != null && x.SpanConfig.EtfKind == category && string.IsNullOrEmpty(x.SpanConfig.BondTerm)) .ToList(); return categoryWildcard.Count > 0 ? categoryWildcard : termMatched; } /// /// R1 模板三层级找到即停解析(public 供集成测试与后续消费方复用): /// 1.交易绑定 trade_margin_template(自定义/录入时显式选择,ValueDate 最新 ≤ 业务日)——找到即停, /// 绑定指向已失效模板时同样停止(不向下回退,避免"改绑定失效后悄悄换成别的规则"),返回 null 并留日志; /// 2.客户默认 client_margin_template(按客户 ClientId 或按客户等级 ClientId=0+ClientLevel, /// 客户等级经 client.LevelId → clientlevel.LevelName 解析;ValueDate 最新 ≤ 业务日), /// 模板须有效且适用结构含收益互换; /// 3.全局默认 margin_template_v2(IsDefault && !IsForClient && IsValid && 适用收益互换,ValueDate 最新)。 /// public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db) { //1.交易绑定(找到即停) if (tradeId > 0) { var bindingTemplateId = db.trade_margin_template.AsNoTracking() .Where(x => x.TradeId == tradeId.Value && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) .Select(x => (int?)x.MarginTemplateId) .FirstOrDefault(); if (bindingTemplateId > 0) { var bound = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == bindingTemplateId.Value && x.IsValid); if (bound != null) { return bound; } logger.Info($"【警告】预付金模板取数:交易{tradeId}绑定的模板{bindingTemplateId}已失效或不存在,按找到即停不向下回退"); return null; } } //2.客户默认(client_margin_template:按客户 或 按客户等级;客户表在独立客户库) //先按 有效+适用收益互换 过滤再取绑定日期最新——同日并存"互换绑定+期权绑定"(页面互斥只挡适用结构重叠)时, //若先取绑定再过滤会随机命中期权绑定而漏掉互换绑定、误穿透到全局 if (clientId > 0) { string levelName = null; using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser)) { var levelId = clientDb.client.AsNoTracking().Where(c => c.id == clientId).Select(c => c.LevelId).FirstOrDefault(); if (levelId > 0) { levelName = clientDb.clientlevel.AsNoTracking().Where(l => l.id == levelId).Select(l => l.LevelName).FirstOrDefault(); } } var clientTemplate = (from cmt in db.client_margin_template.AsNoTracking() join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0 && (cmt.ClientId == clientId || (cmt.ClientId == 0 && cmt.ClientLevel != "" && cmt.ClientLevel == levelName)) && mt.IsValid && mt.TradeTypes.Contains("收益互换") orderby cmt.ValueDate descending, cmt.id descending select mt).FirstOrDefault(); if (clientTemplate != null) { return clientTemplate; } } //3.全局默认 return db.margin_template_v2.AsNoTracking() .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) .FirstOrDefault(); } /// /// R1 三层级找到即停解析(批量版,BUG-02 修正:EOD 引擎与结算查询按交易列表解析, /// 只配了客户默认/全局默认模板的交易与交易级绑定同权生效): /// 语义与单笔 ResolveTieredTemplate 一致—— /// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退); /// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效且适用结构含收益互换,先过滤有效再取绑定最新); /// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,ValueDate/id 最新)。 /// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。 /// public static Dictionary ResolveTieredTemplates(List trades, DateTime valueDate, YLContext db) { var result = new Dictionary(); if (trades == null || trades.Count == 0) { return result; } var tradeIds = trades.Select(t => t.id).Distinct().ToList(); //1.交易绑定(批量取回后内存取每交易最新一条,同日多条按 id 最新兜底——与单笔解析一致) var bindingRows = db.trade_margin_template.AsNoTracking() .Where(x => tradeIds.Contains(x.TradeId) && x.ValueDate <= valueDate) .Select(x => new { x.TradeId, x.MarginTemplateId, x.ValueDate, x.id }) .ToList(); var bindingByTrade = bindingRows .GroupBy(x => x.TradeId) .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.ValueDate).ThenByDescending(x => x.id).First().MarginTemplateId); var boundTemplates = new Dictionary(); if (bindingByTrade.Values.Distinct().Any()) { var boundTemplateIds = bindingByTrade.Values.Distinct().ToList(); boundTemplates = db.margin_template_v2.AsNoTracking() .Where(x => boundTemplateIds.Contains(x.id) && x.IsValid) .ToDictionary(x => x.id); } //2.客户默认(client_margin_template:按客户 或 按客户等级;客户表在独立客户库,等级名批量解析) var clientIds = trades.Select(t => t.ClientId).Where(c => c > 0).Distinct().ToList(); var clientTemplateByClient = new Dictionary(); if (clientIds.Any()) { var levelNames = new Dictionary(); using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser)) { var clientLevels = clientDb.client.AsNoTracking() .Where(c => clientIds.Contains(c.id) && c.LevelId > 0) .Select(c => new { c.id, LevelId = c.LevelId ?? 0 }) .ToList(); var levelIds = clientLevels.Select(c => c.LevelId).Distinct().ToList(); var levelNameById = clientDb.clientlevel.AsNoTracking() .Where(l => levelIds.Contains(l.id)) .Select(l => new { l.id, l.LevelName }) .ToList() .ToDictionary(l => l.id, l => l.LevelName); foreach (var c in clientLevels) { levelNames[c.id] = levelNameById.TryGetValue(c.LevelId, out var name) ? name : null; } } //先按 有效+适用收益互换 过滤再按绑定取最新(与单笔解析一致,同日互换/期权并存不误穿透) var clientBindingRows = (from cmt in db.client_margin_template.AsNoTracking() join mt in db.margin_template_v2.AsNoTracking() on cmt.MarginTemplateId equals mt.id where cmt.ValueDate <= valueDate && cmt.MarginTemplateId > 0 && (clientIds.Contains(cmt.ClientId) || (cmt.ClientId == 0 && cmt.ClientLevel != "")) && mt.IsValid && mt.TradeTypes.Contains("收益互换") orderby cmt.ValueDate descending, cmt.id descending select new { cmt.ClientId, cmt.ClientLevel, Template = mt }).ToList(); foreach (var clientId in clientIds) { var levelName = levelNames.TryGetValue(clientId, out var name) ? name : null; //每客户找到即停:按客户 或 按客户等级 同查询取绑定最新(与单笔解析一致; //直接绑定与等级绑定并存的优先级方案未定义(BUG-16),维持"绑定日期最新"现状不改语义) var hit = clientBindingRows .FirstOrDefault(r => r.ClientId == clientId || (r.ClientId == 0 && r.ClientLevel == levelName)); if (hit != null) { clientTemplateByClient[clientId] = hit.Template; } } } //3.全局默认(全部交易共享,一次查询) var globalDefault = db.margin_template_v2.AsNoTracking() .Where(x => x.IsDefault && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换") && x.ValueDate <= valueDate) .OrderByDescending(x => x.ValueDate) .ThenByDescending(x => x.id) .FirstOrDefault(); foreach (var trade in trades) { if (result.ContainsKey(trade.id)) { continue; } if (bindingByTrade.TryGetValue(trade.id, out var templateId)) { //一级找到即停:绑定指向失效模板时不向下回退(与单笔解析一致,仅留日志) if (boundTemplates.TryGetValue(templateId, out var bound)) { result[trade.id] = bound; } else { logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}已失效或不存在,按找到即停不向下回退"); } continue; } if (clientTemplateByClient.TryGetValue(trade.ClientId, out var clientTemplate)) { result[trade.id] = clientTemplate; continue; } if (globalDefault != null) { result[trade.id] = globalDefault; } } return result; } /// /// 标的细分分类判定钩子(ETF 子类,2026-08-24 实装):基金类标的返回标的维护页维护的 /// underlying_manager.EtfSubType(取值来自数据字典"ETF 子类":国债 ETF/政金债 ETF/…/科创债 ETF/可转债 ETF, /// 基金类必填);非基金或未维护返回 null → 走标志位匹配/通配行兜底(债券/股票等行为与现状一致)。 /// 后续其他细分(中债指数 IndexKind 等)在本方法内扩展。 /// public static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType) { var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(underlyingCode); if (underlying == null || underlying.UnderlyingInstrumentType != ConsGlobal.InstrumentType.Fund) { return null; } return string.IsNullOrWhiteSpace(underlying.EtfSubType) ? null : underlying.EtfSubType.Trim(); } /// /// 明细率值换算:页面上 vue-number-input 以 % 展示,数据库存的就是小数(如 0.05 表示 5%),直接取用 /// private static decimal? ToDecimalRate(double? ratio) { return ratio.HasValue ? (decimal)ratio.Value : (decimal?)null; } } }