volume-by-price.src.js 25 KB

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