Files
zszq-trs/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs
T

368 lines
20 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;
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,供交易确认书、预付金计算等消费点统一使用。
/// 匹配维度:生效日期(ValueDate 最新)→ 利率债期限档(SpanConfig.BondTerm,四档,空=全部兜底)→ 标的资产类型(UnderlyingType 标志位)。
/// </summary>
public static class MarginTemplateV2RateHelper
{
private static readonly IYcLogger logger = LogFactory.GetLogger("预付金模板取数");
/// <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; }
}
/// <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.无预付金)
{
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.利率债期限档匹配:精确档 → "全部"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.标的资产类型匹配(仅当模板选了"按资产类型分类"):精确标志位 → 通配行(None/All)兜底
//标的细分分类判定钩子:本期默认返回 null → 走下方原有标志位匹配/通配行兜底,行为与现状一致;
//后续需求(转债ETF/科创债ETF/中债指数等细分)实现判定规则后,此处按 (BondTerm, EtfKind) 元组匹配明细行。
var underlyingCategory = GetUnderlyingCategory(underlyingCode, underlyingInstrumentType);
if (underlyingCategory != null)
{
//TODO: 后续需求实现:按 (SpanConfig.BondTerm, SpanConfig.EtfKind) 元组匹配明细行,替代/补充下方标志位匹配
}
if (template.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.CustomInstrumentType
&& Enum.TryParse<UnderlyingTypeEnum>(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)
};
}
/// <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_v2IsDefault && !IsForClient && IsValid && 适用收益互换,ValueDate 最新)。
/// </summary>
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();
}
/// <summary>
/// R1 三层级找到即停解析(批量版,BUG-02 修正:EOD 引擎与结算查询按交易列表解析,
/// 只配了客户默认/全局默认模板的交易与交易级绑定同权生效):
/// 语义与单笔 ResolveTieredTemplate 一致——
/// 1.交易绑定(ValueDate 最新 ≤ 业务日,同日多条按 id 最新兜底)找到即停,绑定指向失效模板同样停止(不回退);
/// 2.客户默认(client_margin_template 按客户或按客户等级,模板须有效且适用结构含收益互换,先过滤有效再取绑定最新);
/// 3.全局默认(IsDefault&&!IsForClient&&IsValid&&适用收益互换,ValueDate/id 最新)。
/// 返回 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 clientTemplateByClient = new Dictionary<int, margin_template_v2>();
if (clientIds.Any())
{
var levelNames = new Dictionary<int, string>();
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;
}
/// <summary>
/// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等),public 供单元测试回归默认行为。
/// 本期默认返回 null → 走通配行兜底,行为与现状一致;后续需求按业务给的判定规则(代码段/标的维护字段)实现。
/// </summary>
public static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType)
{
return null;
}
/// <summary>
/// 明细率值换算:页面上 vue-number-input 以 % 展示,数据库存的就是小数(如 0.05 表示 5%),直接取用
/// </summary>
private static decimal? ToDecimalRate(double? ratio)
{
return ratio.HasValue ? (decimal)ratio.Value : (decimal?)null;
}
}
}