486 lines
29 KiB
C#
486 lines
29 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 预付金模板V2 取数帮助类:按交易绑定的模板(trade_margin_template → margin_template_v2 → margin_template_detail)
|
||
/// 取 初始预付金率x / 维持预付金率y,供交易确认书、预付金计算等消费点统一使用。
|
||
/// 匹配维度:簿记账户范围(BookIds 显式限定且匹配者优先于空=全部的通配,同层多候选 2026-08-28 裁定)→
|
||
/// 生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
|
||
/// </summary>
|
||
public static class MarginTemplateV2RateHelper
|
||
{
|
||
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金模板取数");
|
||
|
||
private sealed class ClientTemplateCandidate
|
||
{
|
||
public int ClientId { get; set; }
|
||
public string ClientLevel { get; set; }
|
||
public margin_template_v2 Template { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取数结果。率为小数(如 0.1 表示 10%,与数据库一致),调用方直接用。
|
||
/// </summary>
|
||
public class MarginRateResult
|
||
{
|
||
/// <summary>
|
||
/// 命中的模板
|
||
/// </summary>
|
||
public margin_template_v2 Template { get; set; }
|
||
|
||
/// <summary>
|
||
/// 命中的明细参数行(无预付金规则时为 null)
|
||
/// </summary>
|
||
public margin_template_detail Detail { get; set; }
|
||
|
||
/// <summary>
|
||
/// 初始预付金率 x(小数)
|
||
/// </summary>
|
||
public decimal? InitRate { get; set; }
|
||
|
||
/// <summary>
|
||
/// 维持预付金率 y(小数)
|
||
/// </summary>
|
||
public decimal? MaintainRate { get; set; }
|
||
}
|
||
|
||
internal static int? GetTradeAssetId(int? tradeId, YLContext db)
|
||
{
|
||
if (!tradeId.HasValue || tradeId.Value <= 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return db.trade.AsNoTracking()
|
||
.Where(x => x.id == tradeId.Value)
|
||
.Select(x => (int?)x.AssetId)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按交易绑定的模板取 初始/维持预付金率。
|
||
/// </summary>
|
||
/// <param name="tradeId">交易ID</param>
|
||
/// <param name="underlyingCode">标的代码(用于计算利率债期限档)</param>
|
||
/// <param name="underlyingInstrumentType">标的资产类型(trade/swap_position 的 UnderlyingInstrumentType,如 TBonds)</param>
|
||
/// <param name="valueDate">业务日期</param>
|
||
/// <returns>命中返回结果;交易未绑定模板、模板无效、规则非 无预付金/区间追保结构、或明细无匹配行时返回 null(由调用方决定兜底)</returns>
|
||
public static MarginRateResult GetTradeMarginRate(int tradeId, string underlyingCode, string underlyingInstrumentType, DateTime valueDate)
|
||
{
|
||
using (var db = new YLContext())
|
||
{
|
||
return GetTradeMarginRate(tradeId, underlyingCode, underlyingInstrumentType, valueDate, db);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按交易绑定的模板取 初始/维持预付金率(调用方传入 DbContext,供批量场景复用连接)。
|
||
/// R1 三层级找到即停(2026-08-21 补齐客户/全局两级):交易绑定(自定义)→ 客户默认(client_margin_template,
|
||
/// 按客户或客户等级)→ 全局默认(margin_template_v2 IsDefault&&!IsForClient,适用结构需含收益互换);
|
||
/// 三级均未命中返回 null(由调用方兜底,此处留日志:视为无预付金要求)。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 已解析模板的取率(GetTradeMarginRate 去掉三层级解析的部分;引擎批量解析后逐交易调用,避免二次解析)。
|
||
/// 规则非 无预付金/区间追保结构 或明细无匹配行时返回 null(由调用方兜底)。
|
||
/// </summary>
|
||
public static MarginRateResult GetRateByTemplate(margin_template_v2 template, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db)
|
||
{
|
||
//2.无预付金规则:率直接为 0
|
||
if (template.RuleType == (int)MarginRuleTypeEnum.无预付金)
|
||
{
|
||
logger.Info($"预付金模板取数:模板{template.id}(规则=无预付金)标的{underlyingCode},x=y=0");
|
||
return new MarginRateResult { Template = template, InitRate = 0m, MaintainRate = 0m };
|
||
}
|
||
|
||
if (template.RuleType != (int)MarginRuleTypeEnum.区间追保结构)
|
||
{
|
||
//其他规则不在本帮助类支持范围,显式返回 null
|
||
logger.Info($"【警告】预付金模板取数:模板{template.id} 规则{template.RuleType}不在取数支持范围(仅 无预付金/区间追保结构),返回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())
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:模板{template.id} 无生效明细行(ValueDate≤{valueDate:yyyy-MM-dd}),返回null由调用方兜底");
|
||
return null;
|
||
}
|
||
var latestValueDate = detailQuery.Max(x => x.ValueDate);
|
||
var details = detailQuery.Where(x => x.ValueDate == latestValueDate).ToList();
|
||
|
||
//4.资产类型先行(2026-08-27 顺序裁定:先品种后期限):按资产类型分档的模板先按标的品种缩小行集——
|
||
//品种行 → 通配行(None/All)→ 均无则不缩小(回落到与旧序一致的期限匹配,模板未配该品种的既有兜底不变)。
|
||
//期限档仅利率债允许配置(ConsMarginTerm),品种匹配必须在期限之前:期限精确匹配对任何标的恒有 term
|
||
//(GetApplicableMarginTerm 兜底 <5y),非利率债标的会被利率债期限档行截胡、本品种行(BondTerm 空)永远不参与
|
||
//(2026-08-27 交易2567 实证:信用债标的按 "<5y" 命中利率债行多收追保)
|
||
var candidates = details;
|
||
if (template.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.CustomInstrumentType
|
||
&& Enum.TryParse<UnderlyingTypeEnum>(underlyingInstrumentType, out var instrumentFlag))
|
||
{
|
||
var byInstrument = details.Where(x => (x.UnderlyingType & instrumentFlag) > 0).ToList();
|
||
if (!byInstrument.Any())
|
||
{
|
||
byInstrument = details.Where(x => x.UnderlyingType == UnderlyingTypeEnum.None || x.UnderlyingType == UnderlyingTypeEnum.All).ToList();
|
||
}
|
||
if (byInstrument.Any())
|
||
{
|
||
candidates = byInstrument;
|
||
}
|
||
}
|
||
|
||
//5.期限档匹配(利率债四档):精确档 → "全部"(BondTerm 为空)兜底
|
||
var term = UnderlyingHelper.GetApplicableMarginTerm(underlyingCode, valueDate);
|
||
var matched = candidates.Where(x => x.SpanConfig != null && x.SpanConfig.BondTerm == term).ToList();
|
||
if (!matched.Any())
|
||
{
|
||
matched = candidates.Where(x => x.SpanConfig == null || string.IsNullOrEmpty(x.SpanConfig.BondTerm)).ToList();
|
||
}
|
||
if (!matched.Any())
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})无匹配明细行,返回null由调用方兜底");
|
||
return null;
|
||
}
|
||
|
||
//6.ETF 子类行优先(子类区分度高于期限):标的有 EtfSubType(基金类)时优先取 EtfKind=子类 的行——
|
||
//期限档匹配未命中子类行时再单独尝试"子类 + BondTerm 空"(子类不分档通配);无子类行维持原 matched(基金通配兜底)
|
||
var underlyingCategory = GetUnderlyingCategory(underlyingCode, underlyingInstrumentType);
|
||
if (underlyingCategory != null)
|
||
{
|
||
matched = PreferCategoryRows(matched, details, underlyingCategory);
|
||
}
|
||
|
||
var detail = matched.First();
|
||
var initRate = ToDecimalRate(detail.MarginRatio1);
|
||
var maintainRate = ToDecimalRate(detail.MarginRatio2);
|
||
logger.Info($"预付金模板取数:模板{template.id}(生效日{latestValueDate:yyyy-MM-dd})标的{underlyingCode}(品种{underlyingInstrumentType},期限档{term})命中明细行{detail.id}:初始x={(initRate?.ToString("0.####") ?? "未配")},维持y={(maintainRate?.ToString("0.####") ?? "未配")}");
|
||
return new MarginRateResult
|
||
{
|
||
Template = template,
|
||
Detail = detail,
|
||
InitRate = initRate,
|
||
MaintainRate = maintainRate
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// ETF 子类行优先匹配(纯函数,单测覆盖):
|
||
/// termMatched 为期限档匹配结果(含精确档/BondTerm 空兜底),allDetails 为该模板当日全部明细行。
|
||
/// 优先取 termMatched 中 EtfKind=category 的行;没有则取"allDetails 中 EtfKind=category 且 BondTerm 空"的子类不分档行;
|
||
/// 都没有时原样返回 termMatched(基金通配行兜底)。
|
||
/// </summary>
|
||
public static List<margin_template_detail> PreferCategoryRows(List<margin_template_detail> termMatched,
|
||
List<margin_template_detail> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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 最新)。
|
||
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
|
||
/// </summary>
|
||
public static margin_template_v2 ResolveTieredTemplate(int? tradeId, int clientId, DateTime valueDate, YLContext db)
|
||
{
|
||
var tradeAssetId = GetTradeAssetId(tradeId, 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 && bound.IsApplicableToBook(tradeAssetId))
|
||
{
|
||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=交易绑定 → 模板{bound.id}");
|
||
return bound;
|
||
}
|
||
if (bound != null)
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:交易{tradeId}绑定的模板{bindingTemplateId}不适用簿记账户{tradeAssetId},按找到即停不向下回退");
|
||
return null;
|
||
}
|
||
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).ToList();
|
||
|
||
//同层多候选:簿记条件满足者优先,其次绑定生效日最新(2026-08-28 裁定)
|
||
var ret = PickByBookScopePreference(clientTemplate, tradeAssetId, out var clientByBook);
|
||
if (ret != null)
|
||
{
|
||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{ret.id}{(clientByBook ? "(簿记账户匹配优先)" : "")}");
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
//3.全局默认(同层多候选:簿记条件满足者优先,其次模板生效日最新——双全局默认并存时通配模板不再凭 id 最新恒胜,2026-08-28)
|
||
var globalDefault = PickByBookScopePreference(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)
|
||
.ToList(), tradeAssetId, out var globalByBook);
|
||
if (globalDefault != null)
|
||
{
|
||
logger.Info($"预付金模板取数:交易{tradeId}(客户{clientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "(簿记账户匹配优先)" : "")}");
|
||
}
|
||
return globalDefault;
|
||
}
|
||
|
||
/// <summary>
|
||
/// R1 三层级找到即停解析(批量版,BUG-02 修正:EOD 引擎与结算查询按交易列表解析,
|
||
/// 只配了客户默认/全局默认模板的交易与交易级绑定同权生效):
|
||
/// 语义与单笔 ResolveTieredTemplate 一致——
|
||
/// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退);
|
||
/// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效、适用结构含收益互换且适用当前簿记账户,先过滤有效再取绑定最新);
|
||
/// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,按每笔交易簿记账户取ValueDate/id 最新)。
|
||
/// 2/3 层多候选并存时按 PickByBookScopePreference 取舍:簿记条件满足者优先,其次生效日最新(2026-08-28)。
|
||
/// 返回 tradeId → 命中模板;未命中或因失效绑定停止的交易不在结果中。
|
||
/// </summary>
|
||
public static Dictionary<int, margin_template_v2> ResolveTieredTemplates(List<trade> trades, DateTime valueDate, YLContext db)
|
||
{
|
||
var result = new Dictionary<int, margin_template_v2>();
|
||
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<int, margin_template_v2>();
|
||
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 levelNames = new Dictionary<int, string>();
|
||
var clientTemplateCandidates = new List<ClientTemplateCandidate>();
|
||
if (clientIds.Any())
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
|
||
//先按 有效+适用收益互换 过滤再按绑定取最新(与单笔解析一致,同日互换/期权并存不误穿透)
|
||
clientTemplateCandidates = (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()
|
||
.Select(x => new ClientTemplateCandidate
|
||
{
|
||
ClientId = x.ClientId,
|
||
ClientLevel = x.ClientLevel,
|
||
Template = x.Template
|
||
})
|
||
.ToList();
|
||
}
|
||
|
||
//3.全局默认(候选模板一次查询,每笔交易按簿记账户取最新适用模板)
|
||
var globalDefaults = 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)
|
||
.ToList();
|
||
|
||
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) && bound.IsApplicableToBook(trade.AssetId))
|
||
{
|
||
result[trade.id] = bound;
|
||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=交易绑定 → 模板{bound.id}");
|
||
}
|
||
else if (boundTemplates.ContainsKey(templateId))
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}不适用簿记账户{trade.AssetId},按找到即停不向下回退");
|
||
}
|
||
else
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:交易{trade.id}绑定的模板{templateId}已失效或不存在,按找到即停不向下回退");
|
||
}
|
||
continue;
|
||
}
|
||
var levelName = levelNames.TryGetValue(trade.ClientId, out var clientLevelName) ? clientLevelName : null;
|
||
//同层多候选:先按客户/等级筛出候选(保持绑定生效日降序),再簿记条件满足者优先、其次生效日最新(与单笔解析一致)
|
||
var clientMatchCandidates = clientTemplateCandidates
|
||
.Where(x => x.ClientId == trade.ClientId || (x.ClientId == 0 && x.ClientLevel == levelName))
|
||
.Select(x => x.Template)
|
||
.ToList();
|
||
var clientTemplate = PickByBookScopePreference(clientMatchCandidates, trade.AssetId, out var clientByBook);
|
||
if (clientTemplate != null)
|
||
{
|
||
result[trade.id] = clientTemplate;
|
||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId},等级{levelName ?? "无"})命中层级=客户默认 → 模板{clientTemplate.id}{(clientByBook ? "(簿记账户匹配优先)" : "")}");
|
||
continue;
|
||
}
|
||
var globalDefault = PickByBookScopePreference(globalDefaults, trade.AssetId, out var globalByBook);
|
||
if (globalDefault != null)
|
||
{
|
||
result[trade.id] = globalDefault;
|
||
logger.Info($"预付金模板取数:交易{trade.id}(客户{trade.ClientId})命中层级=全局默认 → 模板{globalDefault.id}{(globalByBook ? "(簿记账户匹配优先)" : "")}");
|
||
}
|
||
else
|
||
{
|
||
logger.Info($"【警告】预付金模板取数:交易{trade.id}(客户{trade.ClientId})三层级(交易绑定/客户默认/全局默认)均未命中有效模板");
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同层级多候选并存时的取舍(2026-08-28 裁定,纯函数单测覆盖):
|
||
/// 入参候选序须已按生效日最新在前(客户层=绑定生效日,全局层=模板生效日,同日按 id 最新)。
|
||
/// 1.存在"簿记条件满足"的候选(BookIds 显式限定且经 IsApplicableToBook 命中当前交易簿记账户)→ 优先取之,
|
||
/// 多个命中时仍按传入顺序取生效日最新;
|
||
/// 2.否则取顺序第一个(生效日最新)。
|
||
/// 背景:双全局默认并存(如 无预付金限定簿记 + 区间追保通配)时,原"先按生效日/id 取最新再验簿记"
|
||
/// 使通配模板恒胜、限定簿记模板永不生效(交易2572/2573 实证)。
|
||
/// preferredByBookScope=true 表示本次由簿记匹配压过了顺序更前的通配候选(供日志辨识)。
|
||
/// </summary>
|
||
public static margin_template_v2 PickByBookScopePreference(List<margin_template_v2> candidatesByLatestFirst, int? tradeAssetId, out bool preferredByBookScope)
|
||
{
|
||
preferredByBookScope = false;
|
||
if (candidatesByLatestFirst == null || candidatesByLatestFirst.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var applicable = candidatesByLatestFirst.Where(x => x.IsApplicableToBook(tradeAssetId)).ToList();
|
||
var bookMatched = applicable.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.BookIds));
|
||
if (bookMatched == null)
|
||
{
|
||
return applicable.FirstOrDefault();
|
||
}
|
||
|
||
preferredByBookScope = applicable[0].id != bookMatched.id;
|
||
return bookMatched;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 标的细分分类判定钩子(ETF 子类,2026-08-24 实装):基金类标的返回标的维护页维护的
|
||
/// underlying_manager.EtfSubType(取值来自数据字典"ETF 子类":国债 ETF/政金债 ETF/…/科创债 ETF/可转债 ETF,
|
||
/// 基金类必填);非基金或未维护返回 null → 走标志位匹配/通配行兜底(债券/股票等行为与现状一致)。
|
||
/// 后续其他细分(中债指数 IndexKind 等)在本方法内扩展。
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 明细率值换算:页面上 vue-number-input 以 % 展示,数据库存的就是小数(如 0.05 表示 5%),直接取用
|
||
/// </summary>
|
||
private static decimal? ToDecimalRate(double? ratio)
|
||
{
|
||
return ratio.HasValue ? (decimal)ratio.Value : (decimal?)null;
|
||
}
|
||
}
|
||
}
|