Repository: cinar/indicatorts
Pull request #455 (2024-04-16 23:32:39)

Fix ichimoku indicators calculation (#452)
Fix https://github.com/cinar/indicatorts/issues/452

- [x] Fix indicators calculation
- [x] Reduce duplication
- [x] Update documentation

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/cinar/indicatorts/pull/455?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Onur+Cinar) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 72.37%. Comparing base [(`496c3e4`)](https://app.codecov.io/gh/cinar/indicatorts/commit/496c3e4cb72256d86cc6ea54507476a4818f4007?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Onur+Cinar) to head [(`5f811e4`)](https://app.codecov.io/gh/cinar/indicatorts/pull/455?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Onur+Cinar).
> Report is 9 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #455      +/-   ##
==========================================
+ Coverage   65.58%   72.37%   +6.79%     
==========================================
  Files          94      102       +8     
  Lines        1537     1665     +128     
  Branches      181      254      +73     
==========================================
+ Hits         1008     1205     +197     
+ Misses        517      448      -69     
  Partials       12       12              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cinar/indicatorts/pull/455?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Onur+Cinar).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Onur+Cinar).

cinar: That's a good point. Let's go with your suggestion.

On Wed, Apr 17, 2024 at 12:46 PM Thomas Heniart ***@***.***>
wrote:

> ***@***.**** commented on this pull request.
> ------------------------------
>
> In src/indicator/momentum/ichimokuCloud.ts
> <https://github.com/cinar/indicatorts/pull/455#discussion_r1569423209>:
>
> >
> -  return {
> -    conversion,
> -    base,
> -    leadingSpanA,
> -    leadingSpanB,
> -    laggingSpan,
> -  };
> +    return {
> +        conversion: tenkanSen,
> +        base: kijunSen,
> +        leadingSpanA: calculateSenkouSpanA({tenkanSen, kijunSen, medium}),
>
> Tenkan, Kijun, SSA, SSB, and LagginSpan terms are widely used among people
> working with Ichimoku.
> I didn't want to introduce a "breaking change" in the code base, that's
> why I didn't touch IchimokuCloudResult
> If you think it's to changeIchimokuCloudResult interface then it could be
>
> export interface IchimokuCloudResult {
>     kijun: number[];
>     tenkan: number[];
>     ssa: number[];
>     ssb: number[];
>     laggingSpan: number[];}
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/cinar/indicatorts/pull/455#discussion_r1569423209>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AANMH3CDUT3BYRTZZF6RYTDY53GS3AVCNFSM6AAAAABGKH25TCVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDAMBWHE4TQOBWGQ>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>


Patch:
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<number>(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.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<IchimokuCloudConfig> = {
-  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<number>)
+
+
+/**
+ * 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<number>)
+
+/**
+ * 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<number>(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<number>(highs.length + medium).fill(0).reduce(averagePriceReducer({
+    period: long + medium,
+    highs,
+    lows,
+    projection: medium
+}), [] as Array<number>)
+
 /**
  * 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<IchimokuCloudConfig> = {
  *
  * 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<IchimokuCloudConfig> = {
  * @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<Action>(indicator.base.length);
+  const actions = new Array<Action>(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;


Test patch:
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]);
+    })
 });