From 17fbbbb2ab9bfb90299a50f10b46f46b18e5e263 Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Sat, 29 Aug 2020 15:38:47 -0400 Subject: [PATCH] add WMA overlay (#98) --- INDICATORS.md | 1 + Indicators/Wma/README.md | 51 +++++++++++++++++++++++++ Indicators/Wma/Wma.Models.cs | 10 +++++ Indicators/Wma/Wma.cs | 73 ++++++++++++++++++++++++++++++++++++ IndicatorsTests/Test.Wma.cs | 51 +++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 Indicators/Wma/README.md create mode 100644 Indicators/Wma/Wma.Models.cs create mode 100644 Indicators/Wma/Wma.cs create mode 100644 IndicatorsTests/Test.Wma.cs diff --git a/INDICATORS.md b/INDICATORS.md index 54f471716..772c0dfeb 100644 --- a/INDICATORS.md +++ b/INDICATORS.md @@ -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) diff --git a/Indicators/Wma/README.md b/Indicators/Wma/README.md new file mode 100644 index 000000000..dca6daf71 --- /dev/null +++ b/Indicators/Wma/README.md @@ -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 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 +``` + +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 history = GetHistoryFromFeed("MSFT"); + +// calculate 20-period WMA +IEnumerable 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 +``` diff --git a/Indicators/Wma/Wma.Models.cs b/Indicators/Wma/Wma.Models.cs new file mode 100644 index 000000000..88248736d --- /dev/null +++ b/Indicators/Wma/Wma.Models.cs @@ -0,0 +1,10 @@ +using System; + +namespace Skender.Stock.Indicators +{ + [Serializable] + public class WmaResult : ResultBase + { + public decimal? Wma { get; set; } // weighted moving average + } +} diff --git a/Indicators/Wma/Wma.cs b/Indicators/Wma/Wma.cs new file mode 100644 index 000000000..953b5f945 --- /dev/null +++ b/Indicators/Wma/Wma.cs @@ -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 GetWma(IEnumerable history, int lookbackPeriod) + { + + // clean quotes + history = Cleaners.PrepareHistory(history); + + // check parameters + ValidateWma(history, lookbackPeriod); + + // initialize + List results = new List(); + 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 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 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)); + } + + } + } + +} diff --git a/IndicatorsTests/Test.Wma.cs b/IndicatorsTests/Test.Wma.cs new file mode 100644 index 000000000..8cd8c38cc --- /dev/null +++ b/IndicatorsTests/Test.Wma.cs @@ -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 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); + } + } +} \ No newline at end of file