-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
17fbbbb
commit 93d5aeb
Showing
5 changed files
with
217 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
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,10 @@ | ||
using System; | ||
|
||
namespace Skender.Stock.Indicators | ||
{ | ||
[Serializable] | ||
public class HmaResult : ResultBase | ||
{ | ||
public decimal? Hma { get; set; } // weighted moving average | ||
} | ||
} |
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,99 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Skender.Stock.Indicators | ||
{ | ||
public static partial class Indicator | ||
{ | ||
// HULL MOVING AVERAGE | ||
public static IEnumerable<HmaResult> GetHma(IEnumerable<Quote> history, int lookbackPeriod) | ||
{ | ||
|
||
// clean quotes | ||
history = Cleaners.PrepareHistory(history); | ||
|
||
// check parameters | ||
ValidateHma(history, lookbackPeriod); | ||
|
||
// initialize | ||
List<Quote> synthHistory = new List<Quote>(); | ||
|
||
IEnumerable<WmaResult> wmaN1 = GetWma(history, lookbackPeriod); | ||
IEnumerable<WmaResult> wmaN2 = GetWma(history, lookbackPeriod / 2); | ||
|
||
// create interim synthetic history | ||
foreach (Quote h in history) | ||
{ | ||
Quote sh = new Quote | ||
{ | ||
Date = h.Date | ||
}; | ||
|
||
WmaResult w1 = wmaN1.Where(x => x.Index == h.Index).FirstOrDefault(); | ||
WmaResult w2 = wmaN2.Where(x => x.Index == h.Index).FirstOrDefault(); | ||
|
||
if (w1.Wma != null && w2.Wma != null) | ||
{ | ||
sh.Close = (decimal)(w2.Wma * 2m - w1.Wma); | ||
synthHistory.Add(sh); | ||
//Console.WriteLine("{0},{1},{2},{3}", h.Index, w1.Wma, w2.Wma, sh.Close); // debugging only | ||
} | ||
|
||
} | ||
|
||
// initialize results, add back truncated null results | ||
int sqN = (int)Math.Sqrt(lookbackPeriod); | ||
int shiftQty = lookbackPeriod - 1; | ||
|
||
List<HmaResult> results = history | ||
.Select(x => new HmaResult | ||
{ | ||
Index = (int)x.Index, | ||
Date = x.Date | ||
}) | ||
.Where(x => x.Index <= shiftQty) | ||
.ToList(); | ||
|
||
// calculate final HMA = WMA with period SQRT(n) | ||
List<HmaResult> hmaResults = GetWma(synthHistory, sqN) | ||
.Select(x => new HmaResult | ||
{ | ||
Index = x.Index + shiftQty, | ||
Date = x.Date, | ||
Hma = x.Wma | ||
}) | ||
.ToList(); | ||
|
||
// add WMA to results | ||
results.AddRange(hmaResults); | ||
results = results.OrderBy(x => x.Index).ToList(); | ||
|
||
return results; | ||
} | ||
|
||
|
||
private static void ValidateHma(IEnumerable<Quote> history, int lookbackPeriod) | ||
{ | ||
|
||
// check parameters | ||
if (lookbackPeriod <= 1) | ||
{ | ||
throw new BadParameterException("Lookback period must be greater than 1 for HMA."); | ||
} | ||
|
||
// check history | ||
int qtyHistory = history.Count(); | ||
int minHistory = lookbackPeriod; | ||
if (qtyHistory < minHistory) | ||
{ | ||
throw new BadHistoryException("Insufficient history provided for HMA. " + | ||
string.Format(cultureProvider, | ||
"You provided {0} periods of history when at least {1} is required.", | ||
qtyHistory, minHistory)); | ||
} | ||
|
||
} | ||
} | ||
|
||
} |
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,51 @@ | ||
# Hull Moving Average (HMA) | ||
|
||
HMA is a modified linear weighted average of `Close` price over `N` lookback periods that reduces lag. | ||
[More info ...](https://alanhull.com/hull-moving-average) | ||
|
||
```csharp | ||
// usage | ||
IEnumerable<HmaResult> results = Indicator.GetHma(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 `N` periods of `history`. | ||
| `lookbackPeriod` | int | Number of periods (`N`) in the moving average. Must be greater than 1. | ||
|
||
## Response | ||
|
||
```csharp | ||
IEnumerable<HmaResult> | ||
``` | ||
|
||
The first `N-(integer of SQRT(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. | ||
|
||
### HmaResult | ||
|
||
| name | type | notes | ||
| -- |-- |-- | ||
| `Index` | int | Sequence of dates | ||
| `Date` | DateTime | Date | ||
| `Hma` | decimal | Hull moving average for `N` lookback periods | ||
|
||
## Example | ||
|
||
```csharp | ||
// fetch historical quotes from your favorite feed, in Quote format | ||
IEnumerable<Quote> history = GetHistoryFromFeed("MSFT"); | ||
|
||
// calculate 20-period HMA | ||
IEnumerable<HmaResult> results = Indicator.GetHma(history,20); | ||
|
||
// use results as needed | ||
DateTime evalDate = DateTime.Parse("12/31/2018"); | ||
HmaResult result = results.Where(x=>x.Date==evalDate).FirstOrDefault(); | ||
Console.WriteLine("HMA on {0} was ${1}", result.Date, result.Hma); | ||
``` | ||
|
||
```bash | ||
HMA on 12/31/2018 was $235.70 | ||
``` |
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,56 @@ | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
using Skender.Stock.Indicators; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace StockIndicators.Tests | ||
{ | ||
[TestClass] | ||
public class HmaTests : TestBase | ||
{ | ||
|
||
[TestMethod()] | ||
public void GetHmaTest() | ||
{ | ||
int lookbackPeriod = 20; | ||
IEnumerable<HmaResult> results = Indicator.GetHma(history, lookbackPeriod); | ||
|
||
foreach (HmaResult r in results) | ||
{ | ||
Console.WriteLine("{0},{1:d},{2:N4}", r.Index, r.Date, r.Hma); // debugging only | ||
} | ||
|
||
// assertions | ||
|
||
// proper quantities | ||
// should always be the same number of results as there is history | ||
Assert.AreEqual(502, results.Count()); | ||
Assert.AreEqual(480, results.Where(x => x.Hma != null).Count()); | ||
|
||
// sample values | ||
HmaResult r1 = results.Where(x => x.Index == 150).FirstOrDefault(); | ||
Assert.AreEqual(236.0835m, Math.Round((decimal)r1.Hma, 4)); | ||
|
||
HmaResult r2 = results.Where(x => x.Index == 502).FirstOrDefault(); | ||
Assert.AreEqual(235.6972m, Math.Round((decimal)r2.Hma, 4)); | ||
} | ||
|
||
|
||
/* EXCEPTIONS */ | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadParameterException), "Bad lookback.")] | ||
public void BadLookback() | ||
{ | ||
Indicator.GetHma(history, 0); | ||
} | ||
|
||
[TestMethod()] | ||
[ExpectedException(typeof(BadHistoryException), "Insufficient history.")] | ||
public void InsufficientHistory() | ||
{ | ||
Indicator.GetHma(history.Where(x => x.Index < 10), 10); | ||
} | ||
} | ||
} |