Skip to content

Commit

Permalink
add HMA overlay (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender authored Aug 29, 2020
1 parent 17fbbbb commit 93d5aeb
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 0 deletions.
1 change: 1 addition & 0 deletions INDICATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Double Exponential Moving Average (DEMA)](/Indicators/Ema/README.md#content)
- [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)
- [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
10 changes: 10 additions & 0 deletions Indicators/Hma/Hma.Models.cs
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
}
}
99 changes: 99 additions & 0 deletions Indicators/Hma/Hma.cs
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));
}

}
}

}
51 changes: 51 additions & 0 deletions Indicators/Hma/README.md
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
```
56 changes: 56 additions & 0 deletions IndicatorsTests/Test.Hma.cs
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);
}
}
}

0 comments on commit 93d5aeb

Please sign in to comment.