Skip to content

Commit

Permalink
Feature #15138, added test and algorithm for OBV.
Browse files Browse the repository at this point in the history
  • Loading branch information
karolkolodziej committed Mar 18, 2021
1 parent d93b70d commit acb0796
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 0 deletions.
6 changes: 6 additions & 0 deletions 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
...
9 changes: 9 additions & 0 deletions samples/unit-tests/indicator-obv/recalculations/demo.html
@@ -0,0 +1,9 @@
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/indicators/indicators.js"></script>
<script src="https://code.highcharts.com/stock/indicators/obv.js"></script>

<div id="qunit"></div>
<div id="qunit-fixture"></div>


<div id="container" style="width: 600px; margin: 0 auto;"></div>
79 changes: 79 additions & 0 deletions 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'
);
});
1 change: 1 addition & 0 deletions test/ts-node-unit-tests/tests/modules.test.ts
Expand Up @@ -240,6 +240,7 @@ export function testStockIndicators() {
'mfi',
'momentum',
'natr',
'obv',
'pivotpoints',
'ppo',
'pc',
Expand Down
227 changes: 227 additions & 0 deletions 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<OBVPoint> = void 0 as any;
public points: Array<OBVPoint> = void 0 as any;
public options: OBVOptions = void 0 as any;

/* *
*
* Functions
*
* */

public getCloseValues(
yVal: Array<number> | Array<Array<number>>
): Array<number> {
const index: number = 3; // take close value
let values: Array<number>;

if (isNumber(yVal[0])) {
// For line series.
values = yVal as Array<number>;
} else {
// For OHLC series.
values = (yVal as Array<Array<number>>).map((value: Array<number>): 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<TLinkedSeries extends LineSeries>(
series: TLinkedSeries,
params: OBVParamsOptions
): (IndicatorValuesObject<TLinkedSeries>|undefined) {
const volumeSeries = series.chart.get(params.volumeSeriesID as string),
xVal: Array<number> = (series.xData as any),
yVal: Array<number> | Array<Array<number>> = (series.yData as any),
OBV: Array<Array<number>> = [],
xData: Array<number> = [],
yData: Array<number> = [];

let OBVPoint: Array<number> = [],
i: number = 0,
previousOBV: number = 0,
curentOBV: number = 0,
previousClose: number = 0,
curentClose: number = 0,
volume: Array<number>,
closeValues: Array<number>,
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<TLinkedSeries>;
}
}

/* *
*
* 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
35 changes: 35 additions & 0 deletions 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;

0 comments on commit acb0796

Please sign in to comment.