diff --git a/.gitignore b/.gitignore index 06c3eac..b19ef92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ coverage/ dist/ +.idea/ diff --git a/package.json b/package.json index a2e6575..642d526 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "bollinger-bands", "finance", "financial-instruments", + "ichimoku", "indicators", "macd", "quant", diff --git a/src/helper/numArray.ts b/src/helper/numArray.ts index 4c507ea..f1209ef 100644 --- a/src/helper/numArray.ts +++ b/src/helper/numArray.ts @@ -197,6 +197,41 @@ export function shiftRightBy(n: number, values: number[]): number[] { return shiftRightAndFillBy(n, 0, values); } +/** + * Shift values left by given amount and fill with value. + * @param n shift amount. + * @param fill fill value. + * @param values values array. + * @returns shifted and filled values. + */ +export function shiftLeftAndFillBy( + n: number, + fill: number, + values: number[] +): number[] { + const result = new Array(values.length); + + for (let i = 0; i < result.length; i++) { + if (i < n) { + result[i] = values[i+n]; + } else { + result[i] = fill; + } + } + + return result; +} + +/** + * Shifts values left by given amount. + * @param n shift amount. + * @param values values array. + * @return shifted values. + */ +export function shiftLeftBy(n: number, values: number[]): number[] { + return shiftLeftAndFillBy(n, 0, values); +} + /** * Change between the current value and the value n before. * @param n shift amount. diff --git a/src/indicator/momentum/README.md b/src/indicator/momentum/README.md index 81f17b2..9a09f60 100644 --- a/src/indicator/momentum/README.md +++ b/src/indicator/momentum/README.md @@ -63,16 +63,16 @@ The [ichimokuCloud](./ichimokuCloud.ts), also known as Ichimoku Kinko Hyo, calcu ``` Tenkan-sen (Conversion Line) = (9-Period High + 9-Period Low) / 2 Kijun-sen (Base Line) = (26-Period High + 26-Period Low) / 2 -Senkou Span A (Leading Span A) = (Conversion Line + Base Line) / 2 -Senkou Span B (Leading Span B) = (52-Period High + 52-Period Low) / 2 -Chikou Span (Lagging Span) = Closing plotted 26 days in the past. +Senkou Span A (Leading Span A) = (Conversion Line + Base Line) / 2 projected 26 periods in the future +Senkou Span B (Leading Span B) = (52-Period High + 52-Period Low) / 2 projected 26 periods in the future +Chikou Span (Lagging Span) = Closing plotted 26 periods in the past. ``` ```TypeScript import { ichimokuCloud } from 'indicatorts'; const defaultConfig = { short: 9, medium: 26, long: 52, close: 26 }; -const { conversion, base, leadingSpanA, leadingSpanB, leadingSpan } = ichimokuCloud(highs, lows, closings, defaultConfig); +const { tenkan, kijub, ssa, ssb, leadingSpan } = ichimokuCloud(highs, lows, closings, defaultConfig); ``` ## Percentage Price Oscillator (PPO) diff --git a/src/indicator/momentum/ichimokuCloud.test.ts b/src/indicator/momentum/ichimokuCloud.test.ts index 676d824..d35e228 100644 --- a/src/indicator/momentum/ichimokuCloud.test.ts +++ b/src/indicator/momentum/ichimokuCloud.test.ts @@ -1,47 +1,70 @@ // Copyright (c) 2022 Onur Cinar. All Rights Reserved. // https://github.com/cinar/indicatorts -import { deepStrictEqual } from 'assert'; -import { roundDigitsAll } from '../../helper/numArray'; -import { ichimokuCloud } from './ichimokuCloud'; +import {deepStrictEqual} from 'assert'; +import {roundDigitsAll} from '../../helper/numArray'; +import {ichimokuCloud} from './ichimokuCloud'; describe('Ichimoku Cloud', () => { - const highs = [10, 11, 12, 13, 14, 15, 16, 17]; - const lows = [1, 2, 3, 4, 5, 6, 7, 8]; - const closings = [5, 6, 7, 8, 9, 10, 11, 12]; - - it('should be able to compute with a config', () => { - const conversion = [5.5, 6, 7, 8, 9, 10, 11, 12]; - const base = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const leadingSpanA = [5.5, 6, 6.75, 7.5, 8.25, 9, 9.75, 10.5]; - const leadingSpanB = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const laggingSpan = [0, 0, 0, 0, 0, 0, 0, 0]; - - const actual = ichimokuCloud(highs, lows, closings, { - short: 2, - medium: 24, - long: 48, - close: 28, - }); - deepStrictEqual(roundDigitsAll(2, actual.conversion), conversion); - deepStrictEqual(roundDigitsAll(2, actual.base), base); - deepStrictEqual(roundDigitsAll(2, actual.leadingSpanA), leadingSpanA); - deepStrictEqual(roundDigitsAll(2, actual.leadingSpanB), leadingSpanB); - deepStrictEqual(roundDigitsAll(2, actual.laggingSpan), laggingSpan); - }); - - it('should be able to compute without a config', () => { - const conversion = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const base = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const leadingSpanA = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const leadingSpanB = [5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9]; - const laggingSpan = [0, 0, 0, 0, 0, 0, 0, 0]; - - const actual = ichimokuCloud(highs, lows, closings); - deepStrictEqual(roundDigitsAll(2, actual.conversion), conversion); - deepStrictEqual(roundDigitsAll(2, actual.base), base); - deepStrictEqual(roundDigitsAll(2, actual.leadingSpanA), leadingSpanA); - deepStrictEqual(roundDigitsAll(2, actual.leadingSpanB), leadingSpanB); - deepStrictEqual(roundDigitsAll(2, actual.laggingSpan), laggingSpan); - }); + it('calculates Tenkan-sen as the middle point between high and low over the configured short period', () => { + const highs = [2, 4] + const lows = [1, 3] + const closings = [1.5, 3.5] + + { + const {tenkan} = ichimokuCloud(highs, lows, closings, {short: 1}) + deepStrictEqual(roundDigitsAll(2, tenkan), [(2 + 1) / 2, (4 + 3) / 2]); + } + { + const {tenkan} = ichimokuCloud(highs, lows, closings, {short: 2}) + deepStrictEqual(roundDigitsAll(2, tenkan), [0, (4 + 1) / 2]); + } + }) + + it('calculates Kijun-sen as the middle point between high and low over the configured medium period', () => { + const highs = [2, 4] + const lows = [1, 3] + const closings = [1.5, 3.5] + + { + const {kijun} = ichimokuCloud(highs, lows, closings, {medium: 1}) + deepStrictEqual(roundDigitsAll(2, kijun), [(2 + 1) / 2, (4 + 3) / 2]); + } + { + const {kijun} = ichimokuCloud(highs, lows, closings, {medium: 2}) + deepStrictEqual(roundDigitsAll(2, kijun), [0, (4 + 1) / 2]); + } + }) + + it('calculates SSA (Senkou-Span A) as the average between Tenkan-Sen and Kijun-Sen projected in the future by the medium period', () => { + const highs = [2, 4] + const lows = [1, 3] + const closings = [1.5, 3.5] + + const {ssa} = ichimokuCloud(highs, lows, closings, {short: 1, medium: 2}) + + const tenkan = (4 + 3) / 2 + const kijun = (4 + 1) / 2 + deepStrictEqual(roundDigitsAll(2, ssa), [0, 0, 0, (tenkan + kijun) / 2]); + }) + + it('calculates SSB (Senkou-Span B) as the middle point between high and low over the configured long period projected in the future by the medium period', () => { + const highs = [2, 4, 8, 10] + const lows = [1, 3, 6, 3] + const closings = [1.5, 3.5, 7.5, 3.5] + + const {ssb} = ichimokuCloud(highs, lows, closings, {medium: 2, long: 3}) + + deepStrictEqual(roundDigitsAll(2, ssb), [0, 0, 0, 0, (8 + 1) / 2, (10 + 3) / 2]); + }) + + it('laggingSpan (Chikou-Span) is closings projected in the past by the close periods', () => { + const highs = [2, 4, 8, 10] + const lows = [1, 3, 6, 3] + const closings = [1.5, 3.5, 7.5, 3.5] + + const {laggingSpan} = ichimokuCloud(highs, lows, closings, {close: 2}) + + deepStrictEqual(roundDigitsAll(2, laggingSpan), [7.5, 3.5, 0, 0]); + }) }); diff --git a/src/indicator/momentum/ichimokuCloud.ts b/src/indicator/momentum/ichimokuCloud.ts index 275c5f0..1bb72f2 100644 --- a/src/indicator/momentum/ichimokuCloud.ts +++ b/src/indicator/momentum/ichimokuCloud.ts @@ -1,46 +1,127 @@ // Copyright (c) 2022 Onur Cinar. All Rights Reserved. // https://github.com/cinar/indicatorts -import { - add, - checkSameLength, - divideBy, - shiftRightBy, -} from '../../helper/numArray'; -import { mmax } from '../trend/movingMax'; -import { mmin } from '../trend/movingMin'; +import {checkSameLength, shiftLeftBy,} from '../../helper/numArray'; /** * Ichimoku cloud result object. */ export interface IchimokuCloudResult { - conversion: number[]; - base: number[]; - leadingSpanA: number[]; - leadingSpanB: number[]; - laggingSpan: number[]; + tenkan: number[]; + kijun: number[]; + ssa: number[]; + ssb: number[]; + laggingSpan: number[]; } /** * Optional configuration of Ichimoku cloud parameters. */ export interface IchimokuCloudConfig { - short?: number; - medium?: number; - long?: number; - close?: number; + short?: number; + medium?: number; + long?: number; + close?: number; } /** * The default configuration of Ichimoku cloud. */ export const IchimokuCloudDefaultConfig: Required = { - short: 9, - medium: 26, - long: 52, - close: 26, + short: 9, + medium: 26, + long: 52, + close: 26, }; +/** + * Returns a function calculating average price (max - min) / 2 based on period and projection + * + * @param period + * @param highs + * @param lows + * @param projection + */ +const averagePriceReducer = ({period, highs, lows, projection = 0}: { + period: number, + highs: number[], + lows: number[], + projection?: number +}) => (acc: number[], _: number, i: number) => { + if (i < period - 1) return [...acc, 0] + const from = i + 1 - period + const to = i - projection + 1 + const max = Math.max(...highs.slice(from, to)) + const min = Math.min(...lows.slice(from, to)) + return [...acc, (max + min) / 2] +} + +/** + * Tenkan-sen (Conversion Line) = (9-Period High + 9-Period Low) / 2 + * + * @param highs high values. + * @param lows low values. + * @param short short period. + */ +const calculateTenkanSen = ({highs, lows, short}: { + highs: number[], + lows: number[], + short: number +}) => highs.reduce(averagePriceReducer({period: short, highs, lows}), [] as Array) + + +/** + * Kijun-sen (Conversion Line) = (26-Period High + 26-Period Low) / 2 + * + * @param highs high values. + * @param lows low values. + * @param medium mediym period. + */ +const calculateKijunSen = ({highs, lows, medium}: { + highs: number[], + lows: number[], + medium: number +}) => highs.reduce(averagePriceReducer({period: medium, highs, lows}), [] as Array) + +/** + * Senkou Span A (Leading Span A) = (Tenkan-sen Line + Kijun-sen) / 2 projected 26 periods in the future + * + * @param tenkanSen Tenkan-sen values. + * @param kijunSen Kijun-sen values. + * @param medium medium period. + */ +const calculateSenkouSpanA = ({tenkanSen, kijunSen, medium}: { + tenkanSen: number[], + kijunSen: number[], + medium: number +}) => { + const ssa = new Array(kijunSen.length + medium).fill(0) + kijunSen.forEach((k, i) => { + if (k) ssa[i + medium] = (k + tenkanSen[i]) / 2 + }) + return ssa +} + +/** + * Senkou Span B (Leading Span B) = (52-Period High + 52-Period Low) / 2 projected 26 periods in the future + * + * @param highs high values. + * @param lows low values. + * @param long long period. + * @param medium mediym period. + */ +const calculateSenkouSpanB = ({highs, lows, long, medium}: { + highs: number[], + lows: number[], + long: number, + medium: number +}) => new Array(highs.length + medium).fill(0).reduce(averagePriceReducer({ + period: long + medium, + highs, + lows, + projection: medium +}), [] as Array) + /** * Ichimoku Cloud. Also known as Ichimoku Kinko Hyo, is a versatile indicator * that defines support and resistence, identifies trend direction, gauges @@ -48,9 +129,9 @@ export const IchimokuCloudDefaultConfig: Required = { * * Tenkan-sen (Conversion Line) = (9-Period High + 9-Period Low) / 2 * Kijun-sen (Base Line) = (26-Period High + 26-Period Low) / 2 - * Senkou Span A (Leading Span A) = (Conversion Line + Base Line) / 2 - * Senkou Span B (Leading Span B) = (52-Period High + 52-Period Low) / 2 - * Chikou Span (Lagging Span) = Closing plotted 26 days in the past. + * Senkou Span A (Leading Span A) = (Conversion Line + Base Line) / 2 projected 26 periods in the future + * Senkou Span B (Leading Span B) = (52-Period High + 52-Period Low) / 2 projected 26 periods in the future + * Chikou Span (Lagging Span) = Closing plotted 26 periods in the past. * * @param highs high values. * @param lows low values. @@ -59,37 +140,26 @@ export const IchimokuCloudDefaultConfig: Required = { * @return ichimoku cloud result object. */ export function ichimokuCloud( - highs: number[], - lows: number[], - closings: number[], - config: IchimokuCloudConfig = {} + highs: number[], + lows: number[], + closings: number[], + config: IchimokuCloudConfig = {} ): IchimokuCloudResult { - checkSameLength(highs, lows, closings); + checkSameLength(highs, lows, closings); + + const {short, medium, long, close} = { + ...IchimokuCloudDefaultConfig, + ...config, + }; - const { short, medium, long, close } = { - ...IchimokuCloudDefaultConfig, - ...config, - }; - const conversion = divideBy( - 2, - add(mmax(highs, { period: short }), mmin(lows, { period: short })) - ); - const base = divideBy( - 2, - add(mmax(highs, { period: medium }), mmin(lows, { period: medium })) - ); - const leadingSpanA = divideBy(2, add(conversion, base)); - const leadingSpanB = divideBy( - 2, - add(mmax(highs, { period: long }), mmin(lows, { period: long })) - ); - const laggingSpan = shiftRightBy(close, closings); + const tenkan = calculateTenkanSen({highs, lows, short}) + const kijun = calculateKijunSen({highs, lows, medium}) - return { - conversion, - base, - leadingSpanA, - leadingSpanB, - laggingSpan, - }; + return { + tenkan, + kijun, + ssa: calculateSenkouSpanA({tenkanSen: tenkan, kijunSen: kijun, medium}), + ssb: calculateSenkouSpanB({highs, lows, medium, long}), + laggingSpan: shiftLeftBy(close, closings), + }; } diff --git a/src/strategy/momentum/ichimokuCloudStrategy.ts b/src/strategy/momentum/ichimokuCloudStrategy.ts index d264713..60cdd95 100644 --- a/src/strategy/momentum/ichimokuCloudStrategy.ts +++ b/src/strategy/momentum/ichimokuCloudStrategy.ts @@ -1,13 +1,9 @@ // Copyright (c) 2022 Onur Cinar. All Rights Reserved. // https://github.com/cinar/indicatorts -import { Asset } from '../asset'; -import { Action } from '../action'; -import { - IchimokuCloudConfig, - IchimokuCloudDefaultConfig, - ichimokuCloud, -} from '../../indicator/momentum/ichimokuCloud'; +import {Asset} from '../asset'; +import {Action} from '../action'; +import {ichimokuCloud, IchimokuCloudConfig, IchimokuCloudDefaultConfig,} from '../../indicator/momentum/ichimokuCloud'; /** * Ichimoku cloud. @@ -28,12 +24,12 @@ export function ichimokuCloudStrategy( strategyConfig ); - const actions = new Array(indicator.base.length); + const actions = new Array(indicator.kijun.length); for (let i = 0; i < actions.length; i++) { - if (indicator.leadingSpanA[i] > indicator.leadingSpanB[i]) { + if (indicator.ssa[i] > indicator.ssb[i]) { actions[i] = Action.BUY; - } else if (indicator.leadingSpanA[i] < indicator.leadingSpanB[i]) { + } else if (indicator.ssa[i] < indicator.ssb[i]) { actions[i] = Action.SELL; } else { actions[i] = Action.HOLD;