volume-by-price.src.js 24 KB

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