diff --git a/UnitTestProject/Modules/CalcModules/MarginTemplateV2InstrumentFirstMatchTest.cs b/UnitTestProject/Modules/CalcModules/MarginTemplateV2InstrumentFirstMatchTest.cs
new file mode 100644
index 00000000..6cf60d56
--- /dev/null
+++ b/UnitTestProject/Modules/CalcModules/MarginTemplateV2InstrumentFirstMatchTest.cs
@@ -0,0 +1,144 @@
+using YLErp.BLL;
+using YLErp.DBModels;
+using YLErp.Enums;
+using YLErp.Modules.MarginModule;
+
+namespace YLErp.Modules.CalcModules
+{
+ ///
+ /// GetRateByTemplate 资产类型先行匹配回归(连 dev 库,2026-08-27 顺序裁定:先品种后期限):
+ /// 期限档仅利率债允许配置,品种匹配在期限之前——防止非利率债标的被利率债期限档行截胡
+ /// (交易2567 实证:信用债标的按 "<5y" 精确匹配到利率债行,信用债行 BondTerm 空永远不参与)。
+ /// 标的代码用库里不存在的代码(GetApplicableMarginTerm 无标的兜底返回 <5y),不依赖真实标的行情数据。
+ /// 测试数据全部带 "ZZZ-品种先行测试-" 名称前缀,TestInitialize/TestCleanup 双向清理。
+ ///
+ [TestClass]
+ public class MarginTemplateV2InstrumentFirstMatchTest
+ {
+ private const string Marker = "ZZZ-品种先行测试-";
+ private DateTime EffectiveDate = new DateTime(2000, 1, 1);
+
+ private YLContext db;
+
+ [TestInitialize]
+ public void Init()
+ {
+ db = new YLContext();
+ Cleanup();
+ }
+
+ [TestCleanup]
+ public void CleanupFixture()
+ {
+ Cleanup();
+ db.Dispose();
+ }
+
+ private void Cleanup()
+ {
+ var templateIds = db.margin_template_v2.Where(x => x.Name.StartsWith(Marker)).Select(x => x.id).ToList();
+ if (templateIds.Count > 0)
+ {
+ db.margin_template_detail.RemoveRange(db.margin_template_detail.Where(x => templateIds.Contains(x.MarginTemplateId)));
+ db.margin_template_v2.RemoveRange(db.margin_template_v2.Where(x => templateIds.Contains(x.id)));
+ db.SaveChanges();
+ }
+ }
+
+ private margin_template_v2 AddTieredTemplate()
+ {
+ var t = new margin_template_v2
+ {
+ Name = Marker + "分档",
+ IsDefault = false,
+ IsForClient = false,
+ IsValid = true,
+ TradeTypes = "收益互换",
+ RuleType = (int)MarginRuleTypeEnum.区间追保结构,
+ UnderlyingSeperateType = (int)UnderlyingSeperateTypeEnum.CustomInstrumentType,
+ ValueDate = EffectiveDate
+ };
+ db.margin_template_v2.Add(t);
+ db.SaveChanges();
+ return t;
+ }
+
+ private void AddDetail(int templateId, UnderlyingTypeEnum underlyingType, string bondTermJson, double initRate, double maintainRate)
+ {
+ db.margin_template_detail.Add(new margin_template_detail
+ {
+ MarginTemplateId = templateId,
+ ValueDate = EffectiveDate,
+ UnderlyingType = underlyingType,
+ SpanConfigJson = bondTermJson,
+ MarginRatio1 = initRate,
+ MarginRatio2 = maintainRate
+ });
+ }
+
+ ///
+ /// 信用债标的不被利率债期限档行截胡:term 恒为 "<5y"(标的不存在兜底),
+ /// 旧序会精确命中利率债 <5y 行;新序品种先行应命中信用债行(BondTerm 空)。
+ ///
+ [TestMethod]
+ public void TI_001_信用债标的_命中信用债行_不被利率债期限档截胡()
+ {
+ var tpl = AddTieredTemplate();
+ AddDetail(tpl.id, UnderlyingTypeEnum.TBonds, "{\"BondTerm\":\"<5y\"}", 0.11, 0.12);
+ AddDetail(tpl.id, UnderlyingTypeEnum.CreditBonds, null, 0.05, 0.06);
+ db.SaveChanges();
+
+ var rate = MarginTemplateV2RateHelper.GetRateByTemplate(tpl, "ZZZ-NOT-EXIST-CD.IB", "CreditBonds", DateTime.Today, db);
+ Assert.IsNotNull(rate, "品种先行后信用债行(BondTerm 空)应经期限兜底命中");
+ Assert.AreEqual(0.05m, rate.InitRate.Value, "应取信用债行的初始预付金率,而非利率债 <5y 行的 0.11");
+ Assert.AreEqual(0.06m, rate.MaintainRate.Value, "应取信用债行的维持预付金率,而非利率债 <5y 行的 0.12");
+ }
+
+ ///
+ /// 利率债标的行为不变:品种命中利率债行后,期限精确档 "<5y" 命中对应期限行(压过 5y-10y 行)。
+ ///
+ [TestMethod]
+ public void TI_002_利率债标的_品种内期限精确档仍生效()
+ {
+ var tpl = AddTieredTemplate();
+ AddDetail(tpl.id, UnderlyingTypeEnum.TBonds, "{\"BondTerm\":\"<5y\"}", 0.11, 0.12);
+ AddDetail(tpl.id, UnderlyingTypeEnum.TBonds, "{\"BondTerm\":\"5y-10y\"}", 0.13, 0.14);
+ db.SaveChanges();
+
+ var rate = MarginTemplateV2RateHelper.GetRateByTemplate(tpl, "ZZZ-NOT-EXIST-TB.IB", "TBonds", DateTime.Today, db);
+ Assert.IsNotNull(rate);
+ Assert.AreEqual(0.11m, rate.InitRate.Value, "期限兜底 <5y 时应精确命中 <5y 档行");
+ Assert.AreEqual(0.12m, rate.MaintainRate.Value);
+ }
+
+ ///
+ /// 模板未配标的品种时的既有兜底不变:品种行与通配行均无 → 不缩小行集,回落期限匹配(与旧序一致)。
+ ///
+ [TestMethod]
+ public void TI_003_模板未配品种_回落期限匹配_行为不变()
+ {
+ var tpl = AddTieredTemplate();
+ AddDetail(tpl.id, UnderlyingTypeEnum.TBonds, "{\"BondTerm\":\"<5y\"}", 0.11, 0.12);
+ db.SaveChanges();
+
+ var rate = MarginTemplateV2RateHelper.GetRateByTemplate(tpl, "ZZZ-NOT-EXIST-CF.IB", "CommodityFutures", DateTime.Today, db);
+ Assert.IsNotNull(rate, "品种落空应回落到期限匹配(旧行为兜底),不应返回 null");
+ Assert.AreEqual(0.11m, rate.InitRate.Value);
+ }
+
+ ///
+ /// 品种行与期限行均无法匹配时返回 null:非利率债标的不再"借用"利率债期限档行,
+ /// 由调用方按无预付金要求兜底(引擎不产出 trade_span)。
+ ///
+ [TestMethod]
+ public void TI_004_品种与期限均无匹配行_返回null()
+ {
+ var tpl = AddTieredTemplate();
+ AddDetail(tpl.id, UnderlyingTypeEnum.TBonds, "{\"BondTerm\":\"5y-10y\"}", 0.13, 0.14);
+ db.SaveChanges();
+
+ var rate = MarginTemplateV2RateHelper.GetRateByTemplate(tpl, "ZZZ-NOT-EXIST-CD.IB", "CreditBonds", DateTime.Today, db);
+ Assert.IsNull(rate, "信用债标的不应命中利率债 5y-10y 期限行");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs
index 99e47346..25999088 100644
--- a/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs
+++ b/YLErpDAL/Modules/MarginModule/MarginTemplateV2RateHelper.cs
@@ -129,19 +129,39 @@ namespace YLErp.Modules.MarginModule
var latestValueDate = detailQuery.Max(x => x.ValueDate);
var details = detailQuery.Where(x => x.ValueDate == latestValueDate).ToList();
- //4.利率债/分档ETF 期限档匹配:精确档 → "全部"(BondTerm 为空)兜底
+ //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(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 = details.Where(x => x.SpanConfig != null && x.SpanConfig.BondTerm == term).ToList();
+ var matched = candidates.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();
+ matched = candidates.Where(x => x.SpanConfig == null || string.IsNullOrEmpty(x.SpanConfig.BondTerm)).ToList();
}
if (!matched.Any())
{
return null;
}
- //5.ETF 子类行优先(子类区分度高于期限):标的有 EtfSubType(基金类)时优先取 EtfKind=子类 的行——
+ //6.ETF 子类行优先(子类区分度高于期限):标的有 EtfSubType(基金类)时优先取 EtfKind=子类 的行——
//期限档匹配未命中子类行时再单独尝试"子类 + BondTerm 空"(子类不分档通配);无子类行维持原 matched(基金通配兜底)
var underlyingCategory = GetUnderlyingCategory(underlyingCode, underlyingInstrumentType);
if (underlyingCategory != null)
@@ -149,24 +169,6 @@ namespace YLErp.Modules.MarginModule
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
{