-
Notifications
You must be signed in to change notification settings - Fork 248
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add Keltner Channel indicators (#56)
* add Keltner Channel indicators
- Loading branch information
1 parent
d1be1e3
commit 7af641f
Showing
5 changed files
with
254 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace Skender.Stock.Indicators | ||
{ | ||
|
||
public class KeltnerResult : ResultBase | ||
{ | ||
public decimal? UpperBand { get; set; } | ||
public decimal? Centerline { get; set; } | ||
public decimal? LowerBand { get; set; } | ||
public decimal? Width { get; set; } | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Skender.Stock.Indicators | ||
{ | ||
public static partial class Indicator | ||
{ | ||
// DONCHIAN CHANNEL | ||
public static IEnumerable<KeltnerResult> GetKeltner( | ||
IEnumerable<Quote> history, int emaPeriod = 20, decimal multiplier = 2, int atrPeriod = 10) | ||
{ | ||
|
||
// clean quotes | ||
history = Cleaners.PrepareHistory(history); | ||
|
||
// validate parameters | ||
ValidateKeltner(history, emaPeriod, multiplier, atrPeriod); | ||
|
||
// initialize | ||
List<KeltnerResult> results = new List<KeltnerResult>(); | ||
IEnumerable<EmaResult> emaResults = GetEma(history, emaPeriod); | ||
IEnumerable<AtrResult> atrResults = GetAtr(history, atrPeriod); | ||
int lookbackPeriod = Math.Max(emaPeriod, atrPeriod); | ||
|
||
decimal? prevWidth = null; | ||
|
||
// roll through history | ||
foreach (Quote h in history) | ||
{ | ||
KeltnerResult result = new KeltnerResult | ||
{ | ||
Index = (int)h.Index, | ||
Date = h.Date | ||
}; | ||
|
||
if (h.Index >= lookbackPeriod) | ||
{ | ||
IEnumerable<Quote> period = history | ||
.Where(x => x.Index > (h.Index - lookbackPeriod) && x.Index <= h.Index); | ||
|
||
EmaResult ema = emaResults.Where(x => x.Index == h.Index).FirstOrDefault(); | ||
AtrResult atr = atrResults.Where(x => x.Index == h.Index).FirstOrDefault(); | ||
|
||
result.UpperBand = ema.Ema + multiplier * atr.Atr; | ||
result.LowerBand = ema.Ema - multiplier * atr.Atr; | ||
result.Centerline = ema.Ema; | ||
result.Width = (result.Centerline == 0) ? null : (result.UpperBand - result.LowerBand) / result.Centerline; | ||
|
||
// for next iteration | ||
prevWidth = result.Width; | ||
} | ||
|
||
results.Add(result); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
|
||
private static void ValidateKeltner( | ||
IEnumerable<Quote> history, int emaPeriod, decimal multiplier, int atrPeriod) | ||
{ | ||
|
||
// check parameters | ||
if (emaPeriod <= 1) | ||
{ | ||
throw new BadParameterException("EMA period must be greater than 1 for Keltner Channel."); | ||
} | ||
|
||
if (atrPeriod <= 1) | ||
{ | ||
throw new BadParameterException("ATR period must be greater than 1 for Keltner Channel."); | ||
} | ||
|
||
if (multiplier <= 0) | ||
{ | ||
throw new BadParameterException("Multiplier must be greater than 0 for Keltner Channel."); | ||
} | ||
|
||
// check history | ||
int lookbackPeriod = Math.Max(emaPeriod, atrPeriod); | ||
int qtyHistory = history.Count(); | ||
int minHistory = Math.Max(2 * lookbackPeriod, lookbackPeriod + 100); | ||
if (qtyHistory < minHistory) | ||
{ | ||
throw new BadHistoryException("Insufficient history provided for Keltner Channel. " + | ||
string.Format("You provided {0} periods of history when at least {1} is required. " | ||
+ "Since this uses a smoothing technique, for a lookback period of {2}, " | ||
+ "we recommend you use at least {3} data points prior to the intended " | ||
+ "usage date for maximum precision.", | ||
qtyHistory, minHistory, lookbackPeriod, lookbackPeriod + 250)); | ||
} | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Keltner Channels | ||
|
||
Keltner Channels are based on an EMA centerline and ATR band widths. | ||
[More info ...](https://school.stockcharts.com/doku.php?id=technical_indicators:keltner_channels) | ||
|
||
```csharp | ||
// usage | ||
IEnumerable<KeltnerResult> results = Indicator.GetKeltner(history, emaPeriod, multiplier, atrPeriod); | ||
``` | ||
|
||
## 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 2×`N` or `N`+100 periods of `history`, whichever is more. Since this uses a smoothing technique, we recommend you use at least `N`+250 data points prior to the intended usage date for maximum precision. | ||
| `emaPeriod` | int | Number of periods (`E`) for the center line moving average. Must be greater than 1 to calculate; however we suggest a larger period for an appropriate sample size. Default is 20. | ||
| `multiplier` | decimal | ATR Multiplier. Must be greater than 0. Default is 2. | ||
| `atrPeriod` | int | Number of periods (`A`) for the center line moving average. Must be greater than 1 to calculate; however we suggest a larger period for an appropriate sample size. Default is 10. | ||
|
||
Note: `N` is the greater of `E` or `A` periods. | ||
|
||
## Response | ||
|
||
```csharp | ||
IEnumerable<KeltnerResult> | ||
``` | ||
|
||
The first `N-1` periods will have `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. | ||
|
||
### KeltnerResult | ||
|
||
| name | type | notes | ||
| -- |-- |-- | ||
| `Index` | int | Sequence of dates | ||
| `Date` | DateTime | Date | ||
| `UpperBand` | decimal | Upper band of Keltner Channel | ||
| `Centerline` | decimal | EMA of Close price | ||
| `LowerBand` | decimal | Lower band of Keltner Channel | ||
| `Width` | decimal | Width as percent of Centerline price. `(UpperBand-LowerBand)/Centerline` | ||
|
||
## Example | ||
|
||
```csharp | ||
// fetch historical quotes from your favorite feed, in Quote format | ||
IEnumerable<Quote> history = GetHistoryFromFeed("SPY"); | ||
|
||
// calculate Keltner(20) | ||
IEnumerable<KeltnerResult> results = Indicator.GetKeltner(history,20,2.0,10); | ||
|
||
// use results as needed | ||
DateTime evalDate = DateTime.Parse("12/31/2018"); | ||
KeltnerResult result = results.Where(x=>x.Date==evalDate).FirstOrDefault(); | ||
Console.WriteLine("Upper Keltner Channel on {0} was ${1}", result.Date, result.UpperBand); | ||
``` | ||
|
||
```bash | ||
Upper Keltner Channel on 12/31/2018 was $262.19 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
using Skender.Stock.Indicators; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace StockIndicators.Tests | ||
{ | ||
[TestClass] | ||
public class KeltnerTests : TestBase | ||
{ | ||
|
||
[TestMethod()] | ||
public void GetKeltnerTest() | ||
{ | ||
int emaPeriod = 20; | ||
int multiplier = 2; | ||
int atrPeriod = 10; | ||
int lookbackPeriod = Math.Max(emaPeriod, atrPeriod); | ||
IEnumerable<KeltnerResult> results = Indicator.GetKeltner(history, emaPeriod, multiplier, atrPeriod); | ||
|
||
// assertions | ||
|
||
// proper quantities | ||
// should always be the same number of results as there is history | ||
Assert.AreEqual(502, results.Count()); | ||
Assert.AreEqual(502 - lookbackPeriod + 1, results.Where(x => x.Centerline != null).Count()); | ||
Assert.AreEqual(502 - lookbackPeriod + 1, results.Where(x => x.UpperBand != null).Count()); | ||
Assert.AreEqual(502 - lookbackPeriod + 1, results.Where(x => x.LowerBand != null).Count()); | ||
Assert.AreEqual(502 - lookbackPeriod + 1, results.Where(x => x.Width != null).Count()); | ||
|
||
// sample value | ||
KeltnerResult r1 = results.Where(x => x.Date == DateTime.Parse("12/31/2018")).FirstOrDefault(); | ||
Assert.AreEqual((decimal)262.1873, Math.Round((decimal)r1.UpperBand, 4)); | ||
Assert.AreEqual((decimal)249.3519, Math.Round((decimal)r1.Centerline, 4)); | ||
Assert.AreEqual((decimal)236.5165, Math.Round((decimal)r1.LowerBand, 4)); | ||
Assert.AreEqual((decimal)0.102950, Math.Round((decimal)r1.Width, 6)); | ||
|
||
KeltnerResult r2 = results.Where(x => x.Date == DateTime.Parse("12/06/2018")).FirstOrDefault(); | ||
Assert.AreEqual((decimal)275.4260, Math.Round((decimal)r2.UpperBand, 4)); | ||
Assert.AreEqual((decimal)265.4599, Math.Round((decimal)r2.Centerline, 4)); | ||
Assert.AreEqual((decimal)255.4938, Math.Round((decimal)r2.LowerBand, 4)); | ||
Assert.AreEqual((decimal)0.075085, Math.Round((decimal)r2.Width, 6)); | ||
} | ||
|
||
|
||
/* EXCEPTIONS */ | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadParameterException), "Bad EMA period.")] | ||
public void BadEmaPeriod() | ||
{ | ||
Indicator.GetKeltner(history, 1, 2, 10); | ||
} | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadParameterException), "Bad ATR period.")] | ||
public void BadAtrPeriod() | ||
{ | ||
Indicator.GetKeltner(history, 20, 2, 1); | ||
} | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadParameterException), "Bad multiplier.")] | ||
public void BadMultiplier() | ||
{ | ||
Indicator.GetKeltner(history, 20, 0, 10); | ||
} | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadHistoryException), "Insufficient history 100.")] | ||
public void InsufficientHistory100() | ||
{ | ||
Indicator.GetKeltner(history.Where(x => x.Index < 120), 20, 2, 10); | ||
} | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadHistoryException), "Insufficient history 250.")] | ||
public void InsufficientHistory250() | ||
{ | ||
Indicator.GetKeltner(history.Where(x => x.Index < 500), 20, 2, 250); | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters