diff --git a/samples/unit-tests/indicator-obv/recalculations/demo.details b/samples/unit-tests/indicator-obv/recalculations/demo.details
new file mode 100644
index 00000000000..a86ee3a2f30
--- /dev/null
+++ b/samples/unit-tests/indicator-obv/recalculations/demo.details
@@ -0,0 +1,6 @@
+---
+ resources:
+ - https://code.jquery.com/qunit/qunit-2.0.1.js
+ - https://code.jquery.com/qunit/qunit-2.0.1.css
+ js_wrap: b
+...
\ No newline at end of file
diff --git a/samples/unit-tests/indicator-obv/recalculations/demo.html b/samples/unit-tests/indicator-obv/recalculations/demo.html
new file mode 100644
index 00000000000..76f838cf056
--- /dev/null
+++ b/samples/unit-tests/indicator-obv/recalculations/demo.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/unit-tests/indicator-obv/recalculations/demo.js b/samples/unit-tests/indicator-obv/recalculations/demo.js
new file mode 100644
index 00000000000..93aac177952
--- /dev/null
+++ b/samples/unit-tests/indicator-obv/recalculations/demo.js
@@ -0,0 +1,79 @@
+QUnit.test('Test RSI calculations on data updates.', function (assert) {
+ const chart = Highcharts.stockChart('container', {
+ yAxis: [{
+ height: '60%'
+ }, {
+ height: '20%',
+ top: '60%'
+ }, {
+ height: '20%',
+ top: '80%'
+ }],
+ series: [{
+ id: 'main',
+ data: [
+ [1552311000000, 10],
+ [1552397400000, 10.15],
+ [1552483800000, 10.17],
+ [1552570200000, 10.13],
+ [1552656600000, 10.11],
+ [1552915800000, 10.15],
+ [1553002200000, 10.20],
+ [1553088600000, 10.20],
+ [1553175000000, 10.22]
+ ]
+ }, {
+ type: 'column',
+ id: 'volume',
+ yAxis: 1,
+ data: [
+ [1552311000000, 25200],
+ [1552397400000, 30000],
+ [1552483800000, 25600],
+ [1552570200000, 32000],
+ [1552656600000, 23000],
+ [1552915800000, 40000],
+ [1553002200000, 36000],
+ [1553088600000, 20500],
+ [1553175000000, 23000]
+ ]
+ }, {
+ volumeSeriesID: 'volume',
+ type: 'obv',
+ linkedTo: 'main',
+ yAxis: 2
+ }]
+ });
+
+ assert.strictEqual(
+ chart.series[0].points.length,
+ chart.series[1].points.length,
+ 'OBV should have the same amount of points as the main series.'
+ );
+
+ chart.series[0].addPoint([1553261400000, 10.21], false);
+ chart.series[1].addPoint([1553261400000, 27500]);
+
+ assert.strictEqual(
+ chart.series[0].points.length,
+ chart.series[1].points.length,
+ 'OBV should have the same amount of points as the main series after adding point.'
+ );
+
+ assert.deepEqual(
+ chart.series[2].yData,
+ [
+ 0,
+ 30000,
+ 55600,
+ 23600,
+ 600,
+ 40600,
+ 76600,
+ 76600,
+ 99600,
+ 72100
+ ],
+ 'Correct values'
+ );
+});
diff --git a/test/ts-node-unit-tests/tests/modules.test.ts b/test/ts-node-unit-tests/tests/modules.test.ts
index fcf47badab8..56f0b284552 100644
--- a/test/ts-node-unit-tests/tests/modules.test.ts
+++ b/test/ts-node-unit-tests/tests/modules.test.ts
@@ -240,6 +240,7 @@ export function testStockIndicators() {
'mfi',
'momentum',
'natr',
+ 'obv',
'pivotpoints',
'ppo',
'pc',
diff --git a/ts/Stock/Indicators/OBV/OBVIndicator.ts b/ts/Stock/Indicators/OBV/OBVIndicator.ts
new file mode 100644
index 00000000000..0793775f111
--- /dev/null
+++ b/ts/Stock/Indicators/OBV/OBVIndicator.ts
@@ -0,0 +1,227 @@
+/* *
+ *
+ * License: www.highcharts.com/license
+ *
+ * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
+ *
+ * */
+
+'use strict';
+
+import type {
+ OBVOptions,
+ OBVParamsOptions
+} from './OBVOptions';
+import type OBVPoint from './OBVPoint';
+import type IndicatorValuesObject from '../IndicatorValuesObject';
+import type LineSeries from '../../../Series/Line/LineSeries';
+import SeriesRegistry from '../../../Core/Series/SeriesRegistry.js';
+import Series from '../../../Core/Series/Series';
+const {
+ seriesTypes: {
+ sma: SMAIndicator
+ }
+} = SeriesRegistry;
+import U from '../../../Core/Utilities.js';
+const {
+ isNumber,
+ merge
+} = U;
+
+/**
+ * The OBV series type.
+ *
+ * @private
+ * @class
+ * @name Highcharts.seriesTypes.obv
+ *
+ * @augments Highcharts.Series
+ */
+class OBVIndicator extends SMAIndicator {
+ /**
+ * On-Balance Volume (OBV) technical indicator. This series
+ * requires the `linkedTo` option to be set and should be loaded after
+ * the `stock/indicators/indicators.js` file.
+ *
+ * @sample stock/indicators/obv
+ * OBV indicator
+ *
+ * @extends plotOptions.sma
+ * @since next
+ * @product highstock
+ * @requires stock/indicators/indicators
+ * @requires stock/indicators/obv
+ * @optionparent plotOptions.obv
+ */
+ public static defaultOptions: OBVOptions = merge(SMAIndicator.defaultOptions, {
+ params: {
+ volumeSeriesID: 'volume'
+ }
+ } as OBVOptions);
+
+ /* *
+ *
+ * Properties
+ *
+ * */
+
+ public data: Array = void 0 as any;
+ public points: Array = void 0 as any;
+ public options: OBVOptions = void 0 as any;
+
+ /* *
+ *
+ * Functions
+ *
+ * */
+
+ public getCloseValues(
+ yVal: Array | Array>
+ ): Array {
+ const index: number = 3; // take close value
+ let values: Array;
+
+ if (isNumber(yVal[0])) {
+ // For line series.
+ values = yVal as Array;
+ } else {
+ // For OHLC series.
+ values = (yVal as Array>).map((value: Array): number => value[index]);
+ }
+
+ return values;
+ }
+
+ public getTrend(
+ curentClose: number,
+ previousClose: number
+ ): number {
+ let trend: number = void 0 as any;
+
+ if (curentClose > previousClose) {
+ trend = 1; // up
+ }
+ if (curentClose === previousClose) {
+ trend = 0; // constant
+ }
+ if (curentClose < previousClose) {
+ trend = -1; // down
+ }
+
+ return trend;
+ }
+
+ public getValues(
+ series: TLinkedSeries,
+ params: OBVParamsOptions
+ ): (IndicatorValuesObject|undefined) {
+ const volumeSeries = series.chart.get(params.volumeSeriesID as string),
+ xVal: Array = (series.xData as any),
+ yVal: Array | Array> = (series.yData as any),
+ OBV: Array> = [],
+ xData: Array = [],
+ yData: Array = [];
+
+ let OBVPoint: Array = [],
+ i: number = 0,
+ previousOBV: number = 0,
+ curentOBV: number = 0,
+ previousClose: number = 0,
+ curentClose: number = 0,
+ volume: Array,
+ closeValues: Array,
+ trend: number;
+
+ // Checks if volume series exists.
+ if (volumeSeries) {
+ closeValues = this.getCloseValues(yVal);
+ volume = ((volumeSeries as Series).yData as any);
+
+ for (i; i < closeValues.length; i++) {
+ // Add first point and qet close value.
+ if (i === 0) {
+ OBVPoint = [xVal[i], previousOBV];
+ previousClose = closeValues[i];
+ } else {
+ curentClose = closeValues[i];
+ trend = this.getTrend(curentClose, previousClose);
+
+ if (trend === 1) {
+ curentOBV = previousOBV + volume[i];
+ }
+ if (trend === 0) {
+ curentOBV = previousOBV;
+ }
+ if (trend === -1) {
+ curentOBV = previousOBV - volume[i];
+ }
+
+ // Add point.
+ OBVPoint = [xVal[i], curentOBV];
+
+ // Asing currend as previous for next iteration
+ previousOBV = curentOBV;
+ previousClose = curentClose;
+ }
+
+ OBV.push(OBVPoint);
+ xData.push(xVal[i]);
+ yData.push(OBVPoint[1]);
+ }
+ } else {
+ return;
+ }
+
+ return {
+ values: OBV,
+ xData: xData,
+ yData: yData
+ } as IndicatorValuesObject;
+ }
+}
+
+/* *
+ *
+ * Prototype Properties
+ *
+ * */
+
+interface OBVIndicator {
+ pointClass: typeof OBVPoint;
+}
+
+/* *
+ *
+ * Registry
+ *
+ * */
+declare module '../../../Core/Series/SeriesType' {
+ interface SeriesTypeRegistry {
+ obv: typeof OBVIndicator;
+ }
+}
+
+SeriesRegistry.registerSeriesType('obv', OBVIndicator);
+
+/* *
+ *
+ * Default Export
+ *
+ * */
+
+export default OBVIndicator;
+
+/**
+ * A `OBV` series. If the [type](#series.obv.type) option is not
+ * specified, it is inherited from [chart.type](#chart.type).
+ *
+ * @extends series,plotOptions.obv
+ * @since next
+ * @product highstock
+ * @excluding dataParser, dataURL
+ * @requires stock/indicators/indicators
+ * @requires stock/indicators/obv
+ * @apioption series.obv
+ */
+
+''; // to include the above in the js output
diff --git a/ts/Stock/Indicators/OBV/OBVOptions.d.ts b/ts/Stock/Indicators/OBV/OBVOptions.d.ts
new file mode 100644
index 00000000000..49fd14a5410
--- /dev/null
+++ b/ts/Stock/Indicators/OBV/OBVOptions.d.ts
@@ -0,0 +1,35 @@
+/* *
+ *
+ * License: www.highcharts.com/license
+ *
+ * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
+ *
+ * */
+
+/* *
+ *
+ * Imports
+ *
+ * */
+
+import type {
+ SMAOptions,
+ SMAParamsOptions
+} from '../SMA/SMAOptions';
+
+/* *
+ *
+ * Declarations
+ *
+ * */
+
+export interface OBVOptions extends SMAOptions {
+ params?: OBVParamsOptions;
+}
+
+export interface OBVParamsOptions extends SMAParamsOptions {
+ // for inheritance
+ volumeSeriesID?: string;
+}
+
+export default OBVOptions;
diff --git a/ts/Stock/Indicators/OBV/OBVPoint.d.ts b/ts/Stock/Indicators/OBV/OBVPoint.d.ts
new file mode 100644
index 00000000000..2fc2e729cc0
--- /dev/null
+++ b/ts/Stock/Indicators/OBV/OBVPoint.d.ts
@@ -0,0 +1,34 @@
+/* *
+ *
+ * License: www.highcharts.com/license
+ *
+ * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
+ *
+ * */
+
+/* *
+ *
+ * Imports
+ *
+ * */
+
+import type OBVIndicator from './OBVIndicator';
+import type SMAPoint from '../SMA/SMAPoint';
+
+/* *
+ *
+ * Class
+ *
+ * */
+
+declare class OBVPoint extends SMAPoint {
+ public series: OBVIndicator;
+}
+
+/* *
+ *
+ * Default Export
+ *
+ * */
+
+export default OBVPoint;
diff --git a/ts/masters/indicators/indicators-all.src.ts b/ts/masters/indicators/indicators-all.src.ts
index 41fb4f88d62..eb4d51d18ab 100644
--- a/ts/masters/indicators/indicators-all.src.ts
+++ b/ts/masters/indicators/indicators-all.src.ts
@@ -34,6 +34,7 @@ import '../../Stock/Indicators/MACD/MACDIndicator.js';
import '../../Stock/Indicators/MFI/MFIIndicator.js';
import '../../Stock/Indicators/Momentum/MomentumIndicator.js';
import '../../Stock/Indicators/NATR/NATRIndicator.js';
+import '../../Stock/Indicators/OBV/OBVIndicator.js';
import '../../Stock/Indicators/PivotPoints/PivotPointsIndicator.js';
import '../../Stock/Indicators/PPO/PPOIndicator.js';
import '../../Stock/Indicators/PC/PCIndicator.js';
diff --git a/ts/masters/indicators/obv.src.ts b/ts/masters/indicators/obv.src.ts
new file mode 100644
index 00000000000..e4de90617f2
--- /dev/null
+++ b/ts/masters/indicators/obv.src.ts
@@ -0,0 +1,14 @@
+/**
+ * @license Highstock JS v@product.version@ (@product.date@)
+ * @module highcharts/indicators/obv
+ * @requires highcharts
+ * @requires highcharts/modules/stock
+ *
+ * Indicator series type for Highstock
+ *
+ * (c) 2010-2021 Karol Kolodziej
+ *
+ * License: www.highcharts.com/license
+ */
+'use strict';
+import '../../Stock/Indicators/OBV/OBVIndicator.js';