368 lines
20 KiB
C#
368 lines
20 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,供交易确认书、预付金计算等消费点统一使用。
|
||
/// 匹配维度:生效日期(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_v2(IsDefault && !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;
|
||
}
|
||
}
|
||
}
|