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';