From e8bb04d6bf73d14083ccd75b3a3c9ab5eb437f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Thu, 13 Aug 2026 13:19:06 +0800 Subject: [PATCH] =?UTF-8?q?#EQD-6947=20=E5=9B=BD=E8=81=94=E6=B0=91?= =?UTF-8?q?=E7=94=9F-=E4=BF=9D=E8=AF=81=E9=87=91=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E6=94=B9=E9=80=A0-=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88=E7=9B=AF=E5=B8=82=E8=A7=84=E5=88=99?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E6=94=AF=E6=8C=81=E5=90=8C=E4=B8=80=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E5=A4=9A=E9=83=A8=E9=97=A8=EF=BC=89=EF=BC=881?= =?UTF-8?q?=EF=BC=89=E4=BF=9D=E8=AF=81=E9=87=91=E8=A7=84=E5=88=99=E7=9A=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=20=E9=98=B6=E6=AE=B52=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20`BondTerm`=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=BA=92=E6=8D=A2=E4=BA=A4=E6=98=93=E6=A8=A1=E6=9D=BF=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DBModels/margin_template_detail.cs | 6 + .../TradeConfirmationGenerator.cs | 14 +- .../MarginCalculationBase.cs | 89 ++++++++++ .../MarginTemplateV2RateHelper.cs | 152 ++++++++++++++++++ .../SwapMarginTemplateConfigService.cs | 3 +- .../Modules/SwapModule/SwapTradeService.cs | 4 +- .../margin_template_v2Controller.cs | 44 +++-- .../margin_template_v2ClientEdit.cshtml | 26 ++- .../margin_template_v2DefaultEdit.cshtml | 26 ++- .../margin_template_v2Edit.cshtml | 20 +++ .../marginTemplateV2DefaultEdit.js | 2 +- .../marginTemplate/marginTemplateV2Edit.js | 2 +- 12 files changed, 364 insertions(+), 24 deletions(-) create mode 100644 YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs diff --git a/Framework/YLErp.Core/DBModels/margin_template_detail.cs b/Framework/YLErp.Core/DBModels/margin_template_detail.cs index 7520311a..1d6c7ad0 100644 --- a/Framework/YLErp.Core/DBModels/margin_template_detail.cs +++ b/Framework/YLErp.Core/DBModels/margin_template_detail.cs @@ -207,5 +207,11 @@ namespace YLErp.DBModels /// 发生追保时系数 /// public double CoefficientFactor { get; set; } = 1.05; + + /// + /// 利率债期限档(区间追保结构使用,取值为空=全部 或 ConsMarginTerm 四档:<5y / 5y-10y / 10y-30y / >30y)。 + /// 存于 SpanConfigJson 文本列,无需 DDL。 + /// + public string BondTerm { get; set; } } } diff --git a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs index a4047271..08ae29bf 100644 --- a/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs +++ b/Plugins/YLErp.Plugins.GuoLian/DocumentGenerator/TradeConfirmationGenerator.cs @@ -1,8 +1,9 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using YLErp.Core.Helpers; using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; +using YLErp.Modules.MarginModule; using YLErp.Modules.UnderlyingModule; using YLErp.Plugins.TradeDocGenerator; using YLErp.Plugins.TradeDocGenerator.Abstracts; @@ -253,8 +254,15 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator : swapRatePosition != null ? "" : "0.0000"; // 获取客户适用的保证金率 - var clientMarginRate = UnderlyingHelper.GetApplicableMarginRate( - client.id, swapPosition.UnderlyingCode, trade.TradeDate ?? DateTime.Now); + //保证金模板V2迁移:优先按交易绑定的预付金模板V2(区间追保结构)取 初始率x/维持率y; + //取不到(未绑定/模板无效/无匹配明细)时回退旧的互换预付金率维护(client_margin_config)链路 + var templateRate = MarginTemplateV2RateHelper.GetTradeMarginRate( + trade.id, swapPosition.UnderlyingCode, swapPosition.UnderlyingInstrumentType, trade.TradeDate ?? DateTime.Now); + + var clientMarginRate = templateRate != null + ? new client_margin_detail { init_rate = templateRate.InitRate ?? 0, maintain_rate = templateRate.MaintainRate ?? 0 } + : UnderlyingHelper.GetApplicableMarginRate( + client.id, swapPosition.UnderlyingCode, trade.TradeDate ?? DateTime.Now); if (clientMarginRate != null) { diff --git a/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs b/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs index 669a3013..f97045f3 100644 --- a/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs +++ b/YLErpDAL/BLL/MarginCalculation/MarginCalculationBase.cs @@ -6,6 +6,7 @@ using YLErp.Model.Enum; using YLErp.Modules; using YLErp.Modules.CalculationModule; using YLErp.Modules.DataProviderModule; +using YLErp.Modules.MarginModule; using YLErp.Modules.TradeModule; namespace YLErp.BLL.MarginCalculation @@ -365,9 +366,97 @@ namespace YLErp.BLL.MarginCalculation } //收益互换预付金计算 + //预付金模板V2迁移:绑定了模板的互换交易按模板规则计算——无预付金=0;区间追保结构=名义本金×x(初始)/名义本金×y(持仓), + //x/y 由 MarginTemplateV2RateHelper 按 期限档+资产类型 匹配明细取得。未绑定或绑定其他规则的互换交易维持现状(不产出 trade_span)。 private static List SwapTradeMarginCalculation(RunMarginCalculationReq req) { var result = new List(); + + var swapTradeIds = req.tradeList.Where(t => t.ParentTradeId == 0 && t.id > 0).Select(t => t.id).ToList(); + if (swapTradeIds.Count == 0) + { + return result; + } + + using (var db = new YLContext()) + { + var bindings = db.trade_margin_template.AsNoTracking() + .Where(x => swapTradeIds.Contains(x.TradeId) && x.ValueDate <= req.settleDate) + .ToList(); + if (bindings.Count == 0) + { + return result; + } + + var templateIds = bindings.Select(x => x.MarginTemplateId).Distinct().ToList(); + var templates = db.margin_template_v2.AsNoTracking() + .Where(x => templateIds.Contains(x.id) && x.IsValid) + .ToList(); + + foreach (var trade in req.tradeList) + { + //剔除多空组合子交易 + if (trade.ParentTradeId > 0 || trade.id <= 0) + { + continue; + } + + var binding = bindings.Where(x => x.TradeId == trade.id).OrderByDescending(x => x.ValueDate).FirstOrDefault(); + var template = binding == null ? null : templates.FirstOrDefault(x => x.id == binding.MarginTemplateId); + if (template == null) + { + continue; + } + + double rate; + if (template.RuleType == (int)MarginRuleTypeEnum.无预付金) + { + rate = 0; + } + else if (template.RuleType == (int)MarginRuleTypeEnum.区间追保结构) + { + var rateResult = MarginTemplateV2RateHelper.GetTradeMarginRate(trade.id, trade.UnderlyingCode, trade.UnderlyingInstrumentType, req.settleDate, db); + if (rateResult == null) + { + continue; + } + //初始预付金=名义本金×x;持仓预付金=名义本金×y + rate = (double)(req.CalcMarginType == CalcMarginTypeEnum.InitialMargin + ? rateResult.InitRate ?? 0m + : rateResult.MaintainRate ?? 0m); + } + else + { + //其他规则不在默认引擎支持范围,显式跳过 + continue; + } + + var margin = rate * trade.StockEqvNotional; + result.Add(new trade_span() + { + TradeId = trade.id, + OptDate = DateTime.Now, + OptId = req.userId, + OptName = req.userName, + ClientId = trade.ClientId, + UnderlyingId = trade.UnderlyingId, + UnderlyingCode = trade.UnderlyingCode, + ValueDate = req.settleDate, + Spv1 = margin, + Spv2 = margin, + Spv3 = margin, + Spv4 = margin, + Spv5 = margin, + Spv6 = margin, + Spv7 = margin, + Spv8 = margin, + Spv = margin, + WorstCastClientPayable = margin, + Margin = 0 + }); + } + } + return result; } } diff --git a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs new file mode 100644 index 00000000..95e261d7 --- /dev/null +++ b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using YLErp.BLL; +using YLErp.DBModels; +using YLErp.Enums; +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 + { + /// + /// 取数结果。率已统一换算为小数(如 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,供批量场景复用连接)。 + /// + public static MarginRateResult GetTradeMarginRate(int tradeId, string underlyingCode, string underlyingInstrumentType, DateTime valueDate, YLContext db) + { + //1.交易绑定(ValueDate 最新) + var binding = db.trade_margin_template.AsNoTracking() + .Where(x => x.TradeId == tradeId && x.ValueDate <= valueDate) + .OrderByDescending(x => x.ValueDate) + .FirstOrDefault(); + if (binding == null) + { + return null; + } + + var template = db.margin_template_v2.AsNoTracking().FirstOrDefault(x => x.id == binding.MarginTemplateId); + if (template == null || !template.IsValid) + { + return null; + } + + //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)兜底 + 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) + }; + } + + /// + /// 模板页录入的百分数(如 10 表示 10%)换算为小数(0.1) + /// + private static decimal? ToDecimalRate(double? ratio) + { + return ratio.HasValue ? (decimal)(ratio.Value / 100d) : (decimal?)null; + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs b/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs index 27c10b4a..3f95bba5 100644 --- a/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapMarginTemplateConfigService.cs @@ -12,9 +12,10 @@ namespace YLErp.Modules.SwapModule public static SwapMarginTemplateConfig GetConfig() { //保证金模板V2迁移:交易页"保证金模板"下拉的选项改为"预付金模板V2"页面维护的模板(自定义+全局默认,存模板名称),不再使用数据字典 + //互换交易只显示"适用结构"包含 收益互换 的模板 using var db = new YLErp.BLL.YLContext(); var items = db.margin_template_v2 - .Where(item => !item.IsForClient && item.IsValid) + .Where(item => !item.IsForClient && item.IsValid && item.TradeTypes.Contains("收益互换")) .OrderBy(item => item.IsDefault).ThenBy(item => item.id) .Select(item => new SelectItem { Text = item.Name, Value = item.Name }) .ToArray(); diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 2fc1fc87..ccc58da6 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -134,9 +134,9 @@ namespace YLErp.Modules.SwapModule DbContext.trade_margin_template.RemoveRange(olds); if (!string.IsNullOrWhiteSpace(dbTrade.MarginTemplateName)) { - //匹配自定义+全局默认模板(重名时优先自定义) + //匹配自定义+全局默认模板(重名时优先自定义),模板"适用结构"需包含 收益互换 var template = DbContext.margin_template_v2 - .Where(x => x.Name == dbTrade.MarginTemplateName && !x.IsForClient && x.IsValid) + .Where(x => x.Name == dbTrade.MarginTemplateName && !x.IsForClient && x.IsValid && x.TradeTypes.Contains("收益互换")) .OrderBy(x => x.IsDefault) .FirstOrDefault(); if (template != null) diff --git a/YLErpWeb/Controllers/margin_template_v2Controller.cs b/YLErpWeb/Controllers/margin_template_v2Controller.cs index 60edfece..b540be3c 100644 --- a/YLErpWeb/Controllers/margin_template_v2Controller.cs +++ b/YLErpWeb/Controllers/margin_template_v2Controller.cs @@ -301,19 +301,43 @@ namespace YLErp.Web.Controllers if (marginTemplate.Details.Count() > 1) { - var underlyingType = UnderlyingTypeEnum.None; - foreach (var detail in marginTemplate.Details) + if (marginTemplate.RuleType == (int)MarginRuleTypeEnum.区间追保结构) { - if (detail.UnderlyingType == UnderlyingTypeEnum.None) + //区间追保结构:按 利率债期限档(SpanConfig.BondTerm)分组校验,同一期限档内标的类型不允许重复,不同期限档允许相同标的类型 + foreach (var termGroup in marginTemplate.Details.GroupBy(x => x.SpanConfig?.BondTerm ?? "")) { - throw new Exception("标的类型不能为空"); - } - if ((detail.UnderlyingType & underlyingType) > 0) - { - throw new Exception("存在重复的标的类型:" + UnderlyingTypeUtil.GetDesc(detail.UnderlyingType & underlyingType)); - } + var termUnderlyingType = UnderlyingTypeEnum.None; + foreach (var detail in termGroup) + { + if ((detail.UnderlyingType & termUnderlyingType) > 0) + { + if (marginTemplate.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.None) + { + throw new Exception("同一利率债期限档下存在重复的参数组"); + } + throw new Exception("存在重复的标的类型:" + UnderlyingTypeUtil.GetDesc(detail.UnderlyingType & termUnderlyingType)); + } - underlyingType |= detail.UnderlyingType; + termUnderlyingType |= detail.UnderlyingType; + } + } + } + else + { + var underlyingType = UnderlyingTypeEnum.None; + foreach (var detail in marginTemplate.Details) + { + if (detail.UnderlyingType == UnderlyingTypeEnum.None) + { + throw new Exception("标的类型不能为空"); + } + if ((detail.UnderlyingType & underlyingType) > 0) + { + throw new Exception("存在重复的标的类型:" + UnderlyingTypeUtil.GetDesc(detail.UnderlyingType & underlyingType)); + } + + underlyingType |= detail.UnderlyingType; + } } } diff --git a/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml b/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml index 42f6428b..25de064a 100644 --- a/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml +++ b/YLErpWeb/Views/margin_template_v2/margin_template_v2ClientEdit.cshtml @@ -138,7 +138,7 @@ @@ -203,6 +203,16 @@
- +
- +
@@ -467,6 +477,16 @@
- +
- +
@@ -451,6 +461,16 @@