636 lines
36 KiB
JavaScript
636 lines
36 KiB
JavaScript
//定价格式化(来自配置)
|
||
const inputFormatPercent = Object.freeze({ precision: 2, append: '%' });
|
||
const inputFormatPercentNegative = Object.freeze({ precision: 2, append: '%', negative: true });
|
||
const inputFormatNegative = Object.freeze({ precision: 2, negative: true });
|
||
|
||
function normalizeBookIds(values) {
|
||
var ids = Array.isArray(values) ? values : (values ? [values] : []);
|
||
var normalized = [];
|
||
ids.forEach(function (id) {
|
||
id = (id || '').toString().trim();
|
||
if (id && normalized.indexOf(id) < 0) {
|
||
normalized.push(id);
|
||
}
|
||
});
|
||
return normalized.length ? normalized.join(',') : null;
|
||
}
|
||
|
||
const MarginRuleTypeEnum = {
|
||
无预付金: "0",
|
||
按固定利率: "1",
|
||
按浮动盈亏: "2",
|
||
类香草预付金规则: "3",
|
||
自动赎回买入预付金规则: "4",
|
||
触发条件预付金规则1: "5",
|
||
触发条件预付金规则2: "6",
|
||
保底预付金规则: "7",
|
||
含赔付预付金计算规则: "8",
|
||
触发条件预付金规则3: "9",
|
||
交易所基本规则: "10",
|
||
标准SPAN: "11",
|
||
买入自动赎回规则1: "12",
|
||
卖出自动赎回规则1: "13",
|
||
区间追保结构: "15",
|
||
默认预付金规则: "99"
|
||
};
|
||
|
||
const UnderlyingSeperateTypeEnum = {
|
||
None: "0",//不区分
|
||
CustomStock: "1",//按自定义权益分类
|
||
CustomInstrumentType: "2"//按资产类型分类
|
||
};
|
||
|
||
//允许配置期限档(SpanConfig.BondTerm)的标的资产类型标志位掩码,
|
||
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16)
|
||
const TermTierEnabledUnderlyingMask = 16;
|
||
|
||
//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
|
||
const FundTypeMask = 32768;
|
||
|
||
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
|
||
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
|
||
|
||
//区块键自增序号(仅前端分组用,不持久化)
|
||
var spanBlockSeq = 0;
|
||
|
||
//标的资产类型标志位 -> 名称(与明细表格 chosen 选项一致,用于规则15录入区块标题)
|
||
const UnderlyingTypeNames = [
|
||
[16, "利率债"], [32, "信用债"], [64, "其它债券"], [128, "股票"], [256, "股指"], [512, "股指期货"],
|
||
[1024, "商品期货"], [2048, "商品现货"], [4096, "新三板挂牌股票"], [8192, "香港股票"], [16384, "香港股指"],
|
||
[32768, "基金及基金专户"], [65536, "黄金期货"], [131072, "国债期货"], [262144, "其他期货"], [524288, "黄金现货"],
|
||
[1048576, "其他现货"], [2097152, "境外期货"], [4194304, "境外现货"], [8388608, "境外股票"], [16777216, "境外股指"],
|
||
[33554432, "汇率"], [67108864, "Shibor"], [134217728, "银行间回购定盘"], [268435456, "利率收益率"], [536870912, "债券指数"]
|
||
];
|
||
|
||
const vue = new Vue({
|
||
el: '#marginTemplateV2Form',
|
||
data: {
|
||
isAdd: page.isAdd,
|
||
marginTemplate: page.marginTemplate,
|
||
etfSubtypeItems: page.etfSubtypeItems || []
|
||
},
|
||
created: function () {
|
||
//留档进入页面时的生效状态:v-model 改写的就是 page.marginTemplate 同一对象(data 浅拷贝的是引用),
|
||
//原始值须在用户交互前记下,供保存时判断"生效中规则被改为否"弹失效确认;
|
||
//select 的 v-model 取值是字符串,统一 String 化比较
|
||
this.originalIsValid = String(this.marginTemplate.IsValid);
|
||
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
|
||
//编辑过程中不再重排,避免 chosen/选择器失序
|
||
if (this.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构 && this.marginTemplate.Details) {
|
||
var termOrder = ['', '<5y', '5y-10y', '10y-30y', '>30y'];
|
||
var that = this;
|
||
this.marginTemplate.Details.forEach(function (d) {
|
||
//兜底:历史数据可能没有 SpanConfig / 区间录入结构,补齐避免录入区块不渲染
|
||
that.ensureSpanDetail(d);
|
||
});
|
||
this.marginTemplate.Details.sort(function (a, b) {
|
||
var at = a.UnderlyingType || 0, bt = b.UnderlyingType || 0;
|
||
if (at !== bt) return at - bt;
|
||
var ai = termOrder.indexOf((a.SpanConfig && a.SpanConfig.BondTerm) || '');
|
||
var bi = termOrder.indexOf((b.SpanConfig && b.SpanConfig.BondTerm) || '');
|
||
return (ai < 0 ? termOrder.length : ai) - (bi < 0 ? termOrder.length : bi);
|
||
});
|
||
this.marginTemplate.Details = this.rebuildSpanBlocks(this.marginTemplate.Details);
|
||
}
|
||
this.changeRuleType(true);
|
||
},
|
||
computed: {
|
||
//区间追保结构:参数组按资产类型区块展示,相邻且区块键(_bk)相同的行合并(rowspan)。
|
||
//_bk 由 加载重建/新增/切换资产类型 时维护:利率债区块含固定4档行,其他区块单行。
|
||
ruleRangeDetailRows() {
|
||
var details = this.marginTemplate.Details || [];
|
||
var keyOf = function (d, i) {
|
||
return d._bk || ('idx_' + i);
|
||
};
|
||
var rows = [];
|
||
for (var i = 0; i < details.length; i++) {
|
||
var key = keyOf(details[i], i);
|
||
if (i > 0 && keyOf(details[i - 1], i - 1) === key) {
|
||
rows.push({ detail: details[i], index: i, rowspan: 0 });
|
||
continue;
|
||
}
|
||
var span = 1;
|
||
for (var j = i + 1; j < details.length; j++) {
|
||
if (keyOf(details[j], j) !== key) break;
|
||
span++;
|
||
}
|
||
rows.push({ detail: details[i], index: i, rowspan: span });
|
||
}
|
||
return rows;
|
||
},
|
||
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
|
||
//其他区块(含全部 ETF 子类,2026-08-27 裁定 ETF 无期限概念不再分档)单段(BondTerm 为空)。
|
||
//段的 detail 缺失时模板侧 v-if 兜底不渲染该段
|
||
ruleRangeBlocks() {
|
||
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
|
||
var blocks = [];
|
||
var rows = this.ruleRangeDetailRows;
|
||
var details = this.marginTemplate.Details || [];
|
||
for (var i = 0; i < rows.length; i++) {
|
||
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 sections = [];
|
||
if (isTBond) {
|
||
for (var t = 0; t < SpanBondTerms.length; t++) {
|
||
var found = null;
|
||
for (var j = head.index; j < head.index + head.rowspan; j++) {
|
||
var bt = (details[j].SpanConfig && details[j].SpanConfig.BondTerm) || '';
|
||
if (bt === SpanBondTerms[t][0]) { found = details[j]; break; }
|
||
}
|
||
sections.push({ key: SpanBondTerms[t][0], termLabel: SpanBondTerms[t][1], detail: found });
|
||
}
|
||
} 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, etfKind: kind, isTBond: isTBond, isFundBlock: ut === FundTypeMask, sections: sections });
|
||
}
|
||
return blocks;
|
||
}
|
||
},
|
||
methods: {
|
||
changeRuleType(isInitial) {
|
||
switch (this.marginTemplate.RuleType.toString()) {
|
||
case MarginRuleTypeEnum.无预付金:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为零。";
|
||
break;
|
||
case MarginRuleTypeEnum.按固定利率:
|
||
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例。";
|
||
break;
|
||
case MarginRuleTypeEnum.按浮动盈亏:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加上对手方交易浮动亏损,扣除对手方交易浮动盈利。";
|
||
break;
|
||
case MarginRuleTypeEnum.类香草预付金规则:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为为成交/实时名义本金的浮动比例,加上对手方交易浮动亏损。浮动比例依据标的涨跌停情况判断。";
|
||
break;
|
||
case MarginRuleTypeEnum.自动赎回买入预付金规则:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为n+2(≤N)个票息之和,N为合约期内敲出观察日总数,n为当前经过的敲出观察日个数(当n+2>N时,n = N - 2)。";
|
||
break;
|
||
case MarginRuleTypeEnum.触发条件预付金规则1:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为成交名义本金的固定比例,加标的跌幅的市值(跌幅达到一定条件触发)";
|
||
break;
|
||
case MarginRuleTypeEnum.触发条件预付金规则2:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加标的跌幅的市值(跌幅达到一定条件触发)";
|
||
break;
|
||
case MarginRuleTypeEnum.触发条件预付金规则3:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加上对手方分段式交易浮动亏损(涨跌幅达到一定条件触发)";
|
||
break;
|
||
case MarginRuleTypeEnum.保底预付金规则:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为对手方最大亏损金额。";
|
||
break;
|
||
case MarginRuleTypeEnum.含赔付预付金计算规则:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "预付金占用为对手方最大赔付金额。";
|
||
break;
|
||
case MarginRuleTypeEnum.交易所基本规则:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "交易所基本规则";
|
||
break;
|
||
case MarginRuleTypeEnum.标准SPAN:
|
||
this.marginTemplate.Comments = "计算标的价格和波动率上涨下跌4个场景下的对手方最大亏损";
|
||
break;
|
||
case MarginRuleTypeEnum.买入自动赎回规则1:
|
||
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
|
||
this.marginTemplate.Comments = "Min(x*首次票息金额+y*Max(0,Pv),合约期间全部票息金额);\n若勾选了“取与保底金额孰小”,则还要与取较小的一个;\n保底金额 =ABS(执行价格 - 保底价格)* ABS(交易数量)。";
|
||
break;
|
||
case MarginRuleTypeEnum.卖出自动赎回规则1:
|
||
this.marginTemplate.Comments = "名义本金*Max(x,y-0.5*虚值比例)+ Flag*Max(0,PV);\n其中虚值比例 = max(0, P - 执行价)/期初价格,当计算日为成交日时,Flag = 0,P = 期初价格,当计算日不为成交日时,Flag = 1,P = 标的现价;\n若勾选了“取与保底金额孰小”,则取较小的一个;\n保底金额 =ABS(执行价格 - 保底价格)* ABS(交易数量)。";
|
||
break;
|
||
case MarginRuleTypeEnum.区间追保结构:
|
||
this.marginTemplate.Comments = "预付金占用为名义本金的固定比例。按利率债期限(<5y/5y-10y/10y-30y/>30y)分别设置预警线/平仓线与多空各4层追保价格区间及追保金额比例,支持再按标的资产类型(利率债/信用债/其它债券/股票/股指等全部资产类型)分组;以期初净价为基准,标的价格落入录入的追保区间时按对应比例追保,触及预警线/平仓线时触发预警或平仓。";
|
||
break;
|
||
case MarginRuleTypeEnum.默认预付金规则:
|
||
this.marginTemplate.Comments = "考虑了14种场景下的Span算法与Delta算法比较的通用预付金规则,该规则可以调整Span涨跌幅度";
|
||
break;
|
||
default:
|
||
this.marginTemplate.Comments = "";
|
||
break;
|
||
}
|
||
this.changeUnderlyingSeperateType(isInitial);
|
||
},
|
||
changeUnderlyingSeperateType(isInitial) {
|
||
if (isInitial && this.marginTemplate.Details.length >= 1) {
|
||
return;
|
||
}
|
||
|
||
if (this.marginTemplate.UnderlyingSeperateType == UnderlyingSeperateTypeEnum.None) {
|
||
this.marginTemplate.UnderlyingSeperateComments = "无论挂钩标的代码为何,都采用同一套参数";
|
||
} else if (this.marginTemplate.UnderlyingSeperateType == UnderlyingSeperateTypeEnum.CustomInstrumentType) {
|
||
this.marginTemplate.UnderlyingSeperateComments = "按标的的资产类型分类:在每组参数中选择适用的资产类型(利率债/信用债/其它债券/股票/股指/商品期货等全部资产类型),按标的资产类型匹配对应参数组的区间追保参数。";
|
||
} else {
|
||
this.marginTemplate.UnderlyingSeperateComments = "把所有标的根据代码所属区间段自动分为4类:\n【1-指数】沪深指数;\n【2-个股/ETF-核准制发行】主板股票、非科创版ETF;\n【3-个股/ETF-非核准制发行】科创版股票、科创版ETF、创业板股票;\n【4-其他】所有不满足以上分类的。";
|
||
}
|
||
|
||
let that = this
|
||
new Promise((reslove) => {
|
||
that.marginTemplate.Details = [];
|
||
reslove();
|
||
}).then(() => {
|
||
var needDetailRules = [];
|
||
needDetailRules.push(MarginRuleTypeEnum.按固定利率);
|
||
needDetailRules.push(MarginRuleTypeEnum.按浮动盈亏);
|
||
needDetailRules.push(MarginRuleTypeEnum.类香草预付金规则);
|
||
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则1);
|
||
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则2);
|
||
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则3);
|
||
needDetailRules.push(MarginRuleTypeEnum.标准SPAN);
|
||
needDetailRules.push(MarginRuleTypeEnum.买入自动赎回规则1);
|
||
needDetailRules.push(MarginRuleTypeEnum.卖出自动赎回规则1);
|
||
needDetailRules.push(MarginRuleTypeEnum.区间追保结构);
|
||
needDetailRules.push(MarginRuleTypeEnum.默认预付金规则);
|
||
if (needDetailRules.indexOf(that.marginTemplate.RuleType.toString()) >= 0) {
|
||
new Promise((reslove) => {
|
||
//规则15:新增的是"资产类型区块"(含区间录入结构与独立区块键);其他规则维持原样
|
||
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
|
||
var d = that.newSpanDetail(0, '');
|
||
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
|
||
that.marginTemplate.Details.push(d);
|
||
} else {
|
||
that.marginTemplate.Details.push(_.cloneDeep(page.detail));
|
||
}
|
||
reslove();
|
||
}).then(() => {
|
||
SetAceDropDown();
|
||
})
|
||
}
|
||
})
|
||
},
|
||
saveMarginTemplateV2(isForClient) {
|
||
//失效确认:编辑中把"生效中"规则改为否时先弹窗提示降级影响,确认后才真正提交;
|
||
//新增规则尚未生效、生效状态未翻转,不弹。自定义规则编辑页无"是否生效"开关,不经过此逻辑
|
||
if (!this.isAdd && this.originalIsValid === 'true' && String(this.marginTemplate.IsValid) === 'false') {
|
||
var that = this;
|
||
main.confirm("该规则失效后,适用范围内的持仓将降级匹配下一优先级规则(客户→全局),确认继续?", function () {
|
||
that.doSaveMarginTemplateV2(isForClient);
|
||
});
|
||
return;
|
||
}
|
||
this.doSaveMarginTemplateV2(isForClient);
|
||
},
|
||
doSaveMarginTemplateV2(isForClient) {
|
||
if (!this.marginTemplate.Name) {
|
||
main.message("模板名称不能为空");
|
||
return;
|
||
}
|
||
this.marginTemplate.TradeTypes = $("#tradeTypes").val().join(",");
|
||
this.marginTemplate.BookIds = normalizeBookIds($("#BookIds").val());
|
||
this.marginTemplate.IsDefault = true;
|
||
this.marginTemplate.IsForClient = isForClient;
|
||
this.marginTemplate.BuySellType = this.sum($("#BuySellType").val());
|
||
this.marginTemplate.MarginScenarios = this.sum($("#MarginScenarios").val());
|
||
var that = this;
|
||
this.marginTemplate.Details.forEach((detail, index) => {
|
||
if (that.marginTemplate.UnderlyingSeperateType == 0) {
|
||
detail.UnderlyingType = 15;
|
||
}
|
||
detail.SpanConfigJson = JSON.stringify(detail.SpanConfig);
|
||
});
|
||
if (that.marginTemplate.UnderlyingSeperateType != 0) {
|
||
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
|
||
//区间追保结构表格:按区块取值——区块首行读选择器,被合并的档位行跟随本区块(区块内资产类型一致)
|
||
var groupValue = 0;
|
||
that.ruleRangeDetailRows.forEach((row) => {
|
||
if (row.rowspan > 0) {
|
||
var ut = $("#UnderlyingType" + row.index).val();
|
||
groupValue = ut ? that.sum(ut) : (row.detail.UnderlyingType || 0);
|
||
//与 onUnderlyingTypeChange 同口径兜底:含利率债(位16)时只能单选利率债
|
||
if ((groupValue & TermTierEnabledUnderlyingMask) > 0) groupValue = TermTierEnabledUnderlyingMask;
|
||
}
|
||
row.detail.UnderlyingType = groupValue;
|
||
});
|
||
}
|
||
else {
|
||
that.marginTemplate.Details.forEach((detail, index) => {
|
||
detail.UnderlyingType = that.sum($("#UnderlyingType" + index).val());
|
||
});
|
||
}
|
||
}
|
||
main.post("/margin_template_v2/saveMarginTemplateV2", { marginTemplate: this.marginTemplate }).done(function (resp) {
|
||
try {
|
||
window.parent.reloadmargin_template();
|
||
(parent || window).layer.closeAll();
|
||
} catch (e) { }
|
||
});
|
||
},
|
||
addDetail() {
|
||
let that = this
|
||
new Promise((reslove) => {
|
||
//规则15:新增参数组=新增一个资产类型区块(默认未选资产类型,单行;选成纯利率债时自动展开4档)
|
||
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
|
||
var d = that.newSpanDetail(0, '');
|
||
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
|
||
that.marginTemplate.Details.push(d);
|
||
} else {
|
||
that.marginTemplate.Details.push(_.cloneDeep(page.detail));
|
||
}
|
||
reslove();
|
||
}).then(() => {
|
||
that.refreshChosen();
|
||
})
|
||
},
|
||
deleteDetail(index) {
|
||
//规则15:删除区块=删除该区块全部 detail 行(利率债区块4行、其他区块1行)
|
||
if (this.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
|
||
var rows = this.ruleRangeDetailRows;
|
||
for (var i = 0; i < rows.length; i++) {
|
||
if (rows[i].index === index) {
|
||
this.marginTemplate.Details.splice(index, Math.max(rows[i].rowspan, 1));
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
this.marginTemplate.Details.splice(index, 1);
|
||
}
|
||
this.refreshChosen();
|
||
},
|
||
//资产类型 chosen 变更(区块级):含利率债(位16)时强制单选利率债;
|
||
//纯利率债区块改为不含利率债时先弹确认(4档收缩为1条,其余3档录入数据将丢弃);
|
||
//反向(非利率债 → 利率债展开4档)保留已有数据,无需确认
|
||
onUnderlyingTypeChange(index) {
|
||
var ut = this.sum($("#UnderlyingType" + index).val());
|
||
if ((ut & TermTierEnabledUnderlyingMask) > 0) ut = TermTierEnabledUnderlyingMask;
|
||
var rows = this.ruleRangeDetailRows;
|
||
var head = null;
|
||
for (var i = 0; i < rows.length; i++) {
|
||
if (rows[i].index === index) { head = rows[i]; break; }
|
||
}
|
||
if (!head) return;
|
||
if ((head.detail.UnderlyingType || 0) === TermTierEnabledUnderlyingMask && ut !== TermTierEnabledUnderlyingMask) {
|
||
var that = this;
|
||
main.confirm("该区块当前为利率债分档配置,改为其他资产类型后其余期限档的录入内容将被清除,确认修改?", function () {
|
||
that.applyUnderlyingTypeChange(index, ut);
|
||
}, function () {
|
||
//取消:Vue 数据未动,翻转一次 UnderlyingType 强制重渲染 select,再由 chosen 重读选中态回退为纯利率债
|
||
head.detail.UnderlyingType = 0;
|
||
Vue.nextTick(function () {
|
||
head.detail.UnderlyingType = TermTierEnabledUnderlyingMask;
|
||
that.refreshChosen();
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
this.applyUnderlyingTypeChange(index, ut);
|
||
},
|
||
//区块资产类型变更的实际落地(onUnderlyingTypeChange 确认后调用):
|
||
//ut=纯利率债 → 区块固定展开4个期限档 detail(保留已有档位行数据,缺档补新行);
|
||
//否则 → 区块收敛为单条 detail(BondTerm 置空,保留首行数据)
|
||
applyUnderlyingTypeChange(index, ut) {
|
||
var rows = this.ruleRangeDetailRows;
|
||
var head = null;
|
||
for (var i = 0; i < rows.length; i++) {
|
||
if (rows[i].index === index) { head = rows[i]; break; }
|
||
}
|
||
if (!head) return;
|
||
var details = this.marginTemplate.Details;
|
||
var headDetail = head.detail;
|
||
var bk = headDetail._bk;
|
||
if (!bk) {
|
||
bk = 'bk_' + (++spanBlockSeq);
|
||
Vue.set(headDetail, '_bk', bk);
|
||
}
|
||
var oldCount = Math.max(head.rowspan, 1);
|
||
if (ut === TermTierEnabledUnderlyingMask) {
|
||
var byTerm = {};
|
||
details.slice(index, index + oldCount).forEach(function (r) {
|
||
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
|
||
if (!byTerm[bt]) byTerm[bt] = r;
|
||
});
|
||
var merged = [];
|
||
for (var t = 0; t < SpanBondTerms.length; t++) {
|
||
var r = byTerm[SpanBondTerms[t][0]] || (t === 0 ? headDetail : null);
|
||
if (!r) r = this.newSpanDetail(TermTierEnabledUnderlyingMask, SpanBondTerms[t][0]);
|
||
r.UnderlyingType = TermTierEnabledUnderlyingMask;
|
||
r.SpanConfig.BondTerm = SpanBondTerms[t][0];
|
||
Vue.set(r, '_bk', bk);
|
||
merged.push(r);
|
||
}
|
||
details.splice.apply(details, [index, oldCount].concat(merged));
|
||
} else {
|
||
headDetail.UnderlyingType = ut;
|
||
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();
|
||
},
|
||
//行合并重组/删除后重渲染:SetAceDropDown 只增强新出现的 select(已初始化的会跳过,见 MyJs.js:137-138),
|
||
//chosen:updated 让被 Vue 复用的 select 按最新 :selected 重读选中态,两者需同时执行
|
||
refreshChosen() {
|
||
Vue.nextTick(function () {
|
||
SetAceDropDown();
|
||
$('.chosen-select').trigger('chosen:updated');
|
||
});
|
||
},
|
||
//新建一条规则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;
|
||
},
|
||
//ETF 子类选择器变更(仅纯基金区块渲染):子类仅用于行区分(取数侧子类行优先),
|
||
//ETF 无期限概念不分档——任何子类切换后区块始终收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind;
|
||
//存量 4 档子类数据在这里被收敛时仅保留首行录入内容)
|
||
onEtfKindChange(blk) {
|
||
var head = blk.sections[0].detail;
|
||
var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
|
||
var details = this.marginTemplate.Details;
|
||
var bk = head._bk;
|
||
var rows = details.filter(function (d) { return d._bk === bk; });
|
||
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'(连续行归一个区块按固定4档补齐);ETF 子类不分档,基金行一律独立成块
|
||
spanTierKey(d) {
|
||
var ut = d.UnderlyingType || 0;
|
||
if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
|
||
return null;
|
||
},
|
||
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为区块并按固定4档补齐(缺档补空行);
|
||
//同档重复/异常档的分档行、其余全部行(含基金+子类行、基金通配行)均各自独立成块展示,不丢数据——
|
||
//存量 ETF 子类 4 档行会以多个单行区块出现,保存时由服务端"ETF 子类不分期限档"校验拦截提示清理
|
||
rebuildSpanBlocks(details) {
|
||
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
|
||
var result = [];
|
||
var that = this;
|
||
var i = 0;
|
||
while (i < details.length) {
|
||
var d = details[i];
|
||
var tk = this.spanTierKey(d);
|
||
if (tk) {
|
||
var rows = [];
|
||
while (i < details.length && this.spanTierKey(details[i]) === tk) {
|
||
rows.push(details[i]);
|
||
i++;
|
||
}
|
||
var byTerm = {};
|
||
var anomalies = [];
|
||
rows.forEach(function (r) {
|
||
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
|
||
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
|
||
});
|
||
var bk = 'bk_' + (++spanBlockSeq);
|
||
termValues.forEach(function (t) {
|
||
var r = byTerm[t];
|
||
if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
|
||
r.UnderlyingType = TermTierEnabledUnderlyingMask;
|
||
r.SpanConfig.BondTerm = t;
|
||
Vue.set(r, '_bk', bk);
|
||
result.push(r);
|
||
});
|
||
anomalies.forEach(function (r) {
|
||
Vue.set(r, '_bk', 'bk_' + (++spanBlockSeq));
|
||
result.push(r);
|
||
});
|
||
} else {
|
||
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
|
||
result.push(d);
|
||
i++;
|
||
}
|
||
}
|
||
return result;
|
||
},
|
||
underlyingTypeNames(mask) {
|
||
mask = parseInt(mask) || 0;
|
||
if (mask === 0) return "全部";
|
||
var names = [];
|
||
for (var i = 0; i < UnderlyingTypeNames.length; i++) {
|
||
if ((mask & UnderlyingTypeNames[i][0]) > 0) names.push(UnderlyingTypeNames[i][1]);
|
||
}
|
||
return names.length > 0 ? names.join("、") : "全部";
|
||
},
|
||
bondTermLabel(detail) {
|
||
var v = (detail.SpanConfig && detail.SpanConfig.BondTerm) || '';
|
||
var map = { '': '全部', '<5y': '≤5y', '5y-10y': '(5y-10y]', '10y-30y': '(10y-30y]', '>30y': '>30y' };
|
||
return map[v] !== undefined ? map[v] : v;
|
||
},
|
||
//规则15录入区块文案:按 detail 资产类型切换计价口径。
|
||
//比较价格口径与计算侧对齐(MarginCalculationBase.CalcSwapSpanMaintenanceMargin:IsBond→中债估值净价,其余→收盘价):
|
||
//纯债券类(利率债16/信用债32/其它债券64,可组合):期初净价/当前净价,金额=×期初全价×券面总额;
|
||
//其余全部类型(基金/债券指数/股票/股指等):参考标的期初净价/参考标的当前收盘价(多空各层统一收盘价),
|
||
//金额=×参考标的期初价格×参考标的名义份额。
|
||
spanText(detail) {
|
||
var ut = (detail && detail.UnderlyingType) || 0;
|
||
//非空且标志位全部落在债券三类内才按债券口径;混合行(债券|非债券)按收盘价口径显示,与取数侧标的实际类型判定方向一致
|
||
var bondMask = 16 | 32 | 64;
|
||
var isBond = ut !== 0 && (ut & ~bondMask) === 0;
|
||
if (!isBond) {
|
||
return {
|
||
priceInit: '参考标的期初净价',
|
||
priceCur: '参考标的当前收盘价',
|
||
priceCurShort1: '参考标的当前收盘价',
|
||
priceCurShort: '参考标的当前收盘价',
|
||
amountBase: '参考标的期初价格 × 参考标的名义份额'
|
||
};
|
||
}
|
||
return {
|
||
priceInit: '期初净价',
|
||
priceCur: '当前净价',
|
||
priceCurShort1: '当前净价',
|
||
priceCurShort: '当前净价',
|
||
amountBase: '期初全价 × 券面总额'
|
||
};
|
||
},
|
||
//4个空追保区间 tier(区间边界/金额比例均存小数,null=未填)
|
||
emptySpanTiers() {
|
||
var tiers = [];
|
||
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
|
||
return tiers;
|
||
},
|
||
//规则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());
|
||
for (var i = 0; i < 4; i++) {
|
||
if (!sc[key][i]) Vue.set(sc[key], i, { Lower: null, Upper: null, AmountRate: null });
|
||
if (sc[key][i].Lower === undefined) Vue.set(sc[key][i], 'Lower', null);
|
||
if (sc[key][i].Upper === undefined) Vue.set(sc[key][i], 'Upper', null);
|
||
if (sc[key][i].AmountRate === undefined) Vue.set(sc[key][i], 'AmountRate', null);
|
||
}
|
||
});
|
||
},
|
||
sum(items) {
|
||
var total = 0;
|
||
if (items) {
|
||
for (var index = 0; index < items.length; index++) {
|
||
total = total + parseFloat(items[index]);
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
},
|
||
mounted: function () {
|
||
/*console.log(this.marginTemplate.Details);*/
|
||
|
||
},
|
||
components: {
|
||
'vue-datepicker': FastVue.vueDatePicker(),
|
||
'vue-number-input': FastVue.vueNumberInput()
|
||
}
|
||
});
|
||
|
||
$(function () {
|
||
$(".datepicker").change(function () {
|
||
var dateVal = $(this).val();
|
||
if (!dateVal) return;
|
||
dateVal = dateVal.replace(/\D/g, '').padStart(4, '0');
|
||
switch (dateVal.length) {
|
||
case 4:
|
||
dateVal = new Date().getFullYear() + dateVal; break;
|
||
case 6:
|
||
dateVal = '20' + dateVal; break;
|
||
case 8: break;
|
||
default:
|
||
dateVal = dateVal.length > 8 ? dateVal.substring(0, 8) : ''; break;
|
||
}
|
||
|
||
if (dateVal) {
|
||
dateVal = moment(dateVal).format('YYYY-MM-DD');
|
||
!/^\d+/.test(dateVal) && (dateVal = '');
|
||
}
|
||
|
||
$(this).datepicker("setDate", dateVal);
|
||
});
|
||
|
||
//chosen 选中变化通过 jQuery trigger("change") 通知(chosen.jquery.js trigger_form_field_change),
|
||
//Vue 的 v-on:change(原生监听)收不到,这里用 jQuery 委托监听规则15区块的资产类型选择器
|
||
$(document).on('change', 'select.chosen-select[id^="UnderlyingType"]', function () {
|
||
var idx = parseInt(this.id.substring('UnderlyingType'.length));
|
||
if (!isNaN(idx)) vue.onUnderlyingTypeChange(idx);
|
||
});
|
||
|
||
SetAceDropDown();
|
||
});
|