VBPIndicator.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /* *
  2. *
  3. * (c) 2010-2020 Paweł Dalek
  4. *
  5. * Volume By Price (VBP) indicator for Highstock
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../../Core/Globals.js';
  14. import Point from '../../Core/Series/Point.js';
  15. import U from '../../Core/Utilities.js';
  16. var addEvent = U.addEvent, animObject = U.animObject, arrayMax = U.arrayMax, arrayMin = U.arrayMin, correctFloat = U.correctFloat, error = U.error, extend = U.extend, isArray = U.isArray, seriesType = U.seriesType;
  17. /* eslint-disable require-jsdoc */
  18. // Utils
  19. function arrayExtremesOHLC(data) {
  20. var dataLength = data.length, min = data[0][3], max = min, i = 1, currentPoint;
  21. for (; i < dataLength; i++) {
  22. currentPoint = data[i][3];
  23. if (currentPoint < min) {
  24. min = currentPoint;
  25. }
  26. if (currentPoint > max) {
  27. max = currentPoint;
  28. }
  29. }
  30. return {
  31. min: min,
  32. max: max
  33. };
  34. }
  35. /* eslint-enable require-jsdoc */
  36. var abs = Math.abs, noop = H.noop, columnPrototype = H.seriesTypes.column.prototype;
  37. /**
  38. * The Volume By Price (VBP) series type.
  39. *
  40. * @private
  41. * @class
  42. * @name Highcharts.seriesTypes.vbp
  43. *
  44. * @augments Highcharts.Series
  45. */
  46. seriesType('vbp', 'sma',
  47. /**
  48. * Volume By Price indicator.
  49. *
  50. * This series requires `linkedTo` option to be set.
  51. *
  52. * @sample stock/indicators/volume-by-price
  53. * Volume By Price indicator
  54. *
  55. * @extends plotOptions.sma
  56. * @since 6.0.0
  57. * @product highstock
  58. * @requires stock/indicators/indicators
  59. * @requires stock/indicators/volume-by-price
  60. * @optionparent plotOptions.vbp
  61. */
  62. {
  63. /**
  64. * @excluding index, period
  65. */
  66. params: {
  67. /**
  68. * The number of price zones.
  69. */
  70. ranges: 12,
  71. /**
  72. * The id of volume series which is mandatory. For example using
  73. * OHLC data, volumeSeriesID='volume' means the indicator will be
  74. * calculated using OHLC and volume values.
  75. */
  76. volumeSeriesID: 'volume'
  77. },
  78. /**
  79. * The styles for lines which determine price zones.
  80. */
  81. zoneLines: {
  82. /**
  83. * Enable/disable zone lines.
  84. */
  85. enabled: true,
  86. /**
  87. * Specify the style of zone lines.
  88. *
  89. * @type {Highcharts.CSSObject}
  90. * @default {"color": "#0A9AC9", "dashStyle": "LongDash", "lineWidth": 1}
  91. */
  92. styles: {
  93. /** @ignore-options */
  94. color: '#0A9AC9',
  95. /** @ignore-options */
  96. dashStyle: 'LongDash',
  97. /** @ignore-options */
  98. lineWidth: 1
  99. }
  100. },
  101. /**
  102. * The styles for bars when volume is divided into positive/negative.
  103. */
  104. volumeDivision: {
  105. /**
  106. * Option to control if volume is divided.
  107. */
  108. enabled: true,
  109. styles: {
  110. /**
  111. * Color of positive volume bars.
  112. *
  113. * @type {Highcharts.ColorString}
  114. */
  115. positiveColor: 'rgba(144, 237, 125, 0.8)',
  116. /**
  117. * Color of negative volume bars.
  118. *
  119. * @type {Highcharts.ColorString}
  120. */
  121. negativeColor: 'rgba(244, 91, 91, 0.8)'
  122. }
  123. },
  124. // To enable series animation; must be animationLimit > pointCount
  125. animationLimit: 1000,
  126. enableMouseTracking: false,
  127. pointPadding: 0,
  128. zIndex: -1,
  129. crisp: true,
  130. dataGrouping: {
  131. enabled: false
  132. },
  133. dataLabels: {
  134. allowOverlap: true,
  135. enabled: true,
  136. format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}',
  137. padding: 0,
  138. style: {
  139. /** @internal */
  140. fontSize: '7px'
  141. },
  142. verticalAlign: 'top'
  143. }
  144. },
  145. /**
  146. * @lends Highcharts.Series#
  147. */
  148. {
  149. nameBase: 'Volume by Price',
  150. bindTo: {
  151. series: false,
  152. eventName: 'afterSetExtremes'
  153. },
  154. calculateOn: 'render',
  155. markerAttribs: noop,
  156. drawGraph: noop,
  157. getColumnMetrics: columnPrototype.getColumnMetrics,
  158. crispCol: columnPrototype.crispCol,
  159. init: function (chart) {
  160. var indicator = this, params, baseSeries, volumeSeries;
  161. H.seriesTypes.sma.prototype.init.apply(indicator, arguments);
  162. params = indicator.options.params;
  163. baseSeries = indicator.linkedParent;
  164. volumeSeries = chart.get(params.volumeSeriesID);
  165. indicator.addCustomEvents(baseSeries, volumeSeries);
  166. return indicator;
  167. },
  168. // Adds events related with removing series
  169. addCustomEvents: function (baseSeries, volumeSeries) {
  170. var indicator = this;
  171. /* eslint-disable require-jsdoc */
  172. function toEmptyIndicator() {
  173. indicator.chart.redraw();
  174. indicator.setData([]);
  175. indicator.zoneStarts = [];
  176. if (indicator.zoneLinesSVG) {
  177. indicator.zoneLinesSVG.destroy();
  178. delete indicator.zoneLinesSVG;
  179. }
  180. }
  181. /* eslint-enable require-jsdoc */
  182. // If base series is deleted, indicator series data is filled with
  183. // an empty array
  184. indicator.dataEventsToUnbind.push(addEvent(baseSeries, 'remove', function () {
  185. toEmptyIndicator();
  186. }));
  187. // If volume series is deleted, indicator series data is filled with
  188. // an empty array
  189. if (volumeSeries) {
  190. indicator.dataEventsToUnbind.push(addEvent(volumeSeries, 'remove', function () {
  191. toEmptyIndicator();
  192. }));
  193. }
  194. return indicator;
  195. },
  196. // Initial animation
  197. animate: function (init) {
  198. var series = this, inverted = series.chart.inverted, group = series.group, attr = {}, translate, position;
  199. if (!init && group) {
  200. translate = inverted ? 'translateY' : 'translateX';
  201. position = inverted ? series.yAxis.top : series.xAxis.left;
  202. group['forceAnimate:' + translate] = true;
  203. attr[translate] = position;
  204. group.animate(attr, extend(animObject(series.options.animation), {
  205. step: function (val, fx) {
  206. series.group.attr({
  207. scaleX: Math.max(0.001, fx.pos)
  208. });
  209. }
  210. }));
  211. }
  212. },
  213. drawPoints: function () {
  214. var indicator = this;
  215. if (indicator.options.volumeDivision.enabled) {
  216. indicator.posNegVolume(true, true);
  217. columnPrototype.drawPoints.apply(indicator, arguments);
  218. indicator.posNegVolume(false, false);
  219. }
  220. columnPrototype.drawPoints.apply(indicator, arguments);
  221. },
  222. // Function responsible for dividing volume into positive and negative
  223. posNegVolume: function (initVol, pos) {
  224. var indicator = this, signOrder = pos ?
  225. ['positive', 'negative'] :
  226. ['negative', 'positive'], volumeDivision = indicator.options.volumeDivision, pointLength = indicator.points.length, posWidths = [], negWidths = [], i = 0, pointWidth, priceZone, wholeVol, point;
  227. if (initVol) {
  228. indicator.posWidths = posWidths;
  229. indicator.negWidths = negWidths;
  230. }
  231. else {
  232. posWidths = indicator.posWidths;
  233. negWidths = indicator.negWidths;
  234. }
  235. for (; i < pointLength; i++) {
  236. point = indicator.points[i];
  237. point[signOrder[0] + 'Graphic'] = point.graphic;
  238. point.graphic = point[signOrder[1] + 'Graphic'];
  239. if (initVol) {
  240. pointWidth = point.shapeArgs.width;
  241. priceZone = indicator.priceZones[i];
  242. wholeVol = priceZone.wholeVolumeData;
  243. if (wholeVol) {
  244. posWidths.push(pointWidth / wholeVol * priceZone.positiveVolumeData);
  245. negWidths.push(pointWidth / wholeVol * priceZone.negativeVolumeData);
  246. }
  247. else {
  248. posWidths.push(0);
  249. negWidths.push(0);
  250. }
  251. }
  252. point.color = pos ?
  253. volumeDivision.styles.positiveColor :
  254. volumeDivision.styles.negativeColor;
  255. point.shapeArgs.width = pos ?
  256. indicator.posWidths[i] :
  257. indicator.negWidths[i];
  258. point.shapeArgs.x = pos ?
  259. point.shapeArgs.x :
  260. indicator.posWidths[i];
  261. }
  262. },
  263. translate: function () {
  264. 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;
  265. columnPrototype.translate.apply(indicator);
  266. indicatorPoints = indicator.points;
  267. // Do translate operation when points exist
  268. if (indicatorPoints.length) {
  269. pointPadding = options.pointPadding < 0.5 ?
  270. options.pointPadding :
  271. 0.1;
  272. volumeDataArray = indicator.volumeDataArray;
  273. maxVolume = arrayMax(volumeDataArray);
  274. primalBarWidth = chart.plotWidth / 2;
  275. chartPlotTop = chart.plotTop;
  276. barHeight = abs(yAxis.toPixels(yAxisMin) -
  277. yAxis.toPixels(yAxisMin + indicator.rangeStep));
  278. oldBarHeight = abs(yAxis.toPixels(yAxisMin) -
  279. yAxis.toPixels(yAxisMin + indicator.rangeStep));
  280. if (pointPadding) {
  281. barHeightP = abs(barHeight * (1 - 2 * pointPadding));
  282. yBarOffset = abs((barHeight - barHeightP) / 2);
  283. barHeight = abs(barHeightP);
  284. }
  285. indicatorPoints.forEach(function (point, index) {
  286. barX = point.barX = point.plotX = 0;
  287. barY = point.plotY = (yAxis.toPixels(priceZones[index].start) -
  288. chartPlotTop -
  289. (yAxis.reversed ?
  290. (barHeight - oldBarHeight) :
  291. barHeight) -
  292. yBarOffset);
  293. barWidth = correctFloat(primalBarWidth *
  294. priceZones[index].wholeVolumeData / maxVolume);
  295. point.pointWidth = barWidth;
  296. point.shapeArgs = indicator.crispCol.apply(// eslint-disable-line no-useless-call
  297. indicator, [barX, barY, barWidth, barHeight]);
  298. point.volumeNeg = priceZones[index].negativeVolumeData;
  299. point.volumePos = priceZones[index].positiveVolumeData;
  300. point.volumeAll = priceZones[index].wholeVolumeData;
  301. });
  302. if (zoneLinesOptions.enabled) {
  303. indicator.drawZones(chart, yAxis, indicator.zoneStarts, zoneLinesOptions.styles);
  304. }
  305. }
  306. },
  307. getValues: function (series, params) {
  308. var indicator = this, xValues = series.processedXData, yValues = series.processedYData, chart = indicator.chart, ranges = params.ranges, VBP = [], xData = [], yData = [], isOHLC, volumeSeries, priceZones;
  309. // Checks if base series exists
  310. if (!series.chart) {
  311. error('Base series not found! In case it has been removed, add ' +
  312. 'a new one.', true, chart);
  313. return;
  314. }
  315. // Checks if volume series exists
  316. if (!(volumeSeries = (chart.get(params.volumeSeriesID)))) {
  317. error('Series ' +
  318. params.volumeSeriesID +
  319. ' not found! Check `volumeSeriesID`.', true, chart);
  320. return;
  321. }
  322. // Checks if series data fits the OHLC format
  323. isOHLC = isArray(yValues[0]);
  324. if (isOHLC && yValues[0].length !== 4) {
  325. error('Type of ' +
  326. series.name +
  327. ' series is different than line, OHLC or candlestick.', true, chart);
  328. return;
  329. }
  330. // Price zones contains all the information about the zones (index,
  331. // start, end, volumes, etc.)
  332. priceZones = indicator.priceZones = indicator.specifyZones(isOHLC, xValues, yValues, ranges, volumeSeries);
  333. priceZones.forEach(function (zone, index) {
  334. VBP.push([zone.x, zone.end]);
  335. xData.push(VBP[index][0]);
  336. yData.push(VBP[index][1]);
  337. });
  338. return {
  339. values: VBP,
  340. xData: xData,
  341. yData: yData
  342. };
  343. },
  344. // Specifing where each zone should start ans end
  345. specifyZones: function (isOHLC, xValues, yValues, ranges, volumeSeries) {
  346. var indicator = this, rangeExtremes = (isOHLC ? arrayExtremesOHLC(yValues) : false), lowRange = rangeExtremes ?
  347. rangeExtremes.min :
  348. arrayMin(yValues), highRange = rangeExtremes ?
  349. rangeExtremes.max :
  350. arrayMax(yValues), zoneStarts = indicator.zoneStarts = [], priceZones = [], i = 0, j = 1, rangeStep, zoneStartsLength;
  351. if (!lowRange || !highRange) {
  352. if (this.points.length) {
  353. this.setData([]);
  354. this.zoneStarts = [];
  355. this.zoneLinesSVG.destroy();
  356. }
  357. return [];
  358. }
  359. rangeStep = indicator.rangeStep =
  360. correctFloat(highRange - lowRange) / ranges;
  361. zoneStarts.push(lowRange);
  362. for (; i < ranges - 1; i++) {
  363. zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep));
  364. }
  365. zoneStarts.push(highRange);
  366. zoneStartsLength = zoneStarts.length;
  367. // Creating zones
  368. for (; j < zoneStartsLength; j++) {
  369. priceZones.push({
  370. index: j - 1,
  371. x: xValues[0],
  372. start: zoneStarts[j - 1],
  373. end: zoneStarts[j]
  374. });
  375. }
  376. return indicator.volumePerZone(isOHLC, priceZones, volumeSeries, xValues, yValues);
  377. },
  378. // Calculating sum of volume values for a specific zone
  379. volumePerZone: function (isOHLC, priceZones, volumeSeries, xValues, yValues) {
  380. var indicator = this, volumeXData = volumeSeries.processedXData, volumeYData = volumeSeries.processedYData, lastZoneIndex = priceZones.length - 1, baseSeriesLength = yValues.length, volumeSeriesLength = volumeYData.length, previousValue, startFlag, endFlag, value, i;
  381. // Checks if each point has a corresponding volume value
  382. if (abs(baseSeriesLength - volumeSeriesLength)) {
  383. // If the first point don't have volume, add 0 value at the
  384. // beggining of the volume array
  385. if (xValues[0] !== volumeXData[0]) {
  386. volumeYData.unshift(0);
  387. }
  388. // If the last point don't have volume, add 0 value at the end
  389. // of the volume array
  390. if (xValues[baseSeriesLength - 1] !==
  391. volumeXData[volumeSeriesLength - 1]) {
  392. volumeYData.push(0);
  393. }
  394. }
  395. indicator.volumeDataArray = [];
  396. priceZones.forEach(function (zone) {
  397. zone.wholeVolumeData = 0;
  398. zone.positiveVolumeData = 0;
  399. zone.negativeVolumeData = 0;
  400. for (i = 0; i < baseSeriesLength; i++) {
  401. startFlag = false;
  402. endFlag = false;
  403. value = isOHLC ? yValues[i][3] : yValues[i];
  404. previousValue = i ?
  405. (isOHLC ?
  406. yValues[i - 1][3] :
  407. yValues[i - 1]) :
  408. value;
  409. // Checks if this is the point with the
  410. // lowest close value and if so, adds it calculations
  411. if (value <= zone.start && zone.index === 0) {
  412. startFlag = true;
  413. }
  414. // Checks if this is the point with the highest
  415. // close value and if so, adds it calculations
  416. if (value >= zone.end && zone.index === lastZoneIndex) {
  417. endFlag = true;
  418. }
  419. if ((value > zone.start || startFlag) &&
  420. (value < zone.end || endFlag)) {
  421. zone.wholeVolumeData += volumeYData[i];
  422. if (previousValue > value) {
  423. zone.negativeVolumeData += volumeYData[i];
  424. }
  425. else {
  426. zone.positiveVolumeData += volumeYData[i];
  427. }
  428. }
  429. }
  430. indicator.volumeDataArray.push(zone.wholeVolumeData);
  431. });
  432. return priceZones;
  433. },
  434. // Function responsoble for drawing additional lines indicating zones
  435. drawZones: function (chart, yAxis, zonesValues, zonesStyles) {
  436. var indicator = this, renderer = chart.renderer, zoneLinesSVG = indicator.zoneLinesSVG, zoneLinesPath = [], leftLinePos = 0, rightLinePos = chart.plotWidth, verticalOffset = chart.plotTop, verticalLinePos;
  437. zonesValues.forEach(function (value) {
  438. verticalLinePos = yAxis.toPixels(value) - verticalOffset;
  439. zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([[
  440. 'M',
  441. leftLinePos,
  442. verticalLinePos
  443. ], [
  444. 'L',
  445. rightLinePos,
  446. verticalLinePos
  447. ]], zonesStyles.lineWidth));
  448. });
  449. // Create zone lines one path or update it while animating
  450. if (zoneLinesSVG) {
  451. zoneLinesSVG.animate({
  452. d: zoneLinesPath
  453. });
  454. }
  455. else {
  456. zoneLinesSVG = indicator.zoneLinesSVG =
  457. renderer.path(zoneLinesPath).attr({
  458. 'stroke-width': zonesStyles.lineWidth,
  459. 'stroke': zonesStyles.color,
  460. 'dashstyle': zonesStyles.dashStyle,
  461. 'zIndex': indicator.group.zIndex + 0.1
  462. })
  463. .add(indicator.group);
  464. }
  465. }
  466. },
  467. /**
  468. * @lends Highcharts.Point#
  469. */
  470. {
  471. // Required for destroying negative part of volume
  472. destroy: function () {
  473. // @todo: this.negativeGraphic doesn't seem to be used anywhere
  474. if (this.negativeGraphic) {
  475. this.negativeGraphic = this.negativeGraphic.destroy();
  476. }
  477. return Point.prototype.destroy.apply(this, arguments);
  478. }
  479. });
  480. /**
  481. * A `Volume By Price (VBP)` series. If the [type](#series.vbp.type) option is
  482. * not specified, it is inherited from [chart.type](#chart.type).
  483. *
  484. * @extends series,plotOptions.vbp
  485. * @since 6.0.0
  486. * @product highstock
  487. * @excluding dataParser, dataURL
  488. * @requires stock/indicators/indicators
  489. * @requires stock/indicators/volume-by-price
  490. * @apioption series.vbp
  491. */
  492. ''; // to include the above in the js output