#EQD-6948 国联民生-实现保证金规则(2)追保金额的产生与收盘计算 ETF 子类分档(转债ETF/科创债ETF)
This commit is contained in:
@@ -10,13 +10,33 @@ namespace YLErp.DBModels
|
||||
/// <summary>
|
||||
/// 允许配置期限档(SpanConfig.BondTerm)的标的资产类型标志位。
|
||||
/// 区间追保结构"按资产类型分类"时,仅这些类型允许设置非空期限档;
|
||||
/// 本期仅含 利率债(TBonds=1<<4=16),后续按资产分类扩展(转债ETF/科创债ETF 等)时在此补充。
|
||||
/// 本期仅含 利率债(TBonds=1<<4=16);ETF 子类分档能力见 TermTierEnabledEtfKinds。
|
||||
/// </summary>
|
||||
public static readonly UnderlyingTypeEnum[] TermTierEnabledUnderlyingTypes =
|
||||
{
|
||||
UnderlyingTypeEnum.TBonds
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 允许配置期限档的 ETF 子类白名单(来自数据字典"ETF 子类")。
|
||||
/// 选择这些子类的 ETF 区块按固定4档展开(同利率债);其余子类/不区分的 ETF 区块单套参数、不分档。
|
||||
/// 取值与 underlying_manager.EtfSubType 字典项一致,由标的维护页维护。
|
||||
/// 注意:分档 ETF 子类行的 UnderlyingType 仍为 基金(32768),不走 TermTierEnabledUnderlyingTypes 位掩码(§1.2 决策:不再加枚举位)。
|
||||
/// </summary>
|
||||
public static readonly List<string> TermTierEnabledEtfKinds = new List<string>
|
||||
{
|
||||
"可转债 ETF",
|
||||
"科创债 ETF"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 判断 EtfKind 是否允许分档(在 TermTierEnabledEtfKinds 白名单内)。
|
||||
/// </summary>
|
||||
public static bool IsTermTierEnabledEtfKind(string etfKind)
|
||||
{
|
||||
return !string.IsNullOrEmpty(etfKind) && TermTierEnabledEtfKinds.Contains(etfKind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 5年以下(同时也是兜底默认期限)
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.CalcModules
|
||||
{
|
||||
/// <summary>
|
||||
/// ETF 子类分档取数(MarginTemplateV2RateHelper.PreferCategoryRows 纯函数):
|
||||
/// 标的有 EtfSubType(基金类,分类判定钩子实装=读 underlying_manager.EtfSubType)时,
|
||||
/// 期限档匹配结果内优先取 EtfKind=子类 的行;未命中时回落"子类+BondTerm 空"行;
|
||||
/// 均无则维持原期限档匹配结果(基金通配行兜底)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginTemplateV2EtfKindMatchTest
|
||||
{
|
||||
private static margin_template_detail Row(int id, UnderlyingTypeEnum ut, string etfKind, string bondTerm)
|
||||
=> new()
|
||||
{
|
||||
id = id,
|
||||
UnderlyingType = ut,
|
||||
SpanConfigJson = null,
|
||||
SpanConfig = new SpanConfig { EtfKind = etfKind, BondTerm = bondTerm }
|
||||
};
|
||||
|
||||
private const string CB = "可转债 ETF";
|
||||
private const string STAR = "科创债 ETF";
|
||||
|
||||
[TestMethod]
|
||||
public void EK_001_期限档命中子类行_优先取子类()
|
||||
{
|
||||
//期限档匹配结果内既有 子类行又有 通配行:取子类行
|
||||
var termMatched = new List<margin_template_detail>
|
||||
{
|
||||
Row(1, UnderlyingTypeEnum.Fund, CB, "<5y"), //子类+精确档
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, "<5y"), //通配+精确档
|
||||
};
|
||||
var all = new List<margin_template_detail>(termMatched);
|
||||
var matched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, CB);
|
||||
Assert.AreEqual(1, matched.Count);
|
||||
Assert.AreEqual(1, matched[0].id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EK_002_期限档未命中子类行_回落子类空档行()
|
||||
{
|
||||
//期限档匹配(>30y)结果内无子类行 → 取"子类+BondTerm 空"的未分档行
|
||||
var termMatched = new List<margin_template_detail>
|
||||
{
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, null), //通配(BondTerm 空兜底命中)
|
||||
};
|
||||
var all = new List<margin_template_detail>
|
||||
{
|
||||
Row(1, UnderlyingTypeEnum.Fund, CB, "<5y"),
|
||||
Row(3, UnderlyingTypeEnum.Fund, CB, null), //子类未分档行(兜底候选)
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, null),
|
||||
};
|
||||
var matched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, CB);
|
||||
Assert.AreEqual(1, matched.Count);
|
||||
Assert.AreEqual(3, matched[0].id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EK_003_无子类行_维持原期限档匹配结果()
|
||||
{
|
||||
//模板无该子类任何行:维持原结果(通配兜底)
|
||||
var termMatched = new List<margin_template_detail>
|
||||
{
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, null),
|
||||
};
|
||||
var all = new List<margin_template_detail>(termMatched)
|
||||
{
|
||||
Row(4, UnderlyingTypeEnum.Fund, STAR, "5y-10y"), //别的子类不相关
|
||||
};
|
||||
var matched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, CB);
|
||||
Assert.AreEqual(1, matched.Count);
|
||||
Assert.AreEqual(2, matched[0].id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EK_004_多子类并存_各自命中不串()
|
||||
{
|
||||
//转债ETF 与 科创债ETF 各自成组:转债标的只命中转债行
|
||||
var termMatched = new List<margin_template_detail>
|
||||
{
|
||||
Row(1, UnderlyingTypeEnum.Fund, CB, "<5y"),
|
||||
Row(4, UnderlyingTypeEnum.Fund, STAR, "<5y"),
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, "<5y"),
|
||||
};
|
||||
var all = new List<margin_template_detail>(termMatched);
|
||||
var cbMatched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, CB);
|
||||
Assert.AreEqual(1, cbMatched.Count);
|
||||
Assert.AreEqual(1, cbMatched[0].id);
|
||||
var starMatched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, STAR);
|
||||
Assert.AreEqual(1, starMatched.Count);
|
||||
Assert.AreEqual(4, starMatched[0].id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EK_005_子类行但期限档不带BondTerm_不视为未分档兜底()
|
||||
{
|
||||
//子类+分档行(EtfKind=CB + BondTerm 非空)不能当"子类+空档"兜底候选
|
||||
var termMatched = new List<margin_template_detail>
|
||||
{
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, null),
|
||||
};
|
||||
var all = new List<margin_template_detail>
|
||||
{
|
||||
Row(1, UnderlyingTypeEnum.Fund, CB, "<5y"), //分档行不是兜底候选
|
||||
Row(2, UnderlyingTypeEnum.Fund, null, null),
|
||||
};
|
||||
var matched = MarginTemplateV2RateHelper.PreferCategoryRows(termMatched, all, CB);
|
||||
Assert.AreEqual(1, matched.Count);
|
||||
Assert.AreEqual(2, matched[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ namespace YLErp.Modules.MarginModule
|
||||
var latestValueDate = detailQuery.Max(x => x.ValueDate);
|
||||
var details = detailQuery.Where(x => x.ValueDate == latestValueDate).ToList();
|
||||
|
||||
//4.利率债期限档匹配:精确档 → "全部"(BondTerm 为空)兜底
|
||||
//4.利率债/分档ETF 期限档匹配:精确档 → "全部"(BondTerm 为空)兜底
|
||||
var term = UnderlyingHelper.GetApplicableMarginTerm(underlyingCode, valueDate);
|
||||
var matched = details.Where(x => x.SpanConfig != null && x.SpanConfig.BondTerm == term).ToList();
|
||||
if (!matched.Any())
|
||||
@@ -120,13 +120,12 @@ namespace YLErp.Modules.MarginModule
|
||||
return null;
|
||||
}
|
||||
|
||||
//5.标的资产类型匹配(仅当模板选了"按资产类型分类"):精确标志位 → 通配行(None/All)兜底
|
||||
//标的细分分类判定钩子:本期默认返回 null → 走下方原有标志位匹配/通配行兜底,行为与现状一致;
|
||||
//后续需求(转债ETF/科创债ETF/中债指数等细分)实现判定规则后,此处按 (BondTerm, EtfKind) 元组匹配明细行。
|
||||
//5.ETF 子类行优先(子类区分度高于期限):标的有 EtfSubType(基金类)时优先取 EtfKind=子类 的行——
|
||||
//期限档匹配未命中子类行时再单独尝试"子类 + BondTerm 空"(子类不分档通配);无子类行维持原 matched(基金通配兜底)
|
||||
var underlyingCategory = GetUnderlyingCategory(underlyingCode, underlyingInstrumentType);
|
||||
if (underlyingCategory != null)
|
||||
{
|
||||
//TODO: 后续需求实现:按 (SpanConfig.BondTerm, SpanConfig.EtfKind) 元组匹配明细行,替代/补充下方标志位匹配
|
||||
matched = PreferCategoryRows(matched, details, underlyingCategory);
|
||||
}
|
||||
|
||||
if (template.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.CustomInstrumentType
|
||||
@@ -157,6 +156,26 @@ namespace YLErp.Modules.MarginModule
|
||||
};
|
||||
}
|
||||
|
||||
/// <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 最新 ≤ 业务日)——找到即停,
|
||||
@@ -348,12 +367,19 @@ namespace YLErp.Modules.MarginModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标的细分分类判定钩子(转债ETF/科创债ETF/中债指数等),public 供单元测试回归默认行为。
|
||||
/// 本期默认返回 null → 走通配行兜底,行为与现状一致;后续需求按业务给的判定规则(代码段/标的维护字段)实现。
|
||||
/// 标的细分分类判定钩子(ETF 子类,2026-08-24 实装):基金类标的返回标的维护页维护的
|
||||
/// underlying_manager.EtfSubType(取值来自数据字典"ETF 子类":国债 ETF/政金债 ETF/…/科创债 ETF/可转债 ETF,
|
||||
/// 基金类必填);非基金或未维护返回 null → 走标志位匹配/通配行兜底(债券/股票等行为与现状一致)。
|
||||
/// 后续其他细分(中债指数 IndexKind 等)在本方法内扩展。
|
||||
/// </summary>
|
||||
public static string GetUnderlyingCategory(string underlyingCode, string underlyingInstrumentType)
|
||||
{
|
||||
return null;
|
||||
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>
|
||||
|
||||
@@ -299,7 +299,8 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
//区间追保结构 + 按资产类型分类时,期限档仅允许"允许分期限档的资产类型"(本期仅利率债)配置,防止信用债等非分档类型误配期限档;
|
||||
//区间追保结构 + 按资产类型分类时,期限档仅允许"允许分期限档的资产类型"(利率债)或"允许分档的 ETF 子类"
|
||||
//(可转债 ETF/科创债 ETF,行 UnderlyingType=基金 + EtfKind=子类,不加枚举位)配置,防止非分档类型误配期限档;
|
||||
//该校验按行生效,与明细行数无关(单行明细同样拦截);
|
||||
//按严格掩码判定:行标的类型位必须全部落在可分档类型内(混合标志位如 利率债|信用债 配期限档同样拦截,与取数侧整行期限档过滤语义一致);
|
||||
//UnderlyingType 为空的通配行允许配期限档(取数侧期限档过滤在前、通配匹配在后,语义自洽)
|
||||
@@ -307,13 +308,34 @@ namespace YLErp.Web.Controllers
|
||||
&& marginTemplate.UnderlyingSeperateType == (int)UnderlyingSeperateTypeEnum.CustomInstrumentType)
|
||||
{
|
||||
var enabledMask = ConsMarginTerm.TermTierEnabledUnderlyingTypes.Aggregate(UnderlyingTypeEnum.None, (a, t) => a | t);
|
||||
var tierEnabledEtfKinds = ConsMarginTerm.TermTierEnabledEtfKinds;
|
||||
foreach (var detail in marginTemplate.Details)
|
||||
{
|
||||
var etfKind = detail.SpanConfig?.EtfKind;
|
||||
//ETF 子类行:仅允许纯基金行(配置页子类选择器也只在纯基金区块出现)
|
||||
if (!string.IsNullOrEmpty(etfKind) && detail.UnderlyingType != UnderlyingTypeEnum.Fund)
|
||||
{
|
||||
throw new Exception("配置了 ETF 子类(" + etfKind + ")的参数组资产类型必须为 基金及基金专户");
|
||||
}
|
||||
var isTierEnabledEtfKind = tierEnabledEtfKinds.Contains(etfKind ?? "");
|
||||
if (!string.IsNullOrEmpty(detail.SpanConfig?.BondTerm)
|
||||
&& (detail.UnderlyingType & ~enabledMask) != UnderlyingTypeEnum.None)
|
||||
&& (detail.UnderlyingType & ~enabledMask) != UnderlyingTypeEnum.None
|
||||
&& !isTierEnabledEtfKind)
|
||||
{
|
||||
throw new Exception("标的类型不允许配置期限档:" + UnderlyingTypeUtil.GetDesc(detail.UnderlyingType));
|
||||
}
|
||||
//分档 ETF 子类必须配期限档(区块固定4档),其余子类行禁配期限档
|
||||
if (!string.IsNullOrEmpty(etfKind))
|
||||
{
|
||||
if (isTierEnabledEtfKind && string.IsNullOrEmpty(detail.SpanConfig?.BondTerm))
|
||||
{
|
||||
throw new Exception("ETF 子类 " + etfKind + " 为分期限档类型,参数行必须配置期限档");
|
||||
}
|
||||
if (!isTierEnabledEtfKind && !string.IsNullOrEmpty(detail.SpanConfig?.BondTerm))
|
||||
{
|
||||
throw new Exception("ETF 子类 " + etfKind + " 不分期限档,参数行不能配置期限档");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
isAdd = Model == null || Model.id == 0,
|
||||
marginTemplate = Model,
|
||||
detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1) },
|
||||
isGFSM = PS.Config.Company == CompanyEnum.广发商贸
|
||||
isGFSM = PS.Config.Company == CompanyEnum.广发商贸,
|
||||
etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetDictionaryItems("ETF 子类").Select(x => x.Name).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,11 +211,14 @@
|
||||
@*规则15区间追保录入(客户原型全量手工录入,资产类型区块式):纯利率债区块固定展开4个期限档、每档一套参数;其他资产类型区块一套参数、无期限档;x/y 字段保留兼容存量数据*@
|
||||
<div v-for="blk in ruleRangeBlocks" v-bind:key="'spanblk'+blk.key" class="border" style="padding:6px 10px; margin-top:10px;">
|
||||
<p style="margin:4px 0; font-weight:bold;">
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span><span v-if="blk.isTBond">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span>
|
||||
@*ETF 子类(仅纯基金区块展示):选 可转债ETF/科创债ETF 分期限档(展开4档),其他子类/不区分不分档*@
|
||||
<span v-if="blk.isFundBlock"> ETF 子类:<select v-model="blk.sections[0].detail.SpanConfig.EtfKind" v-on:change="onEtfKindChange(blk)"><option value="">不区分</option><option v-for="it in etfSubtypeItems" v-bind:value="it">{{it}}</option></select></span>
|
||||
<span v-if="blk.isTBond || blk.isTieredEtf">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
</p>
|
||||
<p style="margin:4px 0; color:#888;">存量 x/y 配置保留但不再展示,请按新区间结构录入。</p>
|
||||
<div v-for="sec in blk.sections" v-bind:key="sec.key">
|
||||
<p v-if="blk.isTBond" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<p v-if="blk.isTBond || blk.isTieredEtf" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<template v-if="sec.detail && sec.detail.SpanConfig">
|
||||
<p style="margin:4px 0;">
|
||||
预警线:<vue-number-input v-model="sec.detail.SpanConfig.WarnLine" v-bind:format="inputFormatPercent"></vue-number-input>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
{
|
||||
isAdd = Model == null || Model.id == 0,
|
||||
marginTemplate = Model,
|
||||
detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1), ComparedInitialMarginRate = 1 }
|
||||
detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1), ComparedInitialMarginRate = 1 },
|
||||
etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetDictionaryItems("ETF 子类").Select(x => x.Name).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,11 +212,14 @@
|
||||
@*规则15区间追保录入(客户原型全量手工录入,资产类型区块式):纯利率债区块固定展开4个期限档、每档一套参数;其他资产类型区块一套参数、无期限档;x/y 字段保留兼容存量数据*@
|
||||
<div v-for="blk in ruleRangeBlocks" v-bind:key="'spanblk'+blk.key" class="border" style="padding:6px 10px; margin-top:10px;">
|
||||
<p style="margin:4px 0; font-weight:bold;">
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span><span v-if="blk.isTBond">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span>
|
||||
@*ETF 子类(仅纯基金区块展示):选 可转债ETF/科创债ETF 分期限档(展开4档),其他子类/不区分不分档*@
|
||||
<span v-if="blk.isFundBlock"> ETF 子类:<select v-model="blk.sections[0].detail.SpanConfig.EtfKind" v-on:change="onEtfKindChange(blk)"><option value="">不区分</option><option v-for="it in etfSubtypeItems" v-bind:value="it">{{it}}</option></select></span>
|
||||
<span v-if="blk.isTBond || blk.isTieredEtf">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
</p>
|
||||
<p style="margin:4px 0; color:#888;">存量 x/y 配置保留但不再展示,请按新区间结构录入。</p>
|
||||
<div v-for="sec in blk.sections" v-bind:key="sec.key">
|
||||
<p v-if="blk.isTBond" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<p v-if="blk.isTBond || blk.isTieredEtf" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<template v-if="sec.detail && sec.detail.SpanConfig">
|
||||
<p style="margin:4px 0;">
|
||||
预警线:<vue-number-input v-model="sec.detail.SpanConfig.WarnLine" v-bind:format="inputFormatPercent"></vue-number-input>
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
{
|
||||
isAdd = Model == null || Model.id == 0,
|
||||
marginTemplate = Model,
|
||||
detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1) }
|
||||
detail = new margin_template_detail() { ValueDate = new DateTime(2020, 1, 1) },
|
||||
etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetDictionaryItems("ETF 子类").Select(x => x.Name).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,11 +188,14 @@
|
||||
@*规则15区间追保录入(客户原型全量手工录入,资产类型区块式):纯利率债区块固定展开4个期限档、每档一套参数;其他资产类型区块一套参数、无期限档;x/y 字段保留兼容存量数据*@
|
||||
<div v-for="blk in ruleRangeBlocks" v-bind:key="'spanblk'+blk.key" class="border" style="padding:6px 10px; margin-top:10px;">
|
||||
<p style="margin:4px 0; font-weight:bold;">
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span><span v-if="blk.isTBond">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
参数组{{blk.no}}:<span v-if="marginTemplate.UnderlyingSeperateType == @((int)UnderlyingSeperateTypeEnum.CustomInstrumentType)">资产类型:{{underlyingTypeNames(blk.ut)}}</span>
|
||||
@*ETF 子类(仅纯基金区块展示):选 可转债ETF/科创债ETF 分期限档(展开4档),其他子类/不区分不分档*@
|
||||
<span v-if="blk.isFundBlock"> ETF 子类:<select v-model="blk.sections[0].detail.SpanConfig.EtfKind" v-on:change="onEtfKindChange(blk)"><option value="">不区分</option><option v-for="it in etfSubtypeItems" v-bind:value="it">{{it}}</option></select></span>
|
||||
<span v-if="blk.isTBond || blk.isTieredEtf">(按期限分档:≤5y、(5y-10y]、(10y-30y]、>30y,每个档位独立设置)</span>
|
||||
</p>
|
||||
<p style="margin:4px 0; color:#888;">存量 x/y 配置保留但不再展示,请按新区间结构录入。</p>
|
||||
<div v-for="sec in blk.sections" v-bind:key="sec.key">
|
||||
<p v-if="blk.isTBond" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<p v-if="blk.isTBond || blk.isTieredEtf" style="margin:8px 0 2px; font-weight:bold;">期限档位:{{sec.termLabel}}</p>
|
||||
<template v-if="sec.detail && sec.detail.SpanConfig">
|
||||
<p style="margin:4px 0;">
|
||||
预警线:<vue-number-input v-model="sec.detail.SpanConfig.WarnLine" v-bind:format="inputFormatPercent"></vue-number-input>
|
||||
|
||||
@@ -32,6 +32,13 @@ const UnderlyingSeperateTypeEnum = {
|
||||
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16)
|
||||
const TermTierEnabledUnderlyingMask = 16;
|
||||
|
||||
//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
|
||||
const FundTypeMask = 32768;
|
||||
|
||||
//允许配置期限档的 ETF 子类(SpanConfig.EtfKind 值,取值=数据字典"ETF 子类"),
|
||||
//与后端 ConsMarginTerm.TermTierEnabledEtfKinds 保持一致;其余子类与"不区分"的基金行不分档
|
||||
const TermTierEnabledEtfKinds = ['可转债 ETF', '科创债 ETF'];
|
||||
|
||||
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
|
||||
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
|
||||
|
||||
@@ -51,7 +58,8 @@ const vue = new Vue({
|
||||
el: '#marginTemplateV2Form',
|
||||
data: {
|
||||
isAdd: page.isAdd,
|
||||
marginTemplate: page.marginTemplate
|
||||
marginTemplate: page.marginTemplate,
|
||||
etfSubtypeItems: page.etfSubtypeItems || []
|
||||
},
|
||||
created: function () {
|
||||
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
|
||||
@@ -98,8 +106,9 @@ const vue = new Vue({
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
|
||||
//其他区块单段(BondTerm 为空)。段的 detail 缺失时模板侧 v-if 兜底不渲染该段
|
||||
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)与分档 ETF 子类
|
||||
//(ut=基金 + EtfKind∈可转债/科创债 ETF)区块固定展开4个期限档段(每段一条 detail),其他区块单段(BondTerm 为空)。
|
||||
//段的 detail 缺失时模板侧 v-if 兜底不渲染该段
|
||||
ruleRangeBlocks() {
|
||||
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
|
||||
var blocks = [];
|
||||
@@ -109,9 +118,12 @@ const vue = new Vue({
|
||||
if (rows[i].rowspan <= 0) continue;
|
||||
var head = rows[i];
|
||||
var ut = head.detail.UnderlyingType || 0;
|
||||
var kind = (head.detail.SpanConfig && head.detail.SpanConfig.EtfKind) || '';
|
||||
var isTBond = ut === 16;
|
||||
var isTieredEtf = ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0;
|
||||
var isTiered = isTBond || isTieredEtf;
|
||||
var sections = [];
|
||||
if (isTBond) {
|
||||
if (isTiered) {
|
||||
for (var t = 0; t < SpanBondTerms.length; t++) {
|
||||
var found = null;
|
||||
for (var j = head.index; j < head.index + head.rowspan; j++) {
|
||||
@@ -123,7 +135,7 @@ const vue = new Vue({
|
||||
} else {
|
||||
sections.push({ key: 'single', termLabel: '', detail: head.detail });
|
||||
}
|
||||
blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, isTBond: isTBond, sections: sections });
|
||||
blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, etfKind: kind, isTBond: isTBond, isTieredEtf: isTieredEtf, isFundBlock: ut === FundTypeMask, sections: sections });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
@@ -382,7 +394,12 @@ const vue = new Vue({
|
||||
details.splice.apply(details, [index, oldCount].concat(merged));
|
||||
} else {
|
||||
headDetail.UnderlyingType = ut;
|
||||
if (headDetail.SpanConfig) headDetail.SpanConfig.BondTerm = '';
|
||||
if (headDetail.SpanConfig)
|
||||
{
|
||||
headDetail.SpanConfig.BondTerm = '';
|
||||
//离开基金类型时清 ETF 子类(服务端校验:配置子类的行必须纯基金)
|
||||
if (ut !== FundTypeMask) headDetail.SpanConfig.EtfKind = null;
|
||||
}
|
||||
if (oldCount > 1) details.splice(index + 1, oldCount - 1);
|
||||
}
|
||||
this.refreshChosen();
|
||||
@@ -395,16 +412,70 @@ const vue = new Vue({
|
||||
$('.chosen-select').trigger('chosen:updated');
|
||||
});
|
||||
},
|
||||
//新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档,并补齐区间录入结构
|
||||
newSpanDetail(ut, bondTerm) {
|
||||
//新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档/ETF子类,并补齐区间录入结构
|
||||
newSpanDetail(ut, bondTerm, etfKind) {
|
||||
var d = _.cloneDeep(page.detail);
|
||||
d.UnderlyingType = ut;
|
||||
this.ensureSpanDetail(d);
|
||||
d.SpanConfig.BondTerm = bondTerm || '';
|
||||
d.SpanConfig.EtfKind = etfKind || null;
|
||||
return d;
|
||||
},
|
||||
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为一个区块并按固定4档补齐(缺档补空行);
|
||||
//同档重复/异常档的利率债行、非利率债行均各自独立成块展示,不丢数据
|
||||
//ETF 子类选择器变更(仅纯基金区块渲染):选可转债/科创债 ETF → 区块展开固定4档(缺档补行、异常档行删除);
|
||||
//选其他子类/不区分 → 收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind)
|
||||
onEtfKindChange(blk) {
|
||||
var head = blk.sections[0].detail;
|
||||
var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
|
||||
var tiered = TermTierEnabledEtfKinds.indexOf(kind) >= 0;
|
||||
var details = this.marginTemplate.Details;
|
||||
var bk = head._bk;
|
||||
var rows = details.filter(function (d) { return d._bk === bk; });
|
||||
if (tiered) {
|
||||
var byTerm = {};
|
||||
rows.forEach(function (r) {
|
||||
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
|
||||
if (byTerm[bt] === undefined) byTerm[bt] = r;
|
||||
});
|
||||
var that = this;
|
||||
var merged = [];
|
||||
SpanBondTerms.forEach(function (t) {
|
||||
var r = byTerm[t[0]];
|
||||
if (!r) r = that.newSpanDetail(FundTypeMask, t[0], kind);
|
||||
r.UnderlyingType = FundTypeMask;
|
||||
r.SpanConfig.EtfKind = kind;
|
||||
r.SpanConfig.BondTerm = t[0];
|
||||
Vue.set(r, '_bk', bk);
|
||||
merged.push(r);
|
||||
var idx = details.indexOf(r);
|
||||
if (idx >= 0 && r !== head) details.splice(idx, 1);
|
||||
});
|
||||
//异常档/重复行全部移除后按档序回插到区块首行位置
|
||||
var headIdx = details.indexOf(head);
|
||||
if (headIdx < 0) headIdx = details.length;
|
||||
details.splice.apply(details, [headIdx + 1, 0].concat(merged.filter(function (r) { return r !== head; })));
|
||||
} else {
|
||||
rows.forEach(function (r) {
|
||||
if (r !== head) {
|
||||
var idx = details.indexOf(r);
|
||||
if (idx >= 0) details.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
head.UnderlyingType = FundTypeMask;
|
||||
head.SpanConfig.BondTerm = '';
|
||||
head.SpanConfig.EtfKind = kind || null;
|
||||
}
|
||||
this.refreshChosen();
|
||||
},
|
||||
//分档行判定与分档键:纯利率债 → 'tbond';基金 + 分档 ETF 子类 → 'etf:子类'(同一子类的连续行归一个区块)
|
||||
spanTierKey(d) {
|
||||
var ut = d.UnderlyingType || 0;
|
||||
var kind = (d.SpanConfig && d.SpanConfig.EtfKind) || '';
|
||||
if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
|
||||
if (ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0) return 'etf:' + kind;
|
||||
return null;
|
||||
},
|
||||
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行、基金+分档ETF子类行各自归为区块并按固定4档补齐(缺档补空行);
|
||||
//同档重复/异常档的分档行、非分档行(含基金+不分档子类、基金通配行)均各自独立成块展示,不丢数据
|
||||
rebuildSpanBlocks(details) {
|
||||
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
|
||||
var result = [];
|
||||
@@ -412,10 +483,10 @@ const vue = new Vue({
|
||||
var i = 0;
|
||||
while (i < details.length) {
|
||||
var d = details[i];
|
||||
var ut = d.UnderlyingType || 0;
|
||||
if (ut === TermTierEnabledUnderlyingMask) {
|
||||
var tk = this.spanTierKey(d);
|
||||
if (tk) {
|
||||
var rows = [];
|
||||
while (i < details.length && (details[i].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) {
|
||||
while (i < details.length && this.spanTierKey(details[i]) === tk) {
|
||||
rows.push(details[i]);
|
||||
i++;
|
||||
}
|
||||
@@ -426,10 +497,13 @@ const vue = new Vue({
|
||||
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
|
||||
});
|
||||
var bk = 'bk_' + (++spanBlockSeq);
|
||||
var utv = tk === 'tbond' ? TermTierEnabledUnderlyingMask : FundTypeMask;
|
||||
var kind = tk === 'tbond' ? '' : tk.substring(4);
|
||||
termValues.forEach(function (t) {
|
||||
var r = byTerm[t];
|
||||
if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
|
||||
r.UnderlyingType = TermTierEnabledUnderlyingMask;
|
||||
if (!r) r = that.newSpanDetail(utv, t, kind);
|
||||
r.UnderlyingType = utv;
|
||||
if (kind) r.SpanConfig.EtfKind = kind;
|
||||
r.SpanConfig.BondTerm = t;
|
||||
Vue.set(r, '_bk', bk);
|
||||
result.push(r);
|
||||
@@ -489,13 +563,14 @@ const vue = new Vue({
|
||||
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
|
||||
return tiers;
|
||||
},
|
||||
//规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine 缺键时补 null,
|
||||
//规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine/EtfKind 缺键时补 null,
|
||||
//LongSpans/ShortSpans 非数组或为空时初始化为4个空 tier,tier 缺键补齐(全程 Vue.set 保证响应式,v-model 绑定点必须预先存在)
|
||||
ensureSpanDetail(detail) {
|
||||
if (!detail.SpanConfig) Vue.set(detail, 'SpanConfig', { BondTerm: '' });
|
||||
var sc = detail.SpanConfig;
|
||||
if (sc.WarnLine === undefined) Vue.set(sc, 'WarnLine', null);
|
||||
if (sc.CloseLine === undefined) Vue.set(sc, 'CloseLine', null);
|
||||
if (sc.EtfKind === undefined) Vue.set(sc, 'EtfKind', null);
|
||||
var that = this;
|
||||
['LongSpans', 'ShortSpans'].forEach(function (key) {
|
||||
if (!Array.isArray(sc[key]) || sc[key].length === 0) Vue.set(sc, key, that.emptySpanTiers());
|
||||
|
||||
@@ -32,6 +32,13 @@ const UnderlyingSeperateTypeEnum = {
|
||||
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16)
|
||||
const TermTierEnabledUnderlyingMask = 16;
|
||||
|
||||
//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
|
||||
const FundTypeMask = 32768;
|
||||
|
||||
//允许配置期限档的 ETF 子类(SpanConfig.EtfKind 值,取值=数据字典"ETF 子类"),
|
||||
//与后端 ConsMarginTerm.TermTierEnabledEtfKinds 保持一致;其余子类与"不区分"的基金行不分档
|
||||
const TermTierEnabledEtfKinds = ['可转债 ETF', '科创债 ETF'];
|
||||
|
||||
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
|
||||
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
|
||||
|
||||
@@ -51,7 +58,8 @@ const vue = new Vue({
|
||||
el: '#marginTemplateV2Form',
|
||||
data: {
|
||||
isAdd: page.isAdd,
|
||||
marginTemplate: page.marginTemplate
|
||||
marginTemplate: page.marginTemplate,
|
||||
etfSubtypeItems: page.etfSubtypeItems || []
|
||||
},
|
||||
created: function () {
|
||||
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
|
||||
@@ -98,8 +106,9 @@ const vue = new Vue({
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
|
||||
//其他区块单段(BondTerm 为空)。段的 detail 缺失时模板侧 v-if 兜底不渲染该段
|
||||
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)与分档 ETF 子类
|
||||
//(ut=基金 + EtfKind∈可转债/科创债 ETF)区块固定展开4个期限档段(每段一条 detail),其他区块单段(BondTerm 为空)。
|
||||
//段的 detail 缺失时模板侧 v-if 兜底不渲染该段
|
||||
ruleRangeBlocks() {
|
||||
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
|
||||
var blocks = [];
|
||||
@@ -109,9 +118,12 @@ const vue = new Vue({
|
||||
if (rows[i].rowspan <= 0) continue;
|
||||
var head = rows[i];
|
||||
var ut = head.detail.UnderlyingType || 0;
|
||||
var kind = (head.detail.SpanConfig && head.detail.SpanConfig.EtfKind) || '';
|
||||
var isTBond = ut === 16;
|
||||
var isTieredEtf = ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0;
|
||||
var isTiered = isTBond || isTieredEtf;
|
||||
var sections = [];
|
||||
if (isTBond) {
|
||||
if (isTiered) {
|
||||
for (var t = 0; t < SpanBondTerms.length; t++) {
|
||||
var found = null;
|
||||
for (var j = head.index; j < head.index + head.rowspan; j++) {
|
||||
@@ -123,7 +135,7 @@ const vue = new Vue({
|
||||
} else {
|
||||
sections.push({ key: 'single', termLabel: '', detail: head.detail });
|
||||
}
|
||||
blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, isTBond: isTBond, sections: sections });
|
||||
blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, etfKind: kind, isTBond: isTBond, isTieredEtf: isTieredEtf, isFundBlock: ut === FundTypeMask, sections: sections });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
@@ -393,7 +405,12 @@ const vue = new Vue({
|
||||
details.splice.apply(details, [index, oldCount].concat(merged));
|
||||
} else {
|
||||
headDetail.UnderlyingType = ut;
|
||||
if (headDetail.SpanConfig) headDetail.SpanConfig.BondTerm = '';
|
||||
if (headDetail.SpanConfig)
|
||||
{
|
||||
headDetail.SpanConfig.BondTerm = '';
|
||||
//离开基金类型时清 ETF 子类(服务端校验:配置子类的行必须纯基金)
|
||||
if (ut !== FundTypeMask) headDetail.SpanConfig.EtfKind = null;
|
||||
}
|
||||
if (oldCount > 1) details.splice(index + 1, oldCount - 1);
|
||||
}
|
||||
this.refreshChosen();
|
||||
@@ -406,16 +423,70 @@ const vue = new Vue({
|
||||
$('.chosen-select').trigger('chosen:updated');
|
||||
});
|
||||
},
|
||||
//新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档,并补齐区间录入结构
|
||||
newSpanDetail(ut, bondTerm) {
|
||||
//新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档/ETF子类,并补齐区间录入结构
|
||||
newSpanDetail(ut, bondTerm, etfKind) {
|
||||
var d = _.cloneDeep(page.detail);
|
||||
d.UnderlyingType = ut;
|
||||
this.ensureSpanDetail(d);
|
||||
d.SpanConfig.BondTerm = bondTerm || '';
|
||||
d.SpanConfig.EtfKind = etfKind || null;
|
||||
return d;
|
||||
},
|
||||
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为一个区块并按固定4档补齐(缺档补空行);
|
||||
//同档重复/异常档的利率债行、非利率债行均各自独立成块展示,不丢数据
|
||||
//ETF 子类选择器变更(仅纯基金区块渲染):选可转债/科创债 ETF → 区块展开固定4档(缺档补行、异常档行删除);
|
||||
//选其他子类/不区分 → 收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind)
|
||||
onEtfKindChange(blk) {
|
||||
var head = blk.sections[0].detail;
|
||||
var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
|
||||
var tiered = TermTierEnabledEtfKinds.indexOf(kind) >= 0;
|
||||
var details = this.marginTemplate.Details;
|
||||
var bk = head._bk;
|
||||
var rows = details.filter(function (d) { return d._bk === bk; });
|
||||
if (tiered) {
|
||||
var byTerm = {};
|
||||
rows.forEach(function (r) {
|
||||
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
|
||||
if (byTerm[bt] === undefined) byTerm[bt] = r;
|
||||
});
|
||||
var that = this;
|
||||
var merged = [];
|
||||
SpanBondTerms.forEach(function (t) {
|
||||
var r = byTerm[t[0]];
|
||||
if (!r) r = that.newSpanDetail(FundTypeMask, t[0], kind);
|
||||
r.UnderlyingType = FundTypeMask;
|
||||
r.SpanConfig.EtfKind = kind;
|
||||
r.SpanConfig.BondTerm = t[0];
|
||||
Vue.set(r, '_bk', bk);
|
||||
merged.push(r);
|
||||
var idx = details.indexOf(r);
|
||||
if (idx >= 0 && r !== head) details.splice(idx, 1);
|
||||
});
|
||||
//异常档/重复行全部移除后按档序回插到区块首行位置
|
||||
var headIdx = details.indexOf(head);
|
||||
if (headIdx < 0) headIdx = details.length;
|
||||
details.splice.apply(details, [headIdx + 1, 0].concat(merged.filter(function (r) { return r !== head; })));
|
||||
} else {
|
||||
rows.forEach(function (r) {
|
||||
if (r !== head) {
|
||||
var idx = details.indexOf(r);
|
||||
if (idx >= 0) details.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
head.UnderlyingType = FundTypeMask;
|
||||
head.SpanConfig.BondTerm = '';
|
||||
head.SpanConfig.EtfKind = kind || null;
|
||||
}
|
||||
this.refreshChosen();
|
||||
},
|
||||
//分档行判定与分档键:纯利率债 → 'tbond';基金 + 分档 ETF 子类 → 'etf:子类'(同一子类的连续行归一个区块)
|
||||
spanTierKey(d) {
|
||||
var ut = d.UnderlyingType || 0;
|
||||
var kind = (d.SpanConfig && d.SpanConfig.EtfKind) || '';
|
||||
if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
|
||||
if (ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0) return 'etf:' + kind;
|
||||
return null;
|
||||
},
|
||||
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行、基金+分档ETF子类行各自归为区块并按固定4档补齐(缺档补空行);
|
||||
//同档重复/异常档的分档行、非分档行(含基金+不分档子类、基金通配行)均各自独立成块展示,不丢数据
|
||||
rebuildSpanBlocks(details) {
|
||||
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
|
||||
var result = [];
|
||||
@@ -423,10 +494,10 @@ const vue = new Vue({
|
||||
var i = 0;
|
||||
while (i < details.length) {
|
||||
var d = details[i];
|
||||
var ut = d.UnderlyingType || 0;
|
||||
if (ut === TermTierEnabledUnderlyingMask) {
|
||||
var tk = this.spanTierKey(d);
|
||||
if (tk) {
|
||||
var rows = [];
|
||||
while (i < details.length && (details[i].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) {
|
||||
while (i < details.length && this.spanTierKey(details[i]) === tk) {
|
||||
rows.push(details[i]);
|
||||
i++;
|
||||
}
|
||||
@@ -437,10 +508,13 @@ const vue = new Vue({
|
||||
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
|
||||
});
|
||||
var bk = 'bk_' + (++spanBlockSeq);
|
||||
var utv = tk === 'tbond' ? TermTierEnabledUnderlyingMask : FundTypeMask;
|
||||
var kind = tk === 'tbond' ? '' : tk.substring(4);
|
||||
termValues.forEach(function (t) {
|
||||
var r = byTerm[t];
|
||||
if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
|
||||
r.UnderlyingType = TermTierEnabledUnderlyingMask;
|
||||
if (!r) r = that.newSpanDetail(utv, t, kind);
|
||||
r.UnderlyingType = utv;
|
||||
if (kind) r.SpanConfig.EtfKind = kind;
|
||||
r.SpanConfig.BondTerm = t;
|
||||
Vue.set(r, '_bk', bk);
|
||||
result.push(r);
|
||||
@@ -500,13 +574,14 @@ const vue = new Vue({
|
||||
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
|
||||
return tiers;
|
||||
},
|
||||
//规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine 缺键时补 null,
|
||||
//规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine/EtfKind 缺键时补 null,
|
||||
//LongSpans/ShortSpans 非数组或为空时初始化为4个空 tier,tier 缺键补齐(全程 Vue.set 保证响应式,v-model 绑定点必须预先存在)
|
||||
ensureSpanDetail(detail) {
|
||||
if (!detail.SpanConfig) Vue.set(detail, 'SpanConfig', { BondTerm: '' });
|
||||
var sc = detail.SpanConfig;
|
||||
if (sc.WarnLine === undefined) Vue.set(sc, 'WarnLine', null);
|
||||
if (sc.CloseLine === undefined) Vue.set(sc, 'CloseLine', null);
|
||||
if (sc.EtfKind === undefined) Vue.set(sc, 'EtfKind', null);
|
||||
var that = this;
|
||||
['LongSpans', 'ShortSpans'].forEach(function (key) {
|
||||
if (!Array.isArray(sc[key]) || sc[key].length === 0) Vue.set(sc, key, that.emptySpanTiers());
|
||||
|
||||
Reference in New Issue
Block a user