Skip to content

Commit

Permalink
feat(CCI,MAD): Add Commodity Channel Index (CCI) & Mean Absolute Devi…
Browse files Browse the repository at this point in the history
…ation (MAD) (#355)
  • Loading branch information
bennycode authored Nov 11, 2021
1 parent 6008472 commit 754398f
Show file tree
Hide file tree
Showing 30 changed files with 350 additions and 105 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/merge-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node-version: [12.x]
node-version: [16.x]
if: github.actor == 'dependabot[bot]'
steps:
- name: 'Checkout repository'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
node-version: [16.x]
steps:
- name: Checkout repository
uses: actions/checkout@v2
Expand All @@ -32,7 +32,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
yml: ./codecov.yml
- name: Release
uses: xt0rted/action-gh-release@testing
uses: softprops/action-gh-release@0465cdad11d833cabb6414f885edd2ccdfda4b96
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: true
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ Technical indicators and overlays to run technical analysis with JavaScript / Ty

The "trading-signals" library provides a TypeScript implementation for common technical indicators with arbitrary-precision decimal arithmetic.

The main focus of this library is on the accuracy of calculations, but using the [faster implementations](./README.md#faster-implementations) it is also suitable for calculations where performance is important.
The main focus of this library is on the accuracy of calculations, but using the [fast implementations](#fast-implementations) it is also suitable for calculations where performance is important.

## Features
## Benefits & Features

- **Accurate.** Don't rely on type `number` and its precision limits. Use [Big][1].
- **Typed.** Source code is 100% TypeScript. No need to install external typings.
- **Accurate.** Indicators with intervals will return a result only when the period is reached.
- **Convenient.** Indicators with intervals will save their all-time highs and lows.
- **Fast.** If you need high throughput, you can use the [fast implementations](#fast-implementations).
- **Precise.** Better accuracy than calculating with numbers thanks to [big.js][1].
- **Tested.** Code coverage is 100%. No surprises when using it.
- **Typed.** Source code is 100% TypeScript. No need to install external typings.
- **Verified.** All results are verified with [other libraries](#alternatives) to guarantee correctness.

## Technical Indicator Types

Expand All @@ -32,9 +36,11 @@ The main focus of this library is on the accuracy of calculations, but using the
1. Awesome Oscillator (AO)
1. Bollinger Bands (BBANDS)
1. Center of Gravity (CG)
1. Commodity Channel Index (CCI)
1. Double Exponential Moving Average (DEMA)
1. Dual Moving Average (DMA)
1. Exponential Moving Average (EMA)
1. Mean Absolute Deviation (MAD)
1. Momentum (MOM)
1. Moving Average Convergence Divergence (MACD)
1. Rate-of-Change (ROC)
Expand Down Expand Up @@ -120,7 +126,7 @@ JavaScript is very bad with numbers. When calculating `0.1 + 0.2` it shows you `

As specified by the ECMAScript standard, all arithmetic in JavaScript uses [double-precision floating-point arithmetic](https://en.wikipedia.org/wiki/Double-precision_floating-point_format), which is only accurate until certain extent. To increase the accuracy and avoid miscalculations, the [trading-signals](https://github.com/bennycode/trading-signals) library uses [big.js][1] which offers arbitrary-precision decimal arithmetic. However, this arbitrary accuracy comes with a downside: Calculations with it are not as performant as with the primitive data type `number`.

### Faster implementations
### Fast implementations

To get the best of both worlds (high accuracy & high performance), you will find two implementations of each indicator (e.g. `SMA` & `FasterSMA`). The standard implementation uses big.js and the `Faster`-prefixed version uses common `number` types. Use the standard one when you need high accuracy and use the `Faster`-one when you need high performance.

Expand Down
2 changes: 1 addition & 1 deletion src/AC/AC.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('AC', () => {
describe('getResult', () => {
it('works with a signal line of SMA(5)', () => {
const ac = new AC(5, 34, 5);
// Test data taken from:
// Test data verified with:
// https://github.com/jesse-ai/jesse/blob/8e502d070c24bed29db80e1d0938781d8cdb1046/tests/data/test_candles_indicators.py#L4351
const candles = [
[1563408000000, 210.8, 225.73, 229.65, 205.71, 609081.49094],
Expand Down
9 changes: 0 additions & 9 deletions src/AC/AC.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {BigIndicatorSeries} from '../Indicator';
import Big, {BigSource} from 'big.js';
import {NotEnoughDataError} from '../error';
import {AO} from '../AO/AO';
import {SMA} from '../SMA/SMA';
import {MOM} from '../MOM/MOM';
Expand Down Expand Up @@ -33,14 +32,6 @@ export class AC extends BigIndicatorSeries {
return this.result !== undefined;
}

override getResult(): Big {
if (!this.result) {
throw new NotEnoughDataError();
}

return this.result;
}

override update(low: BigSource, high: BigSource): void | Big {
const ao = this.ao.update(low, high);
if (ao) {
Expand Down
2 changes: 1 addition & 1 deletion src/ADX/ADX.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('ADX', () => {
});

it('supports different smoothing indicators', () => {
// Test data taken from:
// Test data verified with:
// https://github.com/TulipCharts/tulipindicators/commit/41e59fb33cef5bc97b03d2751dab2b006525c23f
const highs = [
148.115, 148.226, 148.027, 149.603, 149.739, 150.782, 151.247, 151.084, 151.855, 151.874, 150.977, 151.693,
Expand Down
2 changes: 1 addition & 1 deletion src/AO/AO.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {NotEnoughDataError} from '../error';
describe('AO', () => {
describe('getResult', () => {
it('works with an interval setting of 5/34', () => {
// Test data taken from:
// Test data verified with:
// https://github.com/TulipCharts/tulipindicators/blob/v0.8.0/tests/extra.txt#L17-L20
const highs = [
32.11, 27.62, 28.26, 28.02, 26.93, 26.65, 27.25, 27.58, 27.9, 28.9, 29.34, 29.82, 29.54, 29.3, 29.5, 29.5, 29.7,
Expand Down
17 changes: 4 additions & 13 deletions src/AO/AO.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {BigIndicatorSeries} from '../Indicator';
import Big, {BigSource} from 'big.js';
import {SMA} from '../SMA/SMA';
import {NotEnoughDataError} from '../error';

/**
* Awesome Oscillator (AO)
Expand All @@ -10,8 +9,9 @@ import {NotEnoughDataError} from '../error';
* The Awesome Oscillator (AO) is an indicator used to measure market momentum.
* It has been developed by the technical analyst and charting enthusiast Bill Williams.
*
* When AO crosses above Zero, short term momentum is rising faster than long term momentum which signals a bullish buying opportunity.
* When AO crosses below Zero, short term momentum is falling faster then the long term momentum which signals a bearish selling opportunity.
* When AO crosses above Zero, short term momentum is rising faster than long term momentum which signals a bullish
* buying opportunity. When AO crosses below Zero, short term momentum is falling faster then the long term momentum
* which signals a bearish selling opportunity.
*
* @see https://www.tradingview.com/support/solutions/43000501826-awesome-oscillator-ao/
* @see https://tradingstrategyguides.com/bill-williams-awesome-oscillator-strategy/
Expand All @@ -30,14 +30,6 @@ export class AO extends BigIndicatorSeries {
return this.result !== undefined;
}

override getResult(): Big {
if (!this.result) {
throw new NotEnoughDataError();
}

return this.result;
}

override update(low: BigSource, high: BigSource): void | Big {
const candleSum = new Big(low).add(high);
const medianPrice = candleSum.div(2);
Expand All @@ -46,8 +38,7 @@ export class AO extends BigIndicatorSeries {
this.long.update(medianPrice);

if (this.short.isStable && this.long.isStable) {
const result = this.setResult(this.short.getResult().sub(this.long.getResult()));
return result;
return this.setResult(this.short.getResult().sub(this.long.getResult()));
}
}
}
8 changes: 0 additions & 8 deletions src/ATR/ATR.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Big from 'big.js';
import {NotEnoughDataError} from '../error';
import {BigIndicatorSeries} from '../Indicator';
import {MovingAverage} from '../MA/MovingAverage';
import {MovingAverageTypeContext} from '../MA/MovingAverageTypeContext';
Expand Down Expand Up @@ -56,13 +55,6 @@ export class ATR extends BigIndicatorSeries {
}
}

override getResult(): Big {
if (!this.result) {
throw new NotEnoughDataError();
}
return this.result;
}

private trueRange(prevCandle: HighLowClose, currentCandle: HighLowClose): Big {
const prevClose = new Big(prevCandle.close);
const low = new Big(currentCandle.low);
Expand Down
4 changes: 2 additions & 2 deletions src/BBANDS/BollingerBands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('BollingerBands', () => {
});

it('is compatible with results from Tulip Indicators (TI)', () => {
// Test data taken from:
// Test data verified with:
// https://tulipindicators.org/bbands
const inputs = [
81.59, 81.06, 82.87, 83.0, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36, 85.53, 86.54, 86.89, 87.77, 87.29,
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('BollingerBands', () => {
describe('FasterBollingerBands', () => {
describe('getResult', () => {
it('only works with plain numbers', () => {
// Test data taken from:
// Test data verified with:
// https://tulipindicators.org/bbands
const prices = [
81.59, 81.06, 82.87, 83.0, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36, 85.53, 86.54, 86.89, 87.77, 87.29,
Expand Down
50 changes: 50 additions & 0 deletions src/CCI/CCI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {CCI} from './CCI';
import {NotEnoughDataError} from '../error';

describe('CCI', () => {
// Test data verified with:
// https://tulipindicators.org/cci
const candles = [
{close: 83.61, high: 83.85, low: 83.07},
{close: 83.15, high: 83.9, low: 83.11},
{close: 82.84, high: 83.33, low: 82.49},
{close: 83.99, high: 84.3, low: 82.3},
{close: 84.55, high: 84.84, low: 84.15},
{close: 84.36, high: 85.0, low: 84.11},
{close: 85.53, high: 85.9, low: 84.03},
{close: 86.54, high: 86.58, low: 85.39},
{close: 86.89, high: 86.98, low: 85.76},
{close: 87.77, high: 88.0, low: 87.17},
{close: 87.29, high: 87.87, low: 87.01},
];
let expectations: string[] = [];

beforeEach(() => {
expectations = ['166.67', '82.02', '95.50', '130.91', '99.16', '116.34', '71.93'];
});

describe('getResult', () => {
it('calculates the Commodity Channel Index (CCI)', () => {
const cci = new CCI(5);
for (const candle of candles) {
cci.update(candle);
if (cci.isStable) {
const expected = expectations.shift();
expect(cci.getResult().toFixed(2)).toBe(expected!);
}
}
const actual = cci.getResult().toFixed(2);
expect(actual).toBe('71.93');
});

it('throws an error when there is not enough input data', () => {
const cci = new CCI(5);
try {
cci.getResult();
fail('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}
});
});
});
52 changes: 52 additions & 0 deletions src/CCI/CCI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {BigIndicatorSeries} from '../Indicator';
import {Big, BigSource} from 'big.js';
import {HighLowClose} from '../util';
import {SMA} from '../SMA/SMA';
import {MAD} from '../MAD/MAD';

/**
* Commodity Channel Index (CCI)
* Type: Momentum
*
* The Commodity Channel Index (CCI), developed by Donald Lambert in 1980, compares the current mean price with the average mean price over a period of time. Approximately 70 to 80 percent of CCI values are between −100 and +100, which makes it an oscillator. Values above +100 imply an overbought condition, while values below −100 imply an oversold condition.
*
* According to [Investopia.com](https://www.investopedia.com/articles/active-trading/031914/how-traders-can-utilize-cci-commodity-channel-index-trade-stock-trends.asp#multiple-timeframe-cci-strategy), traders often buy when the CCI dips below -100 and then rallies back above -100 to sell the security when it moves above +100 and then drops back below +100.
*
* @see https://en.wikipedia.org/wiki/Commodity_channel_index
*/
export class CCI extends BigIndicatorSeries {
public readonly prices: BigSource[] = [];
protected result?: Big;
private readonly sma: SMA;
private readonly typicalPrices: Big[] = [];

constructor(public readonly interval: number) {
super();
this.sma = new SMA(this.interval);
}

get isStable(): boolean {
return this.result !== undefined;
}

update(candle: HighLowClose): void | Big {
const typicalPrice = this.cacheTypicalPrice(candle);
this.sma.update(typicalPrice);
if (this.sma.isStable) {
const mean = this.sma.getResult();
const meanDeviation = MAD.getResultFromBatch(this.typicalPrices, mean);
const a = typicalPrice.minus(mean);
const b = new Big(0.015).mul(meanDeviation);
return this.setResult(a.div(b));
}
}

private cacheTypicalPrice({high, low, close}: HighLowClose): Big {
const typicalPrice = new Big(high).plus(low).plus(close).div(3);
this.typicalPrices.push(typicalPrice);
if (this.typicalPrices.length > this.interval) {
this.typicalPrices.shift();
}
return typicalPrice;
}
}
9 changes: 0 additions & 9 deletions src/CG/CG.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {BigIndicatorSeries} from '../Indicator';
import Big, {BigSource} from 'big.js';
import {SMA} from '../SMA/SMA';
import {NotEnoughDataError} from '../error';

/**
* Center of Gravity (CG)
Expand Down Expand Up @@ -52,12 +51,4 @@ export class CG extends BigIndicatorSeries {

return this.setResult(cg);
}

override getResult(): Big {
if (!this.isStable || !this.result) {
throw new NotEnoughDataError();
}

return this.result;
}
}
10 changes: 1 addition & 9 deletions src/DEMA/DEMA.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Big, {BigSource} from 'big.js';
import {EMA, NotEnoughDataError} from '..';
import {EMA} from '..';
import {BigIndicatorSeries} from '../Indicator';

/**
Expand Down Expand Up @@ -37,12 +37,4 @@ export class DEMA extends BigIndicatorSeries {
return false;
}
}

override getResult(): Big {
if (!this.result) {
throw new NotEnoughDataError();
}

return this.result;
}
}
19 changes: 16 additions & 3 deletions src/Indicator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Big from 'big.js';
import {NotEnoughDataError} from './error';

export interface Indicator<T = Big> {
getResult(): T;
Expand Down Expand Up @@ -30,7 +31,13 @@ export abstract class BigIndicatorSeries implements IndicatorSeries {

abstract isStable: boolean;

abstract getResult(): Big;
getResult(): Big {
if (!this.result) {
throw new NotEnoughDataError();
}

return this.result;
}

protected setResult(value: Big): Big {
this.result = value;
Expand All @@ -46,7 +53,7 @@ export abstract class BigIndicatorSeries implements IndicatorSeries {
return this.result;
}

abstract update(...args: any): void;
abstract update(...args: any): void | Big;
}

export abstract class NumberIndicatorSeries implements IndicatorSeries<number> {
Expand All @@ -56,7 +63,13 @@ export abstract class NumberIndicatorSeries implements IndicatorSeries<number> {

abstract isStable: boolean;

abstract getResult(): number;
getResult(): number {
if (!this.result) {
throw new NotEnoughDataError();
}

return this.result;
}

protected setResult(value: number): number {
this.result = value;
Expand Down
Loading

0 comments on commit 754398f

Please sign in to comment.