Skip to content

Commit

Permalink
add Ichimoku Cloud indicator (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender authored Aug 30, 2020
1 parent ba81804 commit 41ffde8
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
1 change: 1 addition & 0 deletions INDICATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Exponential Moving Average (EMA)](/Indicators/Ema/README.md#content)
- [Heikin-Ashi](/Indicators/HeikinAshi/README.md#content)
- [Hull Moving Average (HMA)](/Indicators/Hma/README.md#content)
- [Ichimoku Cloud](/Indicators/Ichimoku/README.md#content)
- [Keltner Channel](/Indicators/Keltner/README.md#content)
- [Money Flow Index (MFI)](/Indicators/Mfi/README.md#content)
- [Moving Average Convergence/Divergence (MACD)](/Indicators/Macd/README.md#content)
Expand Down
14 changes: 14 additions & 0 deletions Indicators/Ichimoku/Ichimoku.Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Skender.Stock.Indicators
{
[Serializable]
public class IchimokuResult : ResultBase
{
public decimal? TenkanSen { get; set; } // conversion line
public decimal? KijunSen { get; set; } // base line
public decimal? SenkouSpanA { get; set; } // leading span A
public decimal? SenkauSpanB { get; set; } // leading span B
public decimal? ChikouSpan { get; set; } // lagging span
}
}
139 changes: 139 additions & 0 deletions Indicators/Ichimoku/Ichimoku.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Skender.Stock.Indicators
{
public static partial class Indicator
{
// ICHIMOKU CLOUD
public static IEnumerable<IchimokuResult> GetIchimoku(IEnumerable<Quote> history,
int signalPeriod = 9, int shortSpanPeriod = 26, int longSpanPeriod = 52)
{

// clean quotes
history = Cleaners.PrepareHistory(history);

// check parameters
ValidateIchimoku(history, signalPeriod, shortSpanPeriod, longSpanPeriod);

// initialize
List<IchimokuResult> results = new List<IchimokuResult>();

// roll through history
foreach (Quote h in history)
{

IchimokuResult result = new IchimokuResult
{
Index = (int)h.Index,
Date = h.Date
};

// tenkan-sen conversion line
if (h.Index >= signalPeriod)
{

List<Quote> tenkanPeriod = history
.Where(x => x.Index > (h.Index - signalPeriod) && x.Index <= h.Index)
.ToList();

decimal max = tenkanPeriod.Select(x => x.High).Max();
decimal min = tenkanPeriod.Select(x => x.Low).Min();

result.TenkanSen = (min + max) / 2;
}

// kijun-sen base line
if (h.Index >= shortSpanPeriod)
{
List<Quote> kijunPeriod = history
.Where(x => x.Index > (h.Index - shortSpanPeriod) && x.Index <= h.Index)
.ToList();

decimal max = kijunPeriod.Select(x => x.High).Max();
decimal min = kijunPeriod.Select(x => x.Low).Min();

result.KijunSen = (min + max) / 2;
}

// senkou span A
if (h.Index >= 2 * shortSpanPeriod)
{

IchimokuResult skq = results
.Where(x => x.Index == h.Index - shortSpanPeriod)
.FirstOrDefault();

if (skq != null && skq.TenkanSen != null && skq.KijunSen != null)
{
result.SenkouSpanA = (skq.TenkanSen + skq.KijunSen) / 2;
}
}

// senkou span B
if (h.Index >= shortSpanPeriod + longSpanPeriod)
{
List<Quote> senkauPeriod = history
.Where(x => x.Index > (h.Index - shortSpanPeriod - longSpanPeriod)
&& x.Index <= h.Index - shortSpanPeriod)
.ToList();

decimal max = senkauPeriod.Select(x => x.High).Max();
decimal min = senkauPeriod.Select(x => x.Low).Min();

result.SenkauSpanB = (min + max) / 2;
}

// chikou line
Quote chikouQuote = history
.Where(x => x.Index == h.Index + shortSpanPeriod)
.FirstOrDefault();

if (chikouQuote != null)
{
result.ChikouSpan = chikouQuote.Close;
}

results.Add(result);
}

return results;
}


private static void ValidateIchimoku(IEnumerable<Quote> history,
int signalPeriod, int shortSpanPeriod, int longSpanPeriod)
{

// check parameters
if (signalPeriod <= 0)
{
throw new BadParameterException("Signal period must be greater than 0 for ICHIMOKU.");
}

if (shortSpanPeriod <= 0)
{
throw new BadParameterException("Short span period must be greater than 0 for ICHIMOKU.");
}

if (longSpanPeriod <= shortSpanPeriod)
{
throw new BadParameterException("Long span period must be greater than small span period for ICHIMOKU.");
}

// check history
int qtyHistory = history.Count();
int minHistory = Math.Max(signalPeriod, Math.Max(shortSpanPeriod, longSpanPeriod));
if (qtyHistory < minHistory)
{
throw new BadHistoryException("Insufficient history provided for ICHIMOKU. " +
string.Format(cultureProvider,
"You provided {0} periods of history when at least {1} is required.",
qtyHistory, minHistory));
}

}
}

}
57 changes: 57 additions & 0 deletions Indicators/Ichimoku/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Ichimoku Cloud

The Ichimoku Cloud, also known as Ichimoku Kinkō Hyō, is a collection of indicators that depict support and resistance, momentum, and trend direction.
[More info ...](https://school.stockcharts.com/doku.php?id=technical_indicators:ichimoku_cloud)

```csharp
// usage
IEnumerable<IchimokuResult> results = Indicator.GetIchimoku(history, lookbackPeriod);
```

## Parameters

| name | type | notes
| -- |-- |--
| `history` | IEnumerable\<[Quote](../../GUIDE.md#quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least the maximum of `N` or `S` or `L` periods of `history`; though, given the leading and lagging nature, we recommend notably more.
| `signalPeriod` | int | Number of periods (`N`) in the Tenkan-sen midpoint evaluation. Must be greater than 0. Default is 9.
| `shortSpanPeriod` | int | Number of periods (`S`) in the shorter Kijun-sen midpoint evaluation. It also sets the Chikou span lag/shift. Must be greater than 0. Default is 26.
| `longSpanPeriod` | int | Number of periods (`L`) in the longer Senkou leading span B midpoint evaluation. Must be greater than `S`. Default is 52.

## Response

```csharp
IEnumerable<IchimokuResult>
```

The first `N-1`, `S-1`, and `L-1` periods will have various `null` values since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes.

### IchimokuResult

| name | type | notes
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `TenkanSen` | decimal | Conversion / signal line
| `KijunSen` | decimal | Base line
| `SenkouSpanA` | decimal | Leading span A
| `SenkauSpanB` | decimal | Leading span B
| `ChikouSpan` | decimal | Lagging span

## Example

```csharp
// fetch historical quotes from your favorite feed, in Quote format
IEnumerable<Quote> history = GetHistoryFromFeed("MSFT");

// calculate ICHIMOKU(9,26,52)
IEnumerable<IchimokuResult> results = Indicator.GetIchimoku(history,9,26,52);

// use results as needed
DateTime evalDate = DateTime.Parse("12/31/2018");
IchimokuResult result = results.Where(x=>x.Date==evalDate).FirstOrDefault();
Console.WriteLine("Tenkan-sen on {0} was ${1}", result.Date, result.TenkanSen);
```

```bash
Tenkan-sen on 12/31/2018 was $241.26
```
80 changes: 80 additions & 0 deletions IndicatorsTests/Test.Ichimoku.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;
using System.Collections.Generic;
using System.Linq;

namespace StockIndicators.Tests
{
[TestClass]
public class IchimokuTests : TestBase
{

[TestMethod()]
public void GetIchimokuTest()
{
int signalPeriod = 9;
int shortSpanPeriod = 26;
int longSpanPeriod = 52;

IEnumerable<IchimokuResult> results = Indicator.GetIchimoku(
history, signalPeriod, shortSpanPeriod, longSpanPeriod);

// assertions

// proper quantities
// should always be the same number of results as there is history
Assert.AreEqual(502, results.Count());
Assert.AreEqual(494, results.Where(x => x.TenkanSen != null).Count());
Assert.AreEqual(477, results.Where(x => x.KijunSen != null).Count());
Assert.AreEqual(451, results.Where(x => x.SenkouSpanA != null).Count());
Assert.AreEqual(425, results.Where(x => x.SenkauSpanB != null).Count());
Assert.AreEqual(476, results.Where(x => x.ChikouSpan != null).Count());

// sample values
IchimokuResult r1 = results.Where(x => x.Index == 476).FirstOrDefault();
Assert.AreEqual(265.575m, r1.TenkanSen);
Assert.AreEqual(263.965m, r1.KijunSen);
Assert.AreEqual(274.9475m, r1.SenkouSpanA);
Assert.AreEqual(274.95m, r1.SenkauSpanB);
Assert.AreEqual(245.28m, r1.ChikouSpan);

IchimokuResult r2 = results.Where(x => x.Index == 502).FirstOrDefault();
Assert.AreEqual(241.26m, r2.TenkanSen);
Assert.AreEqual(251.505m, r2.KijunSen);
Assert.AreEqual(264.77m, r2.SenkouSpanA);
Assert.AreEqual(269.82m, r2.SenkauSpanB);
Assert.AreEqual(null, r2.ChikouSpan);
}


/* EXCEPTIONS */

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad signal period.")]
public void BadSignalPeriod()
{
Indicator.GetIchimoku(history, 0);
}

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad short span period.")]
public void BadShortSpanPeriod()
{
Indicator.GetIchimoku(history, 9, 0, 52);
}

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad long span period.")]
public void BadLongSpanPeriod()
{
Indicator.GetIchimoku(history, 9, 26, 26);
}

[TestMethod()]
[ExpectedException(typeof(BadHistoryException), "Insufficient history.")]
public void InsufficientHistory()
{
Indicator.GetIchimoku(history.Where(x => x.Index < 52), 9, 26, 52);
}
}
}

0 comments on commit 41ffde8

Please sign in to comment.