Skip to content

Commit

Permalink
feat(CCI): Add faster CCI implementation (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
bennycode authored Nov 11, 2021
1 parent 2ef901c commit f90e95b
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 22 deletions.
28 changes: 26 additions & 2 deletions src/CCI/CCI.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CCI} from './CCI';
import {CCI, FasterCCI} from './CCI';
import {NotEnoughDataError} from '../error';

describe('CCI', () => {
Expand Down Expand Up @@ -26,17 +26,33 @@ describe('CCI', () => {
describe('getResult', () => {
it('calculates the Commodity Channel Index (CCI)', () => {
const cci = new CCI(5);
const fasterCCI = new FasterCCI(5);
for (const candle of candles) {
cci.update(candle);
if (cci.isStable) {
fasterCCI.update(candle);
if (cci.isStable && fasterCCI.isStable) {
const expected = expectations.shift();
expect(cci.getResult().toFixed(2)).toBe(expected!);
expect(fasterCCI.getResult().toFixed(2)).toBe(expected!);
}
}
const actual = cci.getResult().toFixed(2);
expect(actual).toBe('71.93');
});

it("stores the highest and lowest result throughout the indicator's lifetime", () => {
const cci = new CCI(5);
const fasterCCI = new FasterCCI(5);
for (const candle of candles) {
cci.update(candle);
fasterCCI.update(candle);
}
expect(cci.highest!.toFixed(2)).toBe('166.67');
expect(cci.lowest!.toFixed(2)).toBe('71.93');
expect(fasterCCI.highest!.toFixed(2)).toBe('166.67');
expect(fasterCCI.lowest!.toFixed(2)).toBe('71.93');
});

it('throws an error when there is not enough input data', () => {
const cci = new CCI(5);
try {
Expand All @@ -45,6 +61,14 @@ describe('CCI', () => {
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}

const fasterCCI = new FasterCCI(5);
try {
fasterCCI.getResult();
fail('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(NotEnoughDataError);
}
});
});
});
55 changes: 49 additions & 6 deletions src/CCI/CCI.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import {BigIndicatorSeries} from '../Indicator';
import {BigIndicatorSeries, NumberIndicatorSeries} from '../Indicator';
import {Big, BigSource} from 'big.js';
import {HighLowClose} from '../util';
import {SMA} from '../SMA/SMA';
import {MAD} from '../MAD/MAD';
import {HighLowClose, HighLowCloseNumbers} from '../util';
import {FasterSMA, SMA} from '../SMA/SMA';
import {FasterMAD, 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.
* 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.
* 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
*/
Expand Down Expand Up @@ -50,3 +56,40 @@ export class CCI extends BigIndicatorSeries {
return typicalPrice;
}
}

export class FasterCCI extends NumberIndicatorSeries {
public readonly prices: number[] = [];
protected result?: number;
private readonly sma: FasterSMA;
private readonly typicalPrices: number[] = [];

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

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

override update(candle: HighLowCloseNumbers): void | number {
const typicalPrice = this.cacheTypicalPrice(candle);
this.sma.update(typicalPrice);
if (this.sma.isStable) {
const mean = this.sma.getResult();
const meanDeviation = FasterMAD.getResultFromBatch(this.typicalPrices, mean);
const a = typicalPrice - mean;
const b = 0.015 * meanDeviation;
return this.setResult(a / b);
}
}

private cacheTypicalPrice({high, low, close}: HighLowCloseNumbers): number {
const typicalPrice = (high + low + close) / 3;
this.typicalPrices.push(typicalPrice);
if (this.typicalPrices.length > this.interval) {
this.typicalPrices.shift();
}
return typicalPrice;
}
}
2 changes: 1 addition & 1 deletion src/Indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,5 @@ export abstract class NumberIndicatorSeries implements IndicatorSeries<number> {
return this.result;
}

abstract update(...args: any): void;
abstract update(...args: any): void | number;
}
8 changes: 4 additions & 4 deletions src/MAD/MAD.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ describe('MAD', () => {
// Test data verified with:
// https://en.wikipedia.org/wiki/Average_absolute_deviation#Mean_absolute_deviation_around_a_central_point
const prices = [2, 2, 3, 4, 14];
const result = MAD.getResultFromBatch(prices);
expect(result.valueOf()).toBe('3.6');
expect(MAD.getResultFromBatch(prices).valueOf()).toBe('3.6');
expect(FasterMAD.getResultFromBatch(prices).valueOf()).toBe(3.6);
});

it('accepts a supplied mean', () => {
// Test data verified with:
// https://en.wikipedia.org/wiki/Average_absolute_deviation#Mean_absolute_deviation_around_a_central_point
const prices = [2, 2, 3, 4, 14];
const mean = 5;
const result = MAD.getResultFromBatch(prices, mean);
expect(result.valueOf()).toBe('3.6');
expect(MAD.getResultFromBatch(prices, mean).valueOf()).toBe('3.6');
expect(FasterMAD.getResultFromBatch(prices, mean).valueOf()).toBe(3.6);
});
});
});
10 changes: 10 additions & 0 deletions src/MAD/MAD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,14 @@ export class FasterMAD extends NumberIndicatorSeries {
this.setResult(sum / this.interval);
}
}

static getResultFromBatch(prices: number[], average?: number): number {
const mean = average || getFasterAverage(prices);
let sum = 0;
for (let i = 0; i < prices.length; i++) {
const deviation = Math.abs(prices[i] - mean);
sum += deviation;
}
return sum / prices.length;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './ADX/ADX';
export * from './AO/AO';
export * from './ATR/ATR';
export * from './BBANDS/BollingerBands';
export * from './CCI/CCI';
export * from './CG/CG';
export * from './DEMA/DEMA';
export * from './DMA/DMA';
Expand All @@ -13,6 +14,7 @@ export * from './Indicator';
export * from './MA/MovingAverage';
export * from './MA/MovingAverageTypeContext';
export * from './MACD/MACD';
export * from './MAD/MAD';
export * from './MOM/MOM';
export * from './ROC/ROC';
export * from './RSI/RSI';
Expand Down
42 changes: 33 additions & 9 deletions src/start/startBenchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ import candles from '../test/fixtures/candles/100-candles.json';
import {FasterMAD, MAD} from '../MAD/MAD';
import {BollingerBands, FasterBollingerBands} from '../BBANDS/BollingerBands';
import {EMA, FasterEMA} from '../EMA/EMA';
import {getFasterAverage, getAverage, getFasterStandardDeviation, getStandardDeviation} from '../util';
import {SMA, FasterSMA} from '../SMA/SMA';
import {
getAverage,
getFasterAverage,
getFasterStandardDeviation,
getStandardDeviation,
HighLowCloseNumbers,
} from '../util';
import {FasterSMA, SMA} from '../SMA/SMA';
import {CCI, FasterCCI} from '../CCI/CCI';

const interval = 20;
const prices = candles.map(candle => parseInt(candle.close, 10));
const prices: number[] = candles.map(candle => parseInt(candle.close, 10));
const highLowCloses: HighLowCloseNumbers[] = candles.map(candle => ({
close: parseInt(candle.close, 10),
high: parseInt(candle.high, 10),
low: parseInt(candle.low, 10),
}));

new Benchmark.Suite('Technical Indicators')
.add('BollingerBands', () => {
Expand All @@ -16,6 +28,24 @@ new Benchmark.Suite('Technical Indicators')
bb.update(price);
}
})
.add('FasterBollingerBands', () => {
const fasterBB = new FasterBollingerBands(interval, 2);
for (const price of prices) {
fasterBB.update(price);
}
})
.add('CCI', () => {
const cci = new CCI(interval);
for (const candle of highLowCloses) {
cci.update(candle);
}
})
.add('FasterCCI', () => {
const fasterCCI = new FasterCCI(interval);
for (const candle of highLowCloses) {
fasterCCI.update(candle);
}
})
.add('EMA', () => {
const ema = new EMA(interval);
for (const price of prices) {
Expand All @@ -28,12 +58,6 @@ new Benchmark.Suite('Technical Indicators')
fasterEMA.update(price);
}
})
.add('FasterBollingerBands', () => {
const fasterBB = new FasterBollingerBands(interval, 2);
for (const price of prices) {
fasterBB.update(price);
}
})
.add('MAD', () => {
const mad = new MAD(interval);
for (const price of prices) {
Expand Down
2 changes: 2 additions & 0 deletions src/util/HighLowClose.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {BigSource} from 'big.js';

export type HighLowClose = {close: BigSource; high: BigSource; low: BigSource};

export type HighLowCloseNumbers = {close: number; high: number; low: number};

0 comments on commit f90e95b

Please sign in to comment.