From 0c1b454ad778f95093566ef9e15b442a6702734a Mon Sep 17 00:00:00 2001 From: Chetan Yanamandra Date: Fri, 25 Sep 2020 15:45:41 -0400 Subject: [PATCH] add Ultimate Oscillator (#153) * adding ultimate-oscillator indicator --- docs/INDICATORS.md | 1 + indicators/Ultimate/README.md | 53 +++++++++++ indicators/Ultimate/Ultimate.Models.cs | 14 +++ indicators/Ultimate/Ultimate.cs | 118 +++++++++++++++++++++++++ indicators/Ultimate/chart.png | Bin 0 -> 33088 bytes tests/indicators/Test.Ultimate.cs | 68 ++++++++++++++ tests/performance/Perf.Indicators.cs | 6 ++ 7 files changed, 260 insertions(+) create mode 100644 indicators/Ultimate/README.md create mode 100644 indicators/Ultimate/Ultimate.Models.cs create mode 100644 indicators/Ultimate/Ultimate.cs create mode 100644 indicators/Ultimate/chart.png create mode 100644 tests/indicators/Test.Ultimate.cs diff --git a/docs/INDICATORS.md b/docs/INDICATORS.md index bd9dbf702..41688ead9 100644 --- a/docs/INDICATORS.md +++ b/docs/INDICATORS.md @@ -36,6 +36,7 @@ - [Stochastic RSI](../indicators/StochRsi/README.md#content) - [Triple Exponential Moving Average (TEMA)](../indicators/Ema/README.md#content) - [Ulcer Index](../indicators/UlcerIndex/README.md#content) +- [Ultimate Oscillator](../indicators/Ultimate/README.md#content) - [Volume Simple Moving Average](../indicators/VolSma/README.md#content) - [Weighted Moving Average (WMA)](../indicators/Wma/README.md#content) - [Williams %R](../indicators/WilliamR/README.md#content) diff --git a/indicators/Ultimate/README.md b/indicators/Ultimate/README.md new file mode 100644 index 000000000..c089cd0b3 --- /dev/null +++ b/indicators/Ultimate/README.md @@ -0,0 +1,53 @@ +# Ultimate Oscillator + +[Ultimate Oscillator](https://en.wikipedia.org/wiki/Ultimate_oscillator) uses several lookback periods to weigh buying power against true range price to produce on oversold / overbought oscillator. + +![image](chart.png) + +```csharp +// usage +IEnumerable results = Indicator.GetUltimate(history, shortPeriod, middlePeriod, longPeriod); +``` + +## Parameters + +| name | type | notes +| -- |-- |-- +| `history` | IEnumerable\<[Quote](../../docs/GUIDE.md#quote)\> | Historical Quotes data should be at any consistent frequency (day, hour, minute, etc). You must supply at least `L+1` periods of `history`. +| `shortPeriod` | int | Number of periods (`S`) in the short lookback. Must be greater than 0. +| `middlePeriod` | int | Number of periods (`M`) in the middle lookback. Must be greater than `S`. +| `longPeriod` | int | Number of periods (`L`) in the long lookback. Must be greater than `M`. + +## Response + +```csharp +IEnumerable +``` + +The first `L-1` periods will have `null` Ultimate values since there's not enough data to calculate. We always return the same number of elements as there are in the historical quotes. + +### UltimateResult + +| name | type | notes +| -- |-- |-- +| `Date` | DateTime | Date +| `Ultimate` | decimal | Simple 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 Ultimate +IEnumerable results = Indicator.GetUltimate(history,7,14,28); + +// use results as needed +DateTime evalDate = DateTime.Parse("12/31/2018"); +UltimateResult result = results.Where(x=>x.Date==evalDate).FirstOrDefault(); +Console.WriteLine("ULT on {0} was {1}", result.Date, result.Ultimate); +``` + +```bash +ULT on 12/31/2018 was 49.53 +``` diff --git a/indicators/Ultimate/Ultimate.Models.cs b/indicators/Ultimate/Ultimate.Models.cs new file mode 100644 index 000000000..024a43adc --- /dev/null +++ b/indicators/Ultimate/Ultimate.Models.cs @@ -0,0 +1,14 @@ +using System; + +namespace Skender.Stock.Indicators +{ + [Serializable] + public class UltimateResult : ResultBase + { + public decimal? Ultimate { get; set; } + + // internal use only + internal decimal? Bp { get; set; } // buying pressure + internal decimal? Tr { get; set; } // true range + } +} diff --git a/indicators/Ultimate/Ultimate.cs b/indicators/Ultimate/Ultimate.cs new file mode 100644 index 000000000..f8e90889a --- /dev/null +++ b/indicators/Ultimate/Ultimate.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Skender.Stock.Indicators +{ + public static partial class Indicator + { + // ULTIMATE OSCILLATOR + public static IEnumerable GetUltimate( + IEnumerable history, int shortPeriod = 7, int middlePeriod = 14, int longPeriod = 28) + { + + // clean quotes + List historyList = Cleaners.PrepareHistory(history).ToList(); + + // check parameters + ValidateUltimate(history, shortPeriod, middlePeriod, longPeriod); + + // initialize + List results = new List(); + decimal priorClose = 0; + + // roll through history + for (int i = 0; i < historyList.Count; i++) + { + Quote h = historyList[i]; + + UltimateResult r = new UltimateResult + { + Index = (int)h.Index, + Date = h.Date + }; + results.Add(r); + + if (i > 0) + { + r.Bp = h.Close - Math.Min(h.Low, priorClose); + r.Tr = Math.Max(h.High, priorClose) - Math.Min(h.Low, priorClose); + } + + if (h.Index >= longPeriod + 1) + { + decimal sumBP1 = 0m; + decimal sumBP2 = 0m; + decimal sumBP3 = 0m; + + decimal sumTR1 = 0m; + decimal sumTR2 = 0m; + decimal sumTR3 = 0m; + + for (int p = (int)h.Index - longPeriod; p < h.Index; p++) + { + UltimateResult pr = results[p]; + + // short aggregate + if (pr.Index > h.Index - shortPeriod) + { + sumBP1 += (decimal)pr.Bp; + sumTR1 += (decimal)pr.Tr; + } + + // middle aggregate + if (pr.Index > h.Index - middlePeriod) + { + sumBP2 += (decimal)pr.Bp; + sumTR2 += (decimal)pr.Tr; + } + + // long aggregate + sumBP3 += (decimal)pr.Bp; + sumTR3 += (decimal)pr.Tr; + } + + decimal avg1 = sumBP1 / sumTR1; + decimal avg2 = sumBP2 / sumTR2; + decimal avg3 = sumBP3 / sumTR3; + + r.Ultimate = 100 * (4m * avg1 + 2m * avg2 + avg3) / 7m; + } + + priorClose = h.Close; + } + + return results; + } + + + private static void ValidateUltimate( + IEnumerable history, int shortPeriod = 7, int middleAverage = 14, int longPeriod = 28) + { + + // check parameters + if (shortPeriod <= 0 || middleAverage <= 0 || longPeriod <= 0) + { + throw new BadParameterException("Average periods must be greater than 0 for Ultimate Oscillator."); + } + + if (shortPeriod >= middleAverage || middleAverage >= longPeriod) + { + throw new BadParameterException("Average periods must be increasingly larger than each other for Ultimate Oscillator."); + } + + // check history + int qtyHistory = history.Count(); + int minHistory = longPeriod + 1; + if (qtyHistory < minHistory) + { + throw new BadHistoryException("Insufficient history provided for Ultimate. " + + string.Format(englishCulture, + "You provided {0} periods of history when at least {1} is required.", + qtyHistory, minHistory)); + } + + } + } + +} diff --git a/indicators/Ultimate/chart.png b/indicators/Ultimate/chart.png new file mode 100644 index 0000000000000000000000000000000000000000..79a61038db3e5d211b13555478d367832f7cfeed GIT binary patch literal 33088 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 z⩔l9X`)tlCNMLc-f6Ir6#bEhqyj4 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 literal 0 HcmV?d00001 diff --git a/tests/indicators/Test.Ultimate.cs b/tests/indicators/Test.Ultimate.cs new file mode 100644 index 000000000..ad168ba38 --- /dev/null +++ b/tests/indicators/Test.Ultimate.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skender.Stock.Indicators; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Internal.Tests +{ + [TestClass] + public class UltimateTests : TestBase + { + + [TestMethod()] + public void GetUltimateTest() + { + IEnumerable results = Indicator.GetUltimate(history, 7, 14, 28); + + // assertions + + // proper quantities + // should always be the same number of results as there is history + Assert.AreEqual(502, results.Count()); + Assert.AreEqual(474, results.Where(x => x.Ultimate != null).Count()); + + // sample values + UltimateResult r1 = results.Where(x => x.Index == 502).FirstOrDefault(); + Assert.AreEqual(49.5257m, Math.Round((decimal)r1.Ultimate, 4)); + + UltimateResult r2 = results.Where(x => x.Index == 250).FirstOrDefault(); + Assert.AreEqual(45.3121m, Math.Round((decimal)r2.Ultimate, 4)); + + UltimateResult r3 = results.Where(x => x.Index == 75).FirstOrDefault(); + Assert.AreEqual(51.7770m, Math.Round((decimal)r3.Ultimate, 4)); + + } + + + /* EXCEPTIONS */ + + [TestMethod()] + [ExpectedException(typeof(BadParameterException), "Bad short period.")] + public void BadShortPeriod() + { + Indicator.GetUltimate(history, 0); + } + + [TestMethod()] + [ExpectedException(typeof(BadParameterException), "Bad middle period.")] + public void BadMiddlePeriod() + { + Indicator.GetUltimate(history, 7, 6); + } + + [TestMethod()] + [ExpectedException(typeof(BadParameterException), "Bad long period.")] + public void BadLongPeriod() + { + Indicator.GetUltimate(history, 7, 14, 11); + } + + [TestMethod()] + [ExpectedException(typeof(BadHistoryException), "Insufficient history.")] + public void InsufficientHistory() + { + Indicator.GetUltimate(history.Where(x => x.Index <= 28), 7, 14, 28); + } + } +} \ No newline at end of file diff --git a/tests/performance/Perf.Indicators.cs b/tests/performance/Perf.Indicators.cs index 1b58feb73..54563df42 100644 --- a/tests/performance/Perf.Indicators.cs +++ b/tests/performance/Perf.Indicators.cs @@ -252,6 +252,12 @@ public object GetUlcerIndex() return Indicator.GetUlcerIndex(hm); } + [Benchmark] + public object GetUltimate() + { + return Indicator.GetUltimate(hm); + } + [Benchmark] public object GetVolSma() {