const RiskFreeRateFormat = Object.freeze({ append: '%' });
const GreekFormat = main.numberFormat({ grouping: true });
//标的树形
const pageMgr = (new function () {
var _self = this, _zTreeObj, _exportHeadCompiled, _popDiyId = '';
const DiyFields = ['UnderlyingPrice', 'DividenRate', 'ConstVol'];
function formatRate(val) {
if (typeof (val) === 'undefined' || val === null || val === false) {
return " ";
}
return (val * 100).toFixed(4).replace(/0{2}$/, "") + "%";
}
//标的--获取计算所需标的信息
this.underlying_forCalc = function () {
if (!_zTreeObj) return [];
let nodes = _zTreeObj.getNodes();
let underlyings = nodes.reduce((acc, cur) => {
return acc.concat(cur.children.map(x => x.mydata));
}, []);
return underlyings;
};
//标的--获取所有被选中的标的信息
this.underlying_checked = function () {
if (!_zTreeObj) return [];
let nodes = _zTreeObj.getNodes();
let underlyings = nodes.reduce((acc, cur) => {
return acc.concat(cur.children.filter(x => x.checked).map(x => x.mydata));
}, []);
return underlyings;
};
//标的--获取选择项并转换为对象返回
this.underlying_getSelectedObj = function () {
return _zTreeObj ? _zTreeObj.getCheckedNodes(true).reduce((acc, cur) => {
!acc && (acc = {});
if (cur.mytype === 'underlying') {
acc[cur.mydata.UnderlyingCode.toUpperCase()] = cur.mydata;
}
return acc;
}, null) : null;
};
//标的--价格/分红率/波动率自定义视图呈现
function underlying_diyDom(treeId, treeNode) {
if (!DiyFields.includes(treeNode.mytype)) return;
let diyId = 'diy_' + treeNode.mytype + '_' + treeNode.tId;
if ($("#" + diyId).length) {
$("#" + diyId).remove();
}
let aObj = $("#" + treeNode.tId + "_a");
let className = 'tree-diy-field ' + treeNode.mytype;
let html = ` ${treeNode.mydata} `;
aObj.after(html);
};
//标的--价格/分红率/波动率自定义视图-更新
function underlying_diyDom_update(underlyings) {
if (!_zTreeObj || !underlyings) return;
let nodes = _zTreeObj.getNodes();
nodes = _.flatMap(nodes, x => x.children);
nodes.forEach(x => {
let data = underlyings.find(y => y.UnderlyingCode === x.name);
let constVol = x.mydata.ConstVol = data ? data.ConstVol : "";
let dividenRate = x.mydata.DividenRate = data ? data.DividenRate : "";
let volNode = x.children.find(y => y.name.includes('波动率'));
if (volNode) {
volNode.mydata = formatRate(constVol);
underlying_diyDom("", volNode);
}
let divNode = x.children.find(y => y.name.includes('分红率'));
if (divNode) {
divNode.mydata = formatRate(dividenRate);
underlying_diyDom("", divNode);
}
});
}
//标的--更新树控件
this.underlying_updateTree = function (underlyings) {
if (_zTreeObj) {
underlying_diyDom_update(underlyings);
return $('.tree-diy-changed').removeClass('tree-diy-changed');
}
$.fn.zTree.destroy();
let treeNodeObj = _.reduce(underlyings, (acc, cur) => {
let varietyCode = cur.VarietyCode || '未知';
if (!(varietyCode in acc)) {
acc[varietyCode] = { name: varietyCode, open: true, checked: true, mytype: 'variety', children: [] };
}
let node = {
name: cur.UnderlyingCode, open: true, checked: true, mytype: 'underlying', mydata: cur, children: [
{ name: '价格:', mydata: cur.UnderlyingPrice, nocheck: true, mytype: 'UnderlyingPrice' },
{ name: '分红率:', mydata: formatRate(cur.DividenRate), nocheck: true, mytype: 'DividenRate' },
{ name: '波动率:', mydata: formatRate(cur.ConstVol), nocheck: true, mytype: 'ConstVol' },
]
};
acc[varietyCode].children.push(node)
return acc;
}, {});
let treeSettings = {
view: {
dblClickExpand: true,
selectedMulti: true,
showLine: true,
showTitle: false,
addDiyDom: underlying_diyDom
},
check: {
enable: true,
chkStyle: "checkbox"
},
callback: {
onCheck(event, treeId, treeNode) {
resultVue.updateKey++;
}
}
};
let treeNodes = _.sortBy(Object.values(treeNodeObj), ['name']);
_zTreeObj = $.fn.zTree.init($("#underlyingTree"), treeSettings, treeNodes);
};
//标的--更新树控件--清除波动率
this.underlying_updateTree_clearVol = function () {
if (!_zTreeObj) return;
let nodes = _zTreeObj.getNodes();
nodes = _.flatMap(nodes, x => x.children);
nodes.forEach(x => {
x.mydata.ConstVol = '';
let volNode = x.children.find(y => y.name.includes('波动率'));
if (volNode) {
volNode.mydata = '';
underlying_diyDom("", volNode);
}
});
};
//标的--更新树控件--清除分红率
this.underlying_updateTree_clearDividenRate = function () {
if (!_zTreeObj) return;
let nodes = _zTreeObj.getNodes();
nodes = _.flatMap(nodes, x => x.children);
nodes.forEach(x => {
x.mydata.DividenRate = '';
let divNode = x.children.find(y => y.name.includes('分红率'));
if (divNode) {
divNode.mydata = '';
underlying_diyDom("", divNode);
}
});
};
//标的--初始化编辑探测
function underlying_initPopEdit() {
$('body').popover({
selector: ".tree-diy-field", placement: 'bottom', html: true, sanitize: false,
content: document.getElementById('underlyingPopEditTpl').innerHTML
}).on('show.bs.popover', function (e) {
$(_popDiyId).popover('hide');
_popDiyId = '#' + $(e.target).attr('id');
}).on('shown.bs.popover', function (e) {
let tid = $(e.target).data('tid');
let tnode = _zTreeObj.getNodeByTId(tid);
$('#underlying_tid').val(tid);
$('#underlying_edit').val(tnode.mydata).on('keydown', function (event) {
let charCode = event.which || event.keyCode;
if (charCode === 13 || charCode === 108) {
pageMgr.underlying_updatePopEdit();
}
});
let isUnderlyingPrice = tnode.mytype === 'UnderlyingPrice';
FastVue.numberInput('underlying_edit', { append: isUnderlyingPrice ? "" : "%", negative: isUnderlyingPrice, precision: 4 });
$('#underlying_edit').focus();
});
}
//标的--更新编辑弹窗
this.underlying_updatePopEdit = function () {
if (!$('#underlying_edit').length) return;
let val = $('#underlying_edit').val();
let tnode = _zTreeObj.getNodeByTId($('#underlying_tid').val());
tnode.mydata = val && val !== '%' ? val : ' ';
$(_popDiyId).html(" " + tnode.mydata + " ").addClass("tree-diy-changed").popover('hide');
let pnode = tnode.getParentNode();
pnode.mydata[tnode.mytype] = FastVue.parseNumber(val) || '';
};
//标的--关闭编辑弹窗
this.underlying_closePopEdit = function () {
$(_popDiyId).popover('hide');
};
//导出--获取导出头部
this.export_getHead = function () {
let data = _.clone(searchVue.CalcData);
data.Norms = searchVue.Norms;
data.Underlyings = pageMgr.underlying_checked();
!_exportHeadCompiled && (_exportHeadCompiled = _.template($('#exportHeaderTpl').html()));
let html = _exportHeadCompiled(data);
let $table = $(html);
$table.find('th').css('background', '#6F9EBA');
let $tbody = $table.children('tbody');
return ($tbody.length ? $tbody : $table).html();
};
//导出--汇总标的导出
function export_underlying() {
let $table = $('
').html($("#excelTable").html())
$table.find('a').replaceWith(function (i, cont) {
return cont;
});
$table.find('.tdstrip').css('background', '#C6E2FF');
$table.find('thead th').css('color', '#fff').css('background', '#3f484d');
let head = _self.export_getHead();
let content = _.map($table.children(), x => x.innerHTML).join();
$("#excelTable_export").html(head + content);
ExcellentExport.excel(this, 'excelTable_export', '情景计算');
}
//导出--按标的品种导出
function export_variety() {
let $table = $('').html($("#excelTable2").html())
$table.find('.tdstrip').css('background', '#C6E2FF');
$table.find('th').css('color', '#fff').css('background', '#3f484d');
let head = _self.export_getHead();
let content = $table.html();
$("#excelTable_export").html(head + content);
ExcellentExport.excel(this, 'excelTable_export', '情景计算');
}
//导出--按标的导出
function export_underlyings() {
let $table = $('').html($("#excelTable3").html())
$table.find('.tdstrip').css('background', '#C6E2FF');
$table.find('th').css('color', '#fff').css('background', '#3f484d');
let head = _self.export_getHead();
let content = $table.html();
$("#excelTable_export").html(head + content);
ExcellentExport.excel(this, 'excelTable_export', '情景计算');
}
$(function () {
underlying_initPopEdit();
$('#export_excel_byVariety').on('click', export_variety);
$('#export_excel_byUnderlying').on('click', export_underlying);
$('#export_excel_byUnderlyings').on('click', export_underlyings);
});
}());
//查询视图
const searchVue = new Vue({
el: '#searchdiv',
data: {
Norms: ['Pv'],
VolType: '对冲',
ValueDate: pageData.ValueDate,
RiskFreeRate: pageData.SysRiskFreeRate,
UseTradeDivendRate: true,
IsFullyHedged: true,
ConfigId: 0,
CalcData: {
updateKey: 0, xType: '', yType: '', xRates: [], yRates: [],
ConfigName: '', ValueDate: '', RiskFreeRate: '', VolType: '', Underlyings: null
}
},
mounted() {
this.ConfigId = pageData.ScenConfigs.length ? pageData.ScenConfigs[0].id : 0;
},
methods: {
scenChange() {
resultVue.scenChange(this.ConfigId);
},
calculate() {
let data = {
ConfigId: this.ConfigId,
SearchModel: pageData.SearchModel,
VolType: this.VolType,
RiskFreeRate: this.RiskFreeRate,
ValueDate: this.ValueDate,
UseTradeDivendRate: this.UseTradeDivendRate,
IsFullyHedged: this.IsFullyHedged,
Underlyings: pageMgr.underlying_forCalc()
};
let self = this;
main.post('/ScenarioAnalysis/AjaxScenarioCalc', data).done(function (resp) {
Object.assign(self.CalcData, data, resp);
pageMgr.underlying_updateTree(resp.Underlyings);
resultVue.xType = resp.xType;
resultVue.yType = resp.yType;
resultVue.xRates = resp.xRates;
resultVue.yRates = resp.yRates;
resultVue.calcResults = resp.ResultItems;
resultVue.updateKey++;
});
},
changeVolType() {
pageMgr.underlying_updateTree_clearVol();
},
changeUseTradeDivendRate() {
pageMgr.underlying_updateTree_clearDividenRate();
},
changeIsFullyHedged() {
this.calculate();
}
},
components: {
'vue-number-input': FastVue.vueNumberInput()
}
});
//指标折线图
const chartMgr = (new function () {
var myChart, _xtype = "", _ytype = "";
//初始化chart控件
function __init() {
let data = {
labels: [],
datasets: [
{
label: 'PV', data: [],
borderColor: '#FF6384',
backgroundColor: '#FF6384'
}
]
};
let config = {
type: 'line',
data: data,
options: {
responsive: false,
plugins: {
legend: {
position: 'right', title: {
display: true, text: '', font: { size: 20, weight: 'bold', lineHeight: 1.2 }
}
},
title: { display: true, text: '' }
},
scales: {
x: {
display: true,
title: {
display: true, text: '', color: '#911',
font: { size: 20, weight: 'bold', lineHeight: 1.2 },
padding: { top: 4, left: 0, right: 0, bottom: 0 }
}
},
y: {
display: true,
title: {
display: true, text: '', color: '#191',
font: { size: 20, weight: 'bold', lineHeight: 1.2 }
}
}
}
}
};
var ctx = document.getElementById("myChart").getContext("2d");
myChart = new Chart(ctx, config);
};
function __showLayer() {
layer.open({
type: 1,
title: '指标折线图',
closeBtn: 1,
area: ['900px', '700px'],
shadeClose: true,
content: $('#myChartDiv')
});
}
const consColors = ["#1DC9B7", "#FFC241", "#0E52BE", "#51ADF6", "#FF7270", "#965AFF", "#387EF0", "#FD3995"];
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
//在弹窗上显示图表
function __showChart(resultRows, norm, byClick) {
if (byClick) {
$(this).parent().children().removeClass('active');
$(this).addClass('active');
}
!norm && (norm = resultRows[0].norm);
let datasets = [], index = 0;
for (let dataRow of resultRows) {
if (dataRow.norm === norm) {
let color = consColors[index++] || getRandomColor();
datasets.push({
label: dataRow.label,
data: dataRow.values.map(x => parseFloat(x.replaceAll(',', ''))),
borderColor: color, backgroundColor: color
});
}
}
myChart.data.datasets = datasets;
myChart.config.options.scales.x.title.text = _xtype;
myChart.config.options.scales.y.title.text = norm;
//myChart.config.options.plugins.title.text = _ytype;
myChart.config.options.plugins.legend.title.text = _ytype;
myChart.update();
}
//在弹窗上显示指标按钮
function __showNormBtns(resultRows) {
let firstRow = resultRows[0];
let $s = $('#myChartDiv').children(':last'); $s.html('');
for (var i = 0; i < firstRow.rowspan; i++) {
let row = resultRows[i];
let $h = $('' + row.norm + '')
$h.on('click', __showChart.bind($h.get(0), resultRows, row.norm, true));
(i === 0) && ($h.addClass('active'));
$h.appendTo($s);
}
}
//颠倒XY位置
function __invertXY(resultRows, xRates) {
let objArr = xRates.map(x => {
return { label: x };
});
resultRows.forEach(row => {
for (let x = 0; x < objArr.length; x++) {
let obj = objArr[x];
if (!(row.norm in obj)) {
obj[row.norm] = [];
}
obj[row.norm].push(row.values[x]);
}
});
let mapArr = objArr.map(x => {
let arr = [];
for (let [k, v] of Object.entries(x)) {
k !== 'label' && arr.push({ norm: k, values: v, label: x.label });
}
arr[0].rowspan = arr.length;
return arr;
});
return mapArr.flat();
}
this.setXyTypeDesc = function (xtype, ytype) {
_xtype = xtype;
_ytype = ytype;
};
//显示Y轴指标线图
this.showChartY = function (resultRows, xRates, yRates) {
!myChart && __init();
resultRows = resultRows.map(x => {
let clone = Object.assign({}, x);
clone.label = clone.yRate;
return clone;
});
__showNormBtns(resultRows);
myChart.data.labels = xRates;
__showChart(resultRows)
__showLayer();
};
//显示X轴指标线图
this.showChartX = function (resultRows, xRates, yRates) {
!myChart && __init();
resultRows = __invertXY(resultRows, xRates);
__showNormBtns(resultRows);
myChart.data.labels = yRates;
__showChart(resultRows)
__showLayer();
};
}());
//结果表格
const resultVue = new Vue({
el: '#resultdiv',
data: {
xType: '标的价格',
yType: '波动率',
xRates: [],
yRates: [],
calcResults: null,
updateKey: 0,
vResults: [],
vUnderlyingResults:[]
},
computed: {
resultRows() {
let norms = searchVue.Norms, xyMap = null;
if (this.updateKey && this.calcResults && this.calcResults.length) {
let selectedUnderlyingObj = pageMgr.underlying_getSelectedObj();
xyMap = selectedUnderlyingObj ? this.calcResults.reduce((acc, cur) => {
if (cur.UnderlyingCode.toUpperCase() in selectedUnderlyingObj) {
let key = cur.xIndex + '_' + cur.yIndex;
if (key in acc) {
let item = acc[key];
for (let [k, v] of Object.entries(item)) {
item[k] += cur[k];
}
} else {
acc[key] = _.cloneDeep(cur);
}
}
return acc;
}, {}) : null;
}
let results = [], yIndex = 0;
for (let yRate of this.yRates) {
let normIndex = 0;
for (let norm of norms) {
let data = {
norm: norm,
yRate: yRate,
rowspan: normIndex > 0 ? 0 : norms.length,
values: new Array(this.xRates.length),
tdStyle: normIndex % 2 === 1 ? 'tdstrip' : ''
};
if (xyMap) {
this.xRates.forEach((value, xIndex) => {
let key = xIndex + '_' + yIndex;
if (norm === 'PnLPercent') {
let pnl = xyMap[key]['PnL'];
let zeroPv = xyMap[key]['ZeroPv'];
data.values[xIndex] = Math.abs(zeroPv) > 0 ? (pnl / zeroPv * 100).toFixed(2) + '%' : '0%';
} else {
data.values[xIndex] = GreekFormat(xyMap[key][norm]);
}
});
}
normIndex++;
results.push(data);
}
yIndex++;
}
if (results.length > 0 && window.parent.getTestStatus()) {
var arr = [];
results.map((item) => item.values.map((item2) => arr.push(item2)));
if (arr.length > 0) {
window.parent.setTestValue(arr);
}
}
return results;
}
},
mounted() {
this.scenChange(searchVue.ConfigId);
},
methods: {
scenChange(configId) {
if (this.calcResults) return;
let config = pageData.ScenConfigs.find(x => x.id === searchVue.ConfigId);
if (!config) return;
let xRates = _.split(config.xRates, /[,,]/);
let yRates = _.split(config.yRates, /[,,]/);
this.xType = config.xType;
this.yType = config.yType;
this.xRates = _.drop(xRates, x => !_.trim(x));
this.yRates = _.drop(yRates, x => !_.trim(x));
},
exportByUnderlying() {
let el = document.getElementById('export_excel_byUnderlying');
el.setAttribute('download', "情景分析_" + searchVue.CalcData.ConfigName);
el.click();
},
exportByVariety() {
this.sumResultByVariety();
this.$nextTick(function () {
let el = document.getElementById('export_excel_byVariety');
el.setAttribute('download', "情景分析_品种_" + searchVue.CalcData.ConfigName);
el.click();
});
},
exportByUnderlyings() {
this.sumResultByUnderlying();
this.$nextTick(function () {
let el = document.getElementById('export_excel_byUnderlyings');
el.setAttribute('download', "情景分析_标的_" + searchVue.CalcData.ConfigName);
el.click();
});
},
sumResultByVariety() {
let norms = searchVue.Norms, vxyMap = null;
if (this.calcResults && this.calcResults.length) {
let selectedUnderlyingObj = pageMgr.underlying_getSelectedObj();
vxyMap = selectedUnderlyingObj ? this.calcResults.reduce((acc, cur) => {
if (cur.UnderlyingCode.toUpperCase() in selectedUnderlyingObj) {
let vaCode = selectedUnderlyingObj[cur.UnderlyingCode.toUpperCase()].VarietyCode || '未知';
let accItem = vaCode in acc ? acc[vaCode] : (acc[vaCode] = {});
let xyKey = cur.xIndex + '_' + cur.yIndex;
if (xyKey in accItem) {
let item = accItem[xyKey];
for (let [k, v] of Object.entries(item)) {
item[k] += cur[k];
}
} else {
accItem[xyKey] = _.cloneDeep(cur);
}
}
return acc;
}, {}) : null;
}
if (!vxyMap) return null;
let results = [];
for (let [vCode, xyMap] of Object.entries(vxyMap)) {
let vResult = { VarietyCode: vCode, rows: [] }, yIndex = 0;
for (let yRate of this.yRates) {
let normIndex = 0;
for (let norm of norms) {
let data = {
norm: norm,
yRate: normIndex > 0 ? "" : yRate,
rowspan: normIndex > 0 ? '' : norms.length,
values: new Array(this.xRates.length),
tdStyle: normIndex % 2 === 1 ? 'tdstrip' : ''
};
this.xRates.forEach((value, xIndex) => {
let xyKey = xIndex + '_' + yIndex;
if (norm === 'PnLPercent') {
let pnl = xyMap[xyKey]['PnL'];
let zeroPv = xyMap[xyKey]['ZeroPv'];
data.values[xIndex] = Math.abs(zeroPv) > 0 ? (pnl / zeroPv * 100).toFixed(2) + '%' : '0%';
} else {
data.values[xIndex] = GreekFormat(xyMap[xyKey][norm]);
}
});
normIndex++;
vResult.rows.push(data);
}
yIndex++;
}
results.push(vResult);
}
return this.vResults = results;
},
sumResultByUnderlying() {
let norms = searchVue.Norms, udyMap = null;
if (this.calcResults && this.calcResults.length) {
let selectedUnderlyingObj = pageMgr.underlying_getSelectedObj();
udyMap = selectedUnderlyingObj ? this.calcResults.reduce((acc, cur) => {
if (cur.UnderlyingCode.toUpperCase() in selectedUnderlyingObj) {
let vaCode = selectedUnderlyingObj[cur.UnderlyingCode.toUpperCase()].UnderlyingCode || '未知';
let accItem = vaCode in acc ? acc[vaCode] : (acc[vaCode] = {});
let xyKey = cur.xIndex + '_' + cur.yIndex;
if (xyKey in accItem) {
let item = accItem[xyKey];
for (let [k, v] of Object.entries(item)) {
item[k] += cur[k];
}
} else {
accItem[xyKey] = _.cloneDeep(cur);
}
}
return acc;
}, {}) : null;
}
if (!udyMap) return null;
let results = [];
for (let [vCode, dyMap] of Object.entries(udyMap)) {
let vResult = { UnderlyingCode: vCode, rows: [] }, yIndex = 0;
for (let yRate of this.yRates) {
let normIndex = 0;
for (let norm of norms) {
let data = {
norm: norm,
yRate: normIndex > 0 ? "" : yRate,
rowspan: normIndex > 0 ? '' : norms.length,
values: new Array(this.xRates.length),
tdStyle: normIndex % 2 === 1 ? 'tdstrip' : ''
};
this.xRates.forEach((value, xIndex) => {
let xyKey = xIndex + '_' + yIndex;
if (norm === 'PnLPercent') {
let pnl = dyMap[xyKey]['PnL'];
let zeroPv = dyMap[xyKey]['ZeroPv'];
data.values[xIndex] = Math.abs(zeroPv) > 0 ? (pnl / zeroPv * 100).toFixed(2) + '%' : '0%';
} else {
data.values[xIndex] = GreekFormat(dyMap[xyKey][norm]);
}
});
normIndex++;
vResult.rows.push(data);
}
yIndex++;
}
results.push(vResult);
}
return this.vUnderlyingResults = results;
},
showChartY(yIndex) {
chartMgr.setXyTypeDesc(this.xType, this.yType);
this.resultRows.length && chartMgr.showChartY(this.resultRows, this.xRates, this.yRates);
},
showChartX(xIndex) {
chartMgr.setXyTypeDesc(this.yType, this.xType);
this.resultRows.length && chartMgr.showChartX(this.resultRows, this.xRates, this.yRates);
}
}
});
$(function () {
if (window.parent.getTestStatus()) {
searchVue.Norms[0] = "PnL";
searchVue.calculate();
}
});