volume-by-price.src.js 18 KB

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