Skip to content

Commit

Permalink
add WMA overlay (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender authored Aug 29, 2020
1 parent 17e019f commit 17fbbbb
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
1 change: 1 addition & 0 deletions INDICATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
- [Stochastic Oscillator](/Indicators/Stochastic/README.md#content)
- [Stochastic RSI](/Indicators/StochasticRsi/README.md#content)
- [Ulcer Index](/Indicators/UlcerIndex/README.md#content)
- [Weighted Moving Average (WMA)](/Indicators/Wma/README.md#content)
- [William %R](/Indicators/WilliamR/README.md#content)
- [Zig Zag](/Indicators/ZigZag/README.md#content)
51 changes: 51 additions & 0 deletions Indicators/Wma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# [Linear] Weighted Moving Average (WMA)

Weighted moving average is the linear weighted average of `Close` price over `N` lookback periods. This also called Linear Weighted Moving Average (LWMA).
[More info ...](https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/wma)

```csharp
// usage
IEnumerable<WmaResult> results = Indicator.GetWma(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 0.

## Response

```csharp
IEnumerable<WmaResult>
```

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.

### WmaResult

| name | type | notes
| -- |-- |--
| `Index` | int | Sequence of dates
| `Date` | DateTime | Date
| `Wma` | decimal | Weighted 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 WMA
IEnumerable<WmaResult> results = Indicator.GetWma(history,20);

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

```bash
WMA on 12/31/2018 was $235.53
```
10 changes: 10 additions & 0 deletions Indicators/Wma/Wma.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 WmaResult : ResultBase
{
public decimal? Wma { get; set; } // weighted moving average
}
}
73 changes: 73 additions & 0 deletions Indicators/Wma/Wma.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;

namespace Skender.Stock.Indicators
{
public static partial class Indicator
{
// WEIGHTED MOVING AVERAGE
public static IEnumerable<WmaResult> GetWma(IEnumerable<Quote> history, int lookbackPeriod)
{

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

// check parameters
ValidateWma(history, lookbackPeriod);

// initialize
List<WmaResult> results = new List<WmaResult>();
decimal divisor = (lookbackPeriod * (lookbackPeriod + 1)) / 2m;

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

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

if (h.Index >= lookbackPeriod)
{
List<Quote> period = history
.Where(x => x.Index > (h.Index - lookbackPeriod) && x.Index <= h.Index)
.ToList();

result.Wma = period
.Select(x => x.Close * (lookbackPeriod - (h.Index - x.Index)) / divisor)
.Sum();
}

results.Add(result);
}

return results;
}


private static void ValidateWma(IEnumerable<Quote> history, int lookbackPeriod)
{

// check parameters
if (lookbackPeriod <= 0)
{
throw new BadParameterException("Lookback period must be greater than 0 for WMA.");
}

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

}
}

}
51 changes: 51 additions & 0 deletions IndicatorsTests/Test.Wma.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Skender.Stock.Indicators;
using System;
using System.Collections.Generic;
using System.Linq;

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

[TestMethod()]
public void GetWmaTest()
{
int lookbackPeriod = 20;
IEnumerable<WmaResult> results = Indicator.GetWma(history, lookbackPeriod);

// 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.Wma != null).Count());

// sample values
WmaResult r1 = results.Where(x => x.Index == 502).FirstOrDefault();
Assert.AreEqual(246.5110m, Math.Round((decimal)r1.Wma,4));

WmaResult r2 = results.Where(x => x.Index == 150).FirstOrDefault();
Assert.AreEqual(235.5253m, Math.Round((decimal)r2.Wma, 4));
}


/* EXCEPTIONS */

[TestMethod()]
[ExpectedException(typeof(BadParameterException), "Bad lookback.")]
public void BadLookback()
{
Indicator.GetWma(history, 0);
}

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

0 comments on commit 17fbbbb

Please sign in to comment.