Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/scenario/scenarioCalc.js
T
2024-05-09 14:06:26 +08:00

744 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `<a id="${diyId}" data-tid="${treeNode.tId}" href="javascript:;" class="${className}">&nbsp;${treeNode.mydata}&nbsp;</a>`;
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("&nbsp;" + tnode.mydata + "&nbsp;").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 = $('<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 = $('<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 = $('<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 = $('<a class="chart-action">' + row.norm + '</a>')
$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();
}
});