/** * @license Highstock JS v9.1.1 (2021-06-04) * * Indicator series type for Highcharts Stock * * (c) 2010-2021 Paweł Dalek * * License: www.highcharts.com/license */ 'use strict'; (function (factory) { if (typeof module === 'object' && module.exports) { factory['default'] = factory; module.exports = factory; } else if (typeof define === 'function' && define.amd) { define('highcharts/indicators/volume-by-price', ['highcharts', 'highcharts/modules/stock'], function (Highcharts) { factory(Highcharts); factory.Highcharts = Highcharts; return factory; }); } else { factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined); } }(function (Highcharts) { var _modules = Highcharts ? Highcharts._modules : {}; function _registerModule(obj, path, args, fn) { if (!obj.hasOwnProperty(path)) { obj[path] = fn.apply(null, args); } } _registerModule(_modules, 'Stock/Indicators/VBP/VBPIndicator.js', [_modules['Core/Animation/AnimationUtilities.js'], _modules['Core/Globals.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (A, H, SeriesRegistry, U) { /* * * * (c) 2010-2021 Paweł Dalek * * Volume By Price (VBP) indicator for Highcharts Stock * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var animObject = A.animObject; var noop = H.noop; var SMAIndicator = SeriesRegistry.seriesTypes.sma; var addEvent = U.addEvent, arrayMax = U.arrayMax, arrayMin = U.arrayMin, correctFloat = U.correctFloat, error = U.error, extend = U.extend, isArray = U.isArray, merge = U.merge; /* eslint-disable require-jsdoc */ // Utils function arrayExtremesOHLC(data) { var dataLength = data.length, min = data[0][3], max = min, i = 1, currentPoint; for (; i < dataLength; i++) { currentPoint = data[i][3]; if (currentPoint < min) { min = currentPoint; } if (currentPoint > max) { max = currentPoint; } } return { min: min, max: max }; } /* eslint-enable require-jsdoc */ var abs = Math.abs, columnPrototype = SeriesRegistry.seriesTypes.column.prototype; /** * The Volume By Price (VBP) series type. * * @private * @class * @name Highcharts.seriesTypes.vbp * * @augments Highcharts.Series */ var VBPIndicator = /** @class */ (function (_super) { __extends(VBPIndicator, _super); function VBPIndicator() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.data = void 0; _this.negWidths = void 0; _this.options = void 0; _this.points = void 0; _this.posWidths = void 0; _this.priceZones = void 0; _this.rangeStep = void 0; _this.volumeDataArray = void 0; _this.zoneStarts = void 0; _this.zoneLinesSVG = void 0; return _this; } VBPIndicator.prototype.init = function (chart) { var indicator = this, params, baseSeries, volumeSeries; H.seriesTypes.sma.prototype.init.apply(indicator, arguments); params = indicator.options.params; baseSeries = indicator.linkedParent; volumeSeries = chart.get(params.volumeSeriesID); indicator.addCustomEvents(baseSeries, volumeSeries); return indicator; }; // Adds events related with removing series VBPIndicator.prototype.addCustomEvents = function (baseSeries, volumeSeries) { var indicator = this; /* eslint-disable require-jsdoc */ function toEmptyIndicator() { indicator.chart.redraw(); indicator.setData([]); indicator.zoneStarts = []; if (indicator.zoneLinesSVG) { indicator.zoneLinesSVG = indicator.zoneLinesSVG.destroy(); } } /* eslint-enable require-jsdoc */ // If base series is deleted, indicator series data is filled with // an empty array indicator.dataEventsToUnbind.push(addEvent(baseSeries, 'remove', function () { toEmptyIndicator(); })); // If volume series is deleted, indicator series data is filled with // an empty array if (volumeSeries) { indicator.dataEventsToUnbind.push(addEvent(volumeSeries, 'remove', function () { toEmptyIndicator(); })); } return indicator; }; // Initial animation VBPIndicator.prototype.animate = function (init) { var series = this, inverted = series.chart.inverted, group = series.group, attr = {}, position; if (!init && group) { position = inverted ? series.yAxis.top : series.xAxis.left; if (inverted) { group['forceAnimate:translateY'] = true; attr.translateY = position; } else { group['forceAnimate:translateX'] = true; attr.translateX = position; } group.animate(attr, extend(animObject(series.options.animation), { step: function (val, fx) { series.group.attr({ scaleX: Math.max(0.001, fx.pos) }); } })); } }; VBPIndicator.prototype.drawPoints = function () { var indicator = this; if (indicator.options.volumeDivision.enabled) { indicator.posNegVolume(true, true); columnPrototype.drawPoints.apply(indicator, arguments); indicator.posNegVolume(false, false); } columnPrototype.drawPoints.apply(indicator, arguments); }; // Function responsible for dividing volume into positive and negative VBPIndicator.prototype.posNegVolume = function (initVol, pos) { var indicator = this, signOrder = pos ? ['positive', 'negative'] : ['negative', 'positive'], volumeDivision = indicator.options.volumeDivision, pointLength = indicator.points.length, posWidths = [], negWidths = [], i = 0, pointWidth, priceZone, wholeVol, point; if (initVol) { indicator.posWidths = posWidths; indicator.negWidths = negWidths; } else { posWidths = indicator.posWidths; negWidths = indicator.negWidths; } for (; i < pointLength; i++) { point = indicator.points[i]; point[signOrder[0] + 'Graphic'] = point.graphic; point.graphic = point[signOrder[1] + 'Graphic']; if (initVol) { pointWidth = point.shapeArgs.width; priceZone = indicator.priceZones[i]; wholeVol = priceZone.wholeVolumeData; if (wholeVol) { posWidths.push(pointWidth / wholeVol * priceZone.positiveVolumeData); negWidths.push(pointWidth / wholeVol * priceZone.negativeVolumeData); } else { posWidths.push(0); negWidths.push(0); } } point.color = pos ? volumeDivision.styles.positiveColor : volumeDivision.styles.negativeColor; point.shapeArgs.width = pos ? indicator.posWidths[i] : indicator.negWidths[i]; point.shapeArgs.x = pos ? point.shapeArgs.x : indicator.posWidths[i]; } }; VBPIndicator.prototype.translate = function () { var indicator = this, options = indicator.options, chart = indicator.chart, yAxis = indicator.yAxis, yAxisMin = yAxis.min, zoneLinesOptions = indicator.options.zoneLines, priceZones = (indicator.priceZones), yBarOffset = 0, indicatorPoints, volumeDataArray, maxVolume, primalBarWidth, barHeight, barHeightP, oldBarHeight, barWidth, pointPadding, chartPlotTop, barX, barY; columnPrototype.translate.apply(indicator); indicatorPoints = indicator.points; // Do translate operation when points exist if (indicatorPoints.length) { pointPadding = options.pointPadding < 0.5 ? options.pointPadding : 0.1; volumeDataArray = indicator.volumeDataArray; maxVolume = arrayMax(volumeDataArray); primalBarWidth = chart.plotWidth / 2; chartPlotTop = chart.plotTop; barHeight = abs(yAxis.toPixels(yAxisMin) - yAxis.toPixels(yAxisMin + indicator.rangeStep)); oldBarHeight = abs(yAxis.toPixels(yAxisMin) - yAxis.toPixels(yAxisMin + indicator.rangeStep)); if (pointPadding) { barHeightP = abs(barHeight * (1 - 2 * pointPadding)); yBarOffset = abs((barHeight - barHeightP) / 2); barHeight = abs(barHeightP); } indicatorPoints.forEach(function (point, index) { barX = point.barX = point.plotX = 0; barY = point.plotY = (yAxis.toPixels(priceZones[index].start) - chartPlotTop - (yAxis.reversed ? (barHeight - oldBarHeight) : barHeight) - yBarOffset); barWidth = correctFloat(primalBarWidth * priceZones[index].wholeVolumeData / maxVolume); point.pointWidth = barWidth; point.shapeArgs = indicator.crispCol.apply(// eslint-disable-line no-useless-call indicator, [barX, barY, barWidth, barHeight]); point.volumeNeg = priceZones[index].negativeVolumeData; point.volumePos = priceZones[index].positiveVolumeData; point.volumeAll = priceZones[index].wholeVolumeData; }); if (zoneLinesOptions.enabled) { indicator.drawZones(chart, yAxis, indicator.zoneStarts, zoneLinesOptions.styles); } } }; VBPIndicator.prototype.getValues = function (series, params) { var indicator = this, xValues = series.processedXData, yValues = series.processedYData, chart = indicator.chart, ranges = params.ranges, VBP = [], xData = [], yData = [], isOHLC, volumeSeries, priceZones; // Checks if base series exists if (!series.chart) { error('Base series not found! In case it has been removed, add ' + 'a new one.', true, chart); return; } // Checks if volume series exists if (!(volumeSeries = (chart.get(params.volumeSeriesID)))) { error('Series ' + params.volumeSeriesID + ' not found! Check `volumeSeriesID`.', true, chart); return; } // Checks if series data fits the OHLC format isOHLC = isArray(yValues[0]); if (isOHLC && yValues[0].length !== 4) { error('Type of ' + series.name + ' series is different than line, OHLC or candlestick.', true, chart); return; } // Price zones contains all the information about the zones (index, // start, end, volumes, etc.) priceZones = indicator.priceZones = indicator.specifyZones(isOHLC, xValues, yValues, ranges, volumeSeries); priceZones.forEach(function (zone, index) { VBP.push([zone.x, zone.end]); xData.push(VBP[index][0]); yData.push(VBP[index][1]); }); return { values: VBP, xData: xData, yData: yData }; }; // Specifing where each zone should start ans end VBPIndicator.prototype.specifyZones = function (isOHLC, xValues, yValues, ranges, volumeSeries) { var indicator = this, rangeExtremes = (isOHLC ? arrayExtremesOHLC(yValues) : false), lowRange = rangeExtremes ? rangeExtremes.min : arrayMin(yValues), highRange = rangeExtremes ? rangeExtremes.max : arrayMax(yValues), zoneStarts = indicator.zoneStarts = [], priceZones = [], i = 0, j = 1, rangeStep, zoneStartsLength; if (!lowRange || !highRange) { if (this.points.length) { this.setData([]); this.zoneStarts = []; if (this.zoneLinesSVG) { this.zoneLinesSVG = this.zoneLinesSVG.destroy(); } } return []; } rangeStep = indicator.rangeStep = correctFloat(highRange - lowRange) / ranges; zoneStarts.push(lowRange); for (; i < ranges - 1; i++) { zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep)); } zoneStarts.push(highRange); zoneStartsLength = zoneStarts.length; // Creating zones for (; j < zoneStartsLength; j++) { priceZones.push({ index: j - 1, x: xValues[0], start: zoneStarts[j - 1], end: zoneStarts[j] }); } return indicator.volumePerZone(isOHLC, priceZones, volumeSeries, xValues, yValues); }; // Calculating sum of volume values for a specific zone VBPIndicator.prototype.volumePerZone = function (isOHLC, priceZones, volumeSeries, xValues, yValues) { var indicator = this, volumeXData = volumeSeries.processedXData, volumeYData = volumeSeries.processedYData, lastZoneIndex = priceZones.length - 1, baseSeriesLength = yValues.length, volumeSeriesLength = volumeYData.length, previousValue, startFlag, endFlag, value, i; // Checks if each point has a corresponding volume value if (abs(baseSeriesLength - volumeSeriesLength)) { // If the first point don't have volume, add 0 value at the // beggining of the volume array if (xValues[0] !== volumeXData[0]) { volumeYData.unshift(0); } // If the last point don't have volume, add 0 value at the end // of the volume array if (xValues[baseSeriesLength - 1] !== volumeXData[volumeSeriesLength - 1]) { volumeYData.push(0); } } indicator.volumeDataArray = []; priceZones.forEach(function (zone) { zone.wholeVolumeData = 0; zone.positiveVolumeData = 0; zone.negativeVolumeData = 0; for (i = 0; i < baseSeriesLength; i++) { startFlag = false; endFlag = false; value = isOHLC ? yValues[i][3] : yValues[i]; previousValue = i ? (isOHLC ? yValues[i - 1][3] : yValues[i - 1]) : value; // Checks if this is the point with the // lowest close value and if so, adds it calculations if (value <= zone.start && zone.index === 0) { startFlag = true; } // Checks if this is the point with the highest // close value and if so, adds it calculations if (value >= zone.end && zone.index === lastZoneIndex) { endFlag = true; } if ((value > zone.start || startFlag) && (value < zone.end || endFlag)) { zone.wholeVolumeData += volumeYData[i]; if (previousValue > value) { zone.negativeVolumeData += volumeYData[i]; } else { zone.positiveVolumeData += volumeYData[i]; } } } indicator.volumeDataArray.push(zone.wholeVolumeData); }); return priceZones; }; // Function responsoble for drawing additional lines indicating zones VBPIndicator.prototype.drawZones = function (chart, yAxis, zonesValues, zonesStyles) { var indicator = this, renderer = chart.renderer, zoneLinesSVG = indicator.zoneLinesSVG, zoneLinesPath = [], leftLinePos = 0, rightLinePos = chart.plotWidth, verticalOffset = chart.plotTop, verticalLinePos; zonesValues.forEach(function (value) { verticalLinePos = yAxis.toPixels(value) - verticalOffset; zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([[ 'M', leftLinePos, verticalLinePos ], [ 'L', rightLinePos, verticalLinePos ]], zonesStyles.lineWidth)); }); // Create zone lines one path or update it while animating if (zoneLinesSVG) { zoneLinesSVG.animate({ d: zoneLinesPath }); } else { zoneLinesSVG = indicator.zoneLinesSVG = renderer.path(zoneLinesPath).attr({ 'stroke-width': zonesStyles.lineWidth, 'stroke': zonesStyles.color, 'dashstyle': zonesStyles.dashStyle, 'zIndex': indicator.group.zIndex + 0.1 }) .add(indicator.group); } }; /** * Volume By Price indicator. * * This series requires `linkedTo` option to be set. * * @sample stock/indicators/volume-by-price * Volume By Price indicator * * @extends plotOptions.sma * @since 6.0.0 * @product highstock * @requires stock/indicators/indicators * @requires stock/indicators/volume-by-price * @optionparent plotOptions.vbp */ VBPIndicator.defaultOptions = merge(SMAIndicator.defaultOptions, { /** * @excluding index, period */ params: { // Index and period are unchangeable, do not inherit (#15362) index: void 0, period: void 0, /** * The number of price zones. */ ranges: 12, /** * The id of volume series which is mandatory. For example using * OHLC data, volumeSeriesID='volume' means the indicator will be * calculated using OHLC and volume values. */ volumeSeriesID: 'volume' }, /** * The styles for lines which determine price zones. */ zoneLines: { /** * Enable/disable zone lines. */ enabled: true, /** * Specify the style of zone lines. * * @type {Highcharts.CSSObject} * @default {"color": "#0A9AC9", "dashStyle": "LongDash", "lineWidth": 1} */ styles: { /** @ignore-options */ color: '#0A9AC9', /** @ignore-options */ dashStyle: 'LongDash', /** @ignore-options */ lineWidth: 1 } }, /** * The styles for bars when volume is divided into positive/negative. */ volumeDivision: { /** * Option to control if volume is divided. */ enabled: true, styles: { /** * Color of positive volume bars. * * @type {Highcharts.ColorString} */ positiveColor: 'rgba(144, 237, 125, 0.8)', /** * Color of negative volume bars. * * @type {Highcharts.ColorString} */ negativeColor: 'rgba(244, 91, 91, 0.8)' } }, // To enable series animation; must be animationLimit > pointCount animationLimit: 1000, enableMouseTracking: false, pointPadding: 0, zIndex: -1, crisp: true, dataGrouping: { enabled: false }, dataLabels: { allowOverlap: true, enabled: true, format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}', padding: 0, style: { /** @internal */ fontSize: '7px' }, verticalAlign: 'top' } }); return VBPIndicator; }(SMAIndicator)); extend(VBPIndicator.prototype, { nameBase: 'Volume by Price', nameComponents: ['ranges'], bindTo: { series: false, eventName: 'afterSetExtremes' }, calculateOn: 'render', markerAttribs: noop, drawGraph: noop, getColumnMetrics: columnPrototype.getColumnMetrics, crispCol: columnPrototype.crispCol }); SeriesRegistry.registerSeriesType('vbp', VBPIndicator); /* * * * Default Export * * */ /** * A `Volume By Price (VBP)` series. If the [type](#series.vbp.type) option is * not specified, it is inherited from [chart.type](#chart.type). * * @extends series,plotOptions.vbp * @since 6.0.0 * @product highstock * @excluding dataParser, dataURL * @requires stock/indicators/indicators * @requires stock/indicators/volume-by-price * @apioption series.vbp */ ''; // to include the above in the js output return VBPIndicator; }); _registerModule(_modules, 'masters/indicators/volume-by-price.src.js', [], function () { }); }));