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(); } });