From 76f837128f3b368daebab27e5a6bcc1dc9e2a693 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Fri, 9 Jun 2023 18:05:47 -0400 Subject: [PATCH 01/13] Refactoring Refactored Average Class --- Core/Calculations/Averages.cs | 56 +++----------- Core/Calculations/BaseCalculator.cs | 54 +++++++++++++ Core/Calculations/ExponetialMovingAverage.cs | 13 ++-- Core/Calculations/Macd.cs | 52 +++++-------- Core/Calculations/MovingAveraage.cs | 6 +- Core/Calculations/RealitiveStrengthIndex.cs | 7 +- .../Response/TrueRangeResponse.cs | 11 +++ Core/Calculations/TrueRange.cs | 34 +++++++++ Core/Domain/Quote.cs | 6 ++ Core/Interfaces/Calculations/IAverage.cs | 14 ++++ Core/Interfaces/Calculations/ICalculate.cs | 4 +- CoreTests/EMATest.cs | 5 +- CoreTests/MACDTests.cs | 2 +- CoreTests/MovingAverageTests.cs | 12 +-- CoreTests/TrueRangeTest.cs | 76 +++++++++++++++++++ README.md | 13 +++- 16 files changed, 265 insertions(+), 100 deletions(-) create mode 100644 Core/Calculations/BaseCalculator.cs create mode 100644 Core/Calculations/Response/TrueRangeResponse.cs create mode 100644 Core/Calculations/TrueRange.cs create mode 100644 Core/Interfaces/Calculations/IAverage.cs create mode 100644 CoreTests/TrueRangeTest.cs diff --git a/Core/Calculations/Averages.cs b/Core/Calculations/Averages.cs index 9dbf27f..e7e6b5f 100644 --- a/Core/Calculations/Averages.cs +++ b/Core/Calculations/Averages.cs @@ -3,32 +3,31 @@ using StockTracker.Core.Calculations.Response; using StockTracker.Core.Interfaces; using StockTracker.Core.Interfaces.Calculations; -using System.Linq; + namespace StockTracker.Core.Calculations { /// /// Takes an array of Trading Structures and creates different types of averages from the data /// - public partial class Averages:ICalculate + public class Averages:BaseCalculator, IAverage { - protected readonly IList activities; + + private readonly ushort numberOfPeriods; + private readonly string columnToAvg; + private readonly int startPostion; /// /// Intialize Object /// /// The data to average - public Averages(IList collection) + public Averages(IList collection, ushort NumberofPeriods, string ColumnToAvg, int StartPostion = 0):base(collection) { - //Make sure the data is in the proper order; - this.activities = (from activity in collection - orderby activity.ActivityDate ascending - select activity).ToList(); - + numberOfPeriods = NumberofPeriods; + columnToAvg = ColumnToAvg; + startPostion = StartPostion; } - - /// /// Calculates an average for a set of numbers /// @@ -36,7 +35,7 @@ orderby activity.ActivityDate ascending /// String that represents the name of the property to calculate the average from /// The postion in the array to start. Needs to be 0 based. Defaults to 0 /// Returns the average - public decimal CalculateSimpleAverage(ushort numberOfPeriods, string columnToAvg, int startPostion = 0) + public decimal Calculate() { if (!ArrayValidforAverage(numberOfPeriods, columnToAvg)) return 0; @@ -46,41 +45,8 @@ public decimal CalculateSimpleAverage(ushort numberOfPeriods, string columnToAvg return (decimal)Math.Round(Sum(startPostion, adjustedEnd, columnToAvg) / numberOfPeriods, 2); } - - - /// - /// Creates a sum of the values in a given range - /// - /// The index in the array to start adding - /// The index in the array to stop adding - /// The property name of the object to add - /// The sum of all the numbers in the give range - protected decimal Sum(int start, int stop, string columnName) - { - decimal currentValue = (decimal)activities[start].GetValue(columnName); - //We have hit the end of the list - if (start == stop) return currentValue; - //Add the current value to the next value in sequence - return currentValue + Sum(start + 1, stop, columnName); - - } - - protected bool ArrayValidforAverage(int requiredNumberOfElements, string columnToAverage) - { - // Is the array too small? - if (activities.Count < requiredNumberOfElements || requiredNumberOfElements == 0) return false; - // Is the column name blank - if (String.IsNullOrEmpty(columnToAverage)) return false; - - return true; - } - - public virtual List Calculate() - { - throw new NotImplementedException(); - } } } \ No newline at end of file diff --git a/Core/Calculations/BaseCalculator.cs b/Core/Calculations/BaseCalculator.cs new file mode 100644 index 0000000..a18927e --- /dev/null +++ b/Core/Calculations/BaseCalculator.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StockTracker.Core.Interfaces; + + +namespace StockTracker.Core.Calculations +{ + public partial class BaseCalculator + { + protected readonly IList activities; + public BaseCalculator(IList collection) + { + //Make sure the data is in the proper order; + this.activities = (from activity in collection + orderby activity.ActivityDate ascending + select activity).ToList(); + } + + public void Initialize() + { + // Initialize the calculator + } + + protected bool ArrayValidforAverage(int requiredNumberOfElements, string columnToAverage) + { + // Is the array too small? + if (activities.Count < requiredNumberOfElements || requiredNumberOfElements == 0) return false; + // Is the column name blank + if (String.IsNullOrEmpty(columnToAverage)) return false; + + return true; + } + + /// + /// Creates a sum of the values in a given range + /// + /// The index in the array to start adding + /// The index in the array to stop adding + /// The property name of the object to add + /// The sum of all the numbers in the give range + protected decimal Sum(int start, int stop, string columnName) + { + decimal currentValue = (decimal)activities[start].GetValue(columnName); + + //We have hit the end of the list + if (start == stop) return currentValue; + + //Add the current value to the next value in sequence + return currentValue + Sum(start + 1, stop, columnName); + + } + } +} \ No newline at end of file diff --git a/Core/Calculations/ExponetialMovingAverage.cs b/Core/Calculations/ExponetialMovingAverage.cs index 5d0d90a..25a6323 100644 --- a/Core/Calculations/ExponetialMovingAverage.cs +++ b/Core/Calculations/ExponetialMovingAverage.cs @@ -6,7 +6,7 @@ namespace StockTracker.Core.Calculations { - public class ExponetialMovingAverage:Averages { + public class ExponetialMovingAverage:BaseCalculator, ICalculate { private ushort _numberOfPeriods; private string _columnToAvg, columnPreviousEma; private int startPosition = 0, smoothingFactor = 2; @@ -46,9 +46,9 @@ public ExponetialMovingAverage(IList activities) : base(activ /// Calculates a weighted average giving more weight to current price movements /// /// A list of averages - public override List Calculate() + public List Calculate() { - List responses = new(); + List responses = new(); int startPos = StartPosition; decimal smoothingWeight = CalculateSmoothingWeight(SmoothingFactor, _numberOfPeriods); @@ -62,7 +62,8 @@ public override List Calculate() //if no EMA exists calculate a simple average as start //and place it into the prevEma variable for the next calculation - prevEma = CalculateSimpleAverage(_numberOfPeriods, _columnToAvg); + Averages simpleAverage = new(activities, _numberOfPeriods, _columnToAvg); + prevEma = simpleAverage.Calculate(); startPos = _numberOfPeriods; // Move index to correct position in the array } else @@ -100,9 +101,9 @@ public static decimal CalculateSmoothingWeight(int smoothingFactor, ushort perio /// The weighted average from the previous calculation /// A weight to be applied in the average calculation /// - private List CalculateEMA(int start, int end, string columnToAverage, decimal lastEma, decimal smoothingWeight) + private List CalculateEMA(int start, int end, string columnToAverage, decimal lastEma, decimal smoothingWeight) { - List responses = new(); + List responses = new(); decimal holdEma = lastEma; for(int i = start; i < end; i++) { diff --git a/Core/Calculations/Macd.cs b/Core/Calculations/Macd.cs index 869b3da..f3b651d 100644 --- a/Core/Calculations/Macd.cs +++ b/Core/Calculations/Macd.cs @@ -4,19 +4,16 @@ using StockTracker.Core.Domain; using StockTracker.Core.Interfaces; using StockTracker.Core.Interfaces.Calculations; -using System.Linq; namespace StockTracker.Core.Calculations { /// /// Class to Calculate Moving Average Convergence Divergence /// - public class MACD:Averages + public class MACD:BaseCalculator, ICalculate { private string ema12Column, ema26Column, closingPriceColumn; - private List dataList; - /// /// Intialize the object /// If MACD has never been calculated list should contain all available prices. If resuming a @@ -26,10 +23,6 @@ public class MACD:Averages /// An array containing the Closing Price and poperties to store the required EMAs public MACD(List list):base(list) { - //Make sure the data is in the proper order; - dataList = (from activity in list - orderby activity.ActivityDate ascending - select activity).ToList(); } @@ -56,9 +49,9 @@ orderby activity.ActivityDate ascending /// Calculates Macd Based on the supplied data /// /// list of MACD Response Objects contain the MACD, Signal, 9 Day EMA, 12 Day EMA, 26 Day EMA - public override List Calculate() + public List Calculate() { - List macdResponses = new(); + List macdResponses = new(); //We need at least 27 periods to calculate our first point if (!ArrayValidforAverage(27, ema12Column)) return macdResponses; @@ -69,9 +62,9 @@ public override List Calculate() return CreateResponse(); } - public List CreateResponse() + public List CreateResponse() { - List responses = new(); + List responses = new(); if (activities[0].GetType().Name.Equals("MACDData")) { @@ -121,11 +114,9 @@ private void PopulateClosingPriceEMAs() if (startingPoint == 0) { //For 12 periods This is setting the 13th position with the Average of the first 12 periods (0-11) - dataList[periods].SetDecimalValue(updateColumn, - CalculateSimpleAverage( - periods, - closingPriceColumn - ) + Averages averages = new(this.activities, periods, closingPriceColumn); + this.activities[periods].SetDecimalValue(updateColumn, + averages.Calculate() ); startingPoint = periods + 1; @@ -148,7 +139,7 @@ private void PopulateClosingPriceEMAs() private void PopulateSignalEMAs() { // Find the first entry where we have missing values - int startingPoint = (dataList[0].GetDecimalValue(SignalColumn) == 0)? FindPostionWithValue(0, MACDColumn): FindStartingPostion(1,SignalColumn); + int startingPoint = (this.activities[0].GetDecimalValue(SignalColumn) == 0)? FindPostionWithValue(0, MACDColumn): FindStartingPostion(1,SignalColumn); //All EMAs already exist for this period if (startingPoint == -1) return; @@ -157,12 +148,9 @@ private void PopulateSignalEMAs() if (startingPoint == 0) { //For 9 periods This is setting the 10th position with the Average of the first 9 periods (0-8) - dataList[9].SetDecimalValue(SignalColumn, - CalculateSimpleAverage( - 9, - MACDColumn, - startingPoint - ) + Averages averages = new(this.activities, 9, MACDColumn, startingPoint); + this.activities[9].SetDecimalValue(SignalColumn, + averages.Calculate() ); startingPoint=10; @@ -190,21 +178,21 @@ private void PopulateSignalEMAs() private void CalculateEMAforMacd(decimal smoothingWeight, int start, int stop, string columnToCalculate, string columnToUpdate) { int previous = start - 1; - decimal ema = dataList[previous].GetDecimalValue(columnToUpdate); + decimal ema = this.activities[previous].GetDecimalValue(columnToUpdate); for (int i = start; i < stop; i++) { - dataList[i].SetDecimalValue( + this.activities[i].SetDecimalValue( columnToUpdate, ExponetialMovingAverage.CalculateEMA ( - dataList[i].GetDecimalValue(columnToCalculate), + this.activities[i].GetDecimalValue(columnToCalculate), ema, smoothingWeight ) ); - ema = dataList[i].GetDecimalValue(columnToUpdate); + ema = this.activities[i].GetDecimalValue(columnToUpdate); } } @@ -216,10 +204,10 @@ private void CalculateEMAforMacd(decimal smoothingWeight, int start, int stop, s /// Returns -1 if there are no missing values, otherwise the first postion in the array where no value exists private int FindStartingPostion(int start, string propertyToCheck) { - if (start == dataList.Count) return -1; //EMA Has been calculated for all periods + if (start == this.activities.Count) return -1; //EMA Has been calculated for all periods //This will give us the first row where there are no calculations. We will resume from here. - if (dataList[start].GetDecimalValue(propertyToCheck) == 0) return start; + if (this.activities[start].GetDecimalValue(propertyToCheck) == 0) return start; //Keep Looking return FindStartingPostion(start + 1, propertyToCheck); @@ -233,10 +221,10 @@ private int FindStartingPostion(int start, string propertyToCheck) /// Returns -1 if there are no missing values, otherwise the first postion in the array where no value exists private int FindPostionWithValue(int start, string propertyToCheck) { - if (start == dataList.Count) return -1; //EMA Has been calculated for all periods + if (start == this.activities.Count) return -1; //EMA Has been calculated for all periods //This will give us the first row where there are no calculations. We will resume from here. - if (dataList[start].GetDecimalValue(propertyToCheck) != 0) return start; + if (this.activities[start].GetDecimalValue(propertyToCheck) != 0) return start; //Keep Looking return FindStartingPostion(start + 1, propertyToCheck); diff --git a/Core/Calculations/MovingAveraage.cs b/Core/Calculations/MovingAveraage.cs index 82e59cb..2696c89 100644 --- a/Core/Calculations/MovingAveraage.cs +++ b/Core/Calculations/MovingAveraage.cs @@ -6,7 +6,7 @@ namespace StockTracker.Core.Calculations { - public class MovingAveraage:Averages + public class MovingAveraage:BaseCalculator, ICalculate { private ushort numberOfPeriods; private string columnToAvg; @@ -30,9 +30,9 @@ public MovingAveraage(IList activities) : base(activities) /// An empty array if the array is smaller than the interval else it returns an array of /// AverageResponse objects /// - public override List Calculate() + public List Calculate() { - List responses = new(); + List responses = new(); //Check to see if the array has enough data to calculate an average if (!ArrayValidforAverage(NumberOfPeriods, ColumnToAvg)) return responses; diff --git a/Core/Calculations/RealitiveStrengthIndex.cs b/Core/Calculations/RealitiveStrengthIndex.cs index f099912..46404df 100644 --- a/Core/Calculations/RealitiveStrengthIndex.cs +++ b/Core/Calculations/RealitiveStrengthIndex.cs @@ -86,12 +86,13 @@ private void CalculateIndex() if (itemCount < 14) break; //Not enough data //Since we don't have any previous data we start with a simple Average - Averages averages = new (ConvertArrayForAvg()); + Averages averages = new (ConvertArrayForAvg(), 14,"Gain"); //Advance to the correct place in the array //This is placing the average for the first 14 days in the 15 position i = 14; - dataList[i].AvgGain = (decimal)Math.Round(averages.CalculateSimpleAverage(14, "Gain"),2); - dataList[i].AvgLoss = (decimal)Math.Round(averages.CalculateSimpleAverage(14, "Loss"),2); + dataList[i].AvgGain = (decimal)Math.Round(averages.Calculate(),2); + averages = new (ConvertArrayForAvg(), 14, "Loss"); + dataList[i].AvgLoss = (decimal)Math.Round(averages.Calculate(),2); continue; } diff --git a/Core/Calculations/Response/TrueRangeResponse.cs b/Core/Calculations/Response/TrueRangeResponse.cs new file mode 100644 index 0000000..759e9b1 --- /dev/null +++ b/Core/Calculations/Response/TrueRangeResponse.cs @@ -0,0 +1,11 @@ +using System; +using StockTracker.Core.Calculations.Response; + +public class TrueRangeResponse:BaseResponse{ + public TrueRangeResponse(DateTime date, decimal trueRangeValue){ + ActivityDate = date; + TrueRange = trueRangeValue; + } + public TrueRangeResponse(){} + public decimal TrueRange { get; set; } +} \ No newline at end of file diff --git a/Core/Calculations/TrueRange.cs b/Core/Calculations/TrueRange.cs new file mode 100644 index 0000000..0962d9b --- /dev/null +++ b/Core/Calculations/TrueRange.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using StockTracker.Core.Domain; +using StockTracker.Core.Interfaces.Calculations; + +public class TrueRange:ICalculate { + private List _quotes; + + public TrueRange(List quotes){ + _quotes = quotes; + } + + public List Calculate(){ + List trueRangeResponses = new List(); + decimal HighMinusLow = 0, + HighMinusPreviousClose = 0, + LowMinusPreviousClose = 0, + TrueRange = 0; + + foreach(var quote in _quotes){ + HighMinusLow = quote.High - quote.Low; + HighMinusPreviousClose = Math.Abs( quote.High - quote.Close); + LowMinusPreviousClose = Math.Abs(quote.Low - quote.Close); + + TrueRange = HighMinusLow > HighMinusPreviousClose ? HighMinusLow : HighMinusPreviousClose; + TrueRange = TrueRange > LowMinusPreviousClose ? TrueRange : LowMinusPreviousClose; + + trueRangeResponses.Add(new TrueRangeResponse(quote.ActivityDate, TrueRange)); + } + + return trueRangeResponses; + } + +} \ No newline at end of file diff --git a/Core/Domain/Quote.cs b/Core/Domain/Quote.cs index 655d418..6ec7dcc 100644 --- a/Core/Domain/Quote.cs +++ b/Core/Domain/Quote.cs @@ -19,6 +19,12 @@ public Quote(int id, DateTime activityDate, decimal high, decimal low, decimal o Volume = volume; } + public Quote( DateTime activityDate, decimal high, decimal low, decimal open, decimal close, int volume) + :this(0, activityDate, high, low, open, close, volume) + { + + } + //Properties public decimal High { get; set; } public decimal Low { get; set; } diff --git a/Core/Interfaces/Calculations/IAverage.cs b/Core/Interfaces/Calculations/IAverage.cs new file mode 100644 index 0000000..9c4ea7a --- /dev/null +++ b/Core/Interfaces/Calculations/IAverage.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace StockTracker.Core.Interfaces.Calculations +{ + public interface IAverage + { + + public decimal Calculate(); + + } + + +} diff --git a/Core/Interfaces/Calculations/ICalculate.cs b/Core/Interfaces/Calculations/ICalculate.cs index e142f0f..3e9cc96 100644 --- a/Core/Interfaces/Calculations/ICalculate.cs +++ b/Core/Interfaces/Calculations/ICalculate.cs @@ -3,10 +3,10 @@ namespace StockTracker.Core.Interfaces.Calculations { - public interface ICalculate + public interface ICalculate where T : IResponse { - public List Calculate(); + public List Calculate(); } diff --git a/CoreTests/EMATest.cs b/CoreTests/EMATest.cs index 9796470..1923f37 100644 --- a/CoreTests/EMATest.cs +++ b/CoreTests/EMATest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using StockTracker.Core.Interfaces; using StockTracker.Core.Interfaces.Calculations; +using StockTracker.Core.Calculations.Response; namespace StockTracker.CoreTests { @@ -37,7 +38,7 @@ public void CheckExponetialMovingkAverageCal() averages.NumberOfPeriods = 4; averages.ColumnToAvg = "close"; - List responses = averages.Calculate(); + List responses = averages.Calculate(); Assert.AreEqual(6, responses.Count); Assert.AreEqual(new DateTime(2021, 11, 19, 0, 0, 0), responses[0].ActivityDate); @@ -60,7 +61,7 @@ public void CheckExponetialMovingkAverageWithHistory() averages.ColumnPreviousEma = "PrevEMA"; averages.ColumnToAvg = "CalculateValue"; - List responses = averages.Calculate(); + List responses = averages.Calculate(); Assert.AreEqual(15, responses.Count); Assert.AreEqual(new DateTime(2017, 12, 29, 0, 0, 0), responses[6].ActivityDate); diff --git a/CoreTests/MACDTests.cs b/CoreTests/MACDTests.cs index 81d8769..1c5a9a9 100644 --- a/CoreTests/MACDTests.cs +++ b/CoreTests/MACDTests.cs @@ -88,7 +88,7 @@ public void CheckExponetialMovingkAverageCal() { try { - List responses = averages.Calculate(); + List responses = averages.Calculate(); MacdResponse macdResponse = (MacdResponse)responses[49]; diff --git a/CoreTests/MovingAverageTests.cs b/CoreTests/MovingAverageTests.cs index 03c14a7..7665a3e 100644 --- a/CoreTests/MovingAverageTests.cs +++ b/CoreTests/MovingAverageTests.cs @@ -47,7 +47,7 @@ public void ChecMovingkAverageCal() averages.ColumnToAvg = "close"; averages.NumberOfPeriods = 3; - List responses = averages.Calculate(); + List responses = averages.Calculate(); Assert.AreEqual(7, responses.Count); Assert.AreEqual(DateTime.Now.AddDays(2).Date, responses[0].ActivityDate); @@ -70,7 +70,7 @@ public void CheckInvalidInterval() averages.ColumnToAvg = "close"; averages.NumberOfPeriods = 100; - List responses = averages.Calculate(); + List responses = averages.Calculate(); Assert.AreEqual(0, responses.Count); } @@ -82,8 +82,9 @@ public void CheckInvalidInterval() [Test] public void CheckAverageCal() - { - decimal response = averages.CalculateSimpleAverage(3, "open"); + { + Averages averageCalc = new(stockHistory, 3, "open"); + decimal response = averageCalc.Calculate(); decimal avg = (decimal)Math.Round((stockHistory[0].GetDecimalValue("Open") + stockHistory[1].GetDecimalValue("Open") + stockHistory[2].GetDecimalValue("Open")) / 3, 2); Assert.AreEqual(avg, response); @@ -92,7 +93,8 @@ public void CheckAverageCal() [Test] public void CheckAverageWithOffset() { - decimal response = averages.CalculateSimpleAverage(3, "open", 2); + Averages averageCalc = new(stockHistory, 3, "open", 2); + decimal response = averageCalc.Calculate(); decimal avg = (decimal)Math.Round((stockHistory[2].GetDecimalValue("Open") + stockHistory[3].GetDecimalValue("Open") + stockHistory[4].GetDecimalValue("Open")) / 3, 2); Assert.AreEqual(avg, response); diff --git a/CoreTests/TrueRangeTest.cs b/CoreTests/TrueRangeTest.cs new file mode 100644 index 0000000..80dd9f5 --- /dev/null +++ b/CoreTests/TrueRangeTest.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StockTracker.Core.Calculations; +using StockTracker.Core.Domain; +using NUnit.Framework; + +namespace StockTracker.CoreTests +{ + public class TrueRangeTest + { + List quotes; + + [SetUp] + public void Setup() + { + quotes = new List{ + new Quote (DateTime.Now, 50, 10, 0, 20, 100), + new Quote (DateTime.Now.AddDays(-1), 30, 10, 0, 25, 100), + new Quote (DateTime.Now.AddDays(-2), 100, 90, 0, 95, 100) + }; + } + + [Test] + public void Calculate_ReturnsAccurateTrueRangeResponseList(){ + + var tr = new TrueRange(quotes); + + //Act + var result = tr.Calculate(); + + //Assert + Assert.IsNotNull(result); + Assert.AreEqual(result.Count,3); + Assert.AreEqual(result[0].TrueRange,40); + Assert.AreEqual(result[1].TrueRange,20); + Assert.AreEqual(result[2].TrueRange,10); + } + [Test] + public void Calculate_GivenEmptyQuoteList_ReturnsEmptyTrueRangeResponseList(){ + //Arrange + List quotes = new List(); + var tr = new TrueRange(quotes); + + //Act + var result = tr.Calculate(); + + //Assert + Assert.IsNotNull(result); + Assert.AreEqual(result.Count,0); + } + [Test] + public void Calculate_GivenNullQuoteList_ThrowsArgumentNullException(){ + //Arrange/Act + var ex = Assert.Throws(() => new TrueRange(null).Calculate()); + + //Assert + Assert.AreEqual(ex.Message,"Value cannot be null. (Parameter 'quotes')"); + } + [Test] + public void Calculate_GivenInvalidQuoteData_ThrowsException(){ + //Arrange + List quotes = new List{ + new Quote (DateTime.Now, -50, 10, 0, 20, 100), + new Quote (DateTime.Now.AddDays(-1), 30, 40, 0, 25, 100), + new Quote (DateTime.Now.AddDays(-2), 100, 95, 0, -80, 100) + }; + var tr = new TrueRange(quotes); + + //Act/Assert + var ex = Assert.Throws(() => tr.Calculate()); + Assert.AreEqual(ex.Message,"Invalid quote data"); + } + + } +} \ No newline at end of file diff --git a/README.md b/README.md index 3adce05..35bb562 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ -# StockTracker \ No newline at end of file +# StockTracker + +A .NET library for calculating common technical analysis indicators for stock analysis. + +This library supports: + +Simple Moving Averages +Exponetial Moving Averages (EMA) +Moving Average Convergence Divergence (MACD) +Realitive Strength Index (RSI) +Hammer Pattern Detection + From 592c429b2e7724e66717ce1d7424502c4ee3df30 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sat, 10 Jun 2023 11:07:27 -0400 Subject: [PATCH 02/13] Slope Code to calculate the direction of prices --- Core/Calculations/Slope.cs | 58 ++++++++++++++++++++++++++ Core/Domain/SlopeData.cs | 7 ++++ Core/Interfaces/Calculations/ISlope.cs | 14 +++++++ 3 files changed, 79 insertions(+) create mode 100644 Core/Calculations/Slope.cs create mode 100644 Core/Domain/SlopeData.cs create mode 100644 Core/Interfaces/Calculations/ISlope.cs diff --git a/Core/Calculations/Slope.cs b/Core/Calculations/Slope.cs new file mode 100644 index 0000000..b45fa69 --- /dev/null +++ b/Core/Calculations/Slope.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using StockTracker.Core.Interfaces; +using StockTracker.Core.Interfaces.Calculations; +using System.Linq; +using StockTracker.Core.Domain; + +namespace StockTracker.Core.Calculations +{ + + public class Slope : BaseCalculator, ISlope + { + public Slope(IList collection) : base((IList)collection) + { + + } + + /// + /// Calculate the slope of the line of best fit for the prices in the SlopeData objects + /// + public decimal Calculate() + { + // Get the list of SlopeData objects + IList slopeData = (IList)this.activities; + + // Calculate the mean (average) of the dates and the prices + // We're converting the DateTime to ticks (a long representing 100-nanosecond intervals since 0001-01-01) + // because we need a numerical representation of the dates to calculate the slope + decimal datesMean = (decimal)slopeData.Select(x => x.ActivityDate.Ticks).Average(); + decimal emaPricesMean = slopeData.Select(x=> x.Price).Average(); + + // These variables will hold the sums that we need to calculate the slope + decimal sumNumerator = 0.0m; + decimal sumDenominator = 0.0m; + + // Loop over the SlopeData objects + for (int i = 0; i < slopeData.Count; i++) + { + // Calculate the difference between the current date and the mean date, + // and the difference between the current price and the mean price + decimal dateDiff = slopeData[i].ActivityDate.Ticks - datesMean; + decimal emaPriceDiff = slopeData[i].Price - emaPricesMean; + + // Add to the sums for the numerator and denominator of the slope formula + sumNumerator += dateDiff * emaPriceDiff; + sumDenominator += dateDiff * dateDiff; + } + + // Calculate the slope by dividing the numerator sum by the denominator sum + // This gives us the slope of the line of best fit for the prices over time + decimal slope = sumNumerator / sumDenominator; + + // Return the slope, which indicates the overall direction of prices + // A positive slope indicates an upward trend, and a negative slope indicates a downward trend + return slope; + } + + } +} \ No newline at end of file diff --git a/Core/Domain/SlopeData.cs b/Core/Domain/SlopeData.cs new file mode 100644 index 0000000..e7f1d4f --- /dev/null +++ b/Core/Domain/SlopeData.cs @@ -0,0 +1,7 @@ +namespace StockTracker.Core.Domain +{ + public class SlopeData:BaseObject + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Core/Interfaces/Calculations/ISlope.cs b/Core/Interfaces/Calculations/ISlope.cs new file mode 100644 index 0000000..2b6d71c --- /dev/null +++ b/Core/Interfaces/Calculations/ISlope.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace StockTracker.Core.Interfaces.Calculations +{ + public interface ISlope + { + + public decimal Calculate(); + + } + + +} From 2aeccdf2694de7bc7d80a29ad755f713e02bef5a Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sat, 10 Jun 2023 11:08:22 -0400 Subject: [PATCH 03/13] Update TrueRange.cs Refactor --- Core/Calculations/TrueRange.cs | 44 ++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/Core/Calculations/TrueRange.cs b/Core/Calculations/TrueRange.cs index 0962d9b..68226ec 100644 --- a/Core/Calculations/TrueRange.cs +++ b/Core/Calculations/TrueRange.cs @@ -2,33 +2,35 @@ using System.Collections.Generic; using StockTracker.Core.Domain; using StockTracker.Core.Interfaces.Calculations; +namespace StockTracker.Core.Calculations +{ + public class TrueRange:ICalculate { + private List _quotes; -public class TrueRange:ICalculate { - private List _quotes; + public TrueRange(List quotes){ + _quotes = quotes; + } + + public List Calculate(){ + List trueRangeResponses = new List(); + decimal HighMinusLow = 0, + HighMinusPreviousClose = 0, + LowMinusPreviousClose = 0, + TrueRange = 0; - public TrueRange(List quotes){ - _quotes = quotes; - } - - public List Calculate(){ - List trueRangeResponses = new List(); - decimal HighMinusLow = 0, - HighMinusPreviousClose = 0, - LowMinusPreviousClose = 0, - TrueRange = 0; + foreach(var quote in _quotes){ + HighMinusLow = quote.High - quote.Low; + HighMinusPreviousClose = Math.Abs( quote.High - quote.Close); + LowMinusPreviousClose = Math.Abs(quote.Low - quote.Close); - foreach(var quote in _quotes){ - HighMinusLow = quote.High - quote.Low; - HighMinusPreviousClose = Math.Abs( quote.High - quote.Close); - LowMinusPreviousClose = Math.Abs(quote.Low - quote.Close); + TrueRange = HighMinusLow > HighMinusPreviousClose ? HighMinusLow : HighMinusPreviousClose; + TrueRange = TrueRange > LowMinusPreviousClose ? TrueRange : LowMinusPreviousClose; - TrueRange = HighMinusLow > HighMinusPreviousClose ? HighMinusLow : HighMinusPreviousClose; - TrueRange = TrueRange > LowMinusPreviousClose ? TrueRange : LowMinusPreviousClose; + trueRangeResponses.Add(new TrueRangeResponse(quote.ActivityDate, TrueRange)); + } - trueRangeResponses.Add(new TrueRangeResponse(quote.ActivityDate, TrueRange)); + return trueRangeResponses; } - return trueRangeResponses; } - } \ No newline at end of file From 36f40fd521de4cf01f822db6cc5ae425b461d6bc Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sat, 10 Jun 2023 11:13:32 -0400 Subject: [PATCH 04/13] Create SlopeTests.cs Unit Tests for Slope --- CoreTests/SlopeTests.cs | 109 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 CoreTests/SlopeTests.cs diff --git a/CoreTests/SlopeTests.cs b/CoreTests/SlopeTests.cs new file mode 100644 index 0000000..1fdf405 --- /dev/null +++ b/CoreTests/SlopeTests.cs @@ -0,0 +1,109 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using StockTracker.Core.Calculations; +using StockTracker.Core.Domain; + +namespace StockTracker.Core.Tests +{ + [TestFixture] + public class SlopeTests + { + private readonly decimal delta = 0.0001m; + + [Test] + public void TestCalculate_WithIncreasingPrices_ReturnsPositiveSlope() + { + // Arrange + var slopeData = new List() + { + new SlopeData() + { + ActivityDate = new DateTime(2021, 1, 1), + Price = 100.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 2, 1), + Price = 110.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 3, 1), + Price = 120.0m + } + }; + var slope = new Slope(slopeData); + + // Act + decimal result = slope.Calculate(); + + // Assert + Assert.That(result, Is.GreaterThan(0)); + Assert.That(result, Is.EqualTo(10.0m).Within(delta)); + } + + [Test] + public void TestCalculate_WithDecreasingPrices_ReturnsNegativeSlope() + { + // Arrange + var slopeData = new List() + { + new SlopeData() + { + ActivityDate = new DateTime(2021, 1, 1), + Price = 120.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 2, 1), + Price = 110.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 3, 1), + Price = 100.0m + } + }; + var slope = new Slope(slopeData); + + // Act + decimal result = slope.Calculate(); + + // Assert + Assert.That(result, Is.LessThan(0)); + Assert.That(result, Is.EqualTo(-10.0m).Within(delta)); + } + + [Test] + public void TestCalculate_WithConstantPrices_ReturnsZeroSlope() + { + // Arrange + var slopeData = new List() + { + new SlopeData() + { + ActivityDate = new DateTime(2021, 1, 1), + Price = 100.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 2, 1), + Price = 100.0m + }, + new SlopeData() + { + ActivityDate = new DateTime(2021, 3, 1), + Price = 100.0m + } + }; + var slope = new Slope(slopeData); + + // Act + decimal result = slope.Calculate(); + + // Assert + Assert.That(result, Is.EqualTo(0).Within(delta)); + } + } +} \ No newline at end of file From 50b5718a60f05e366cabe24e60f7025572acb08d Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Fri, 16 Jun 2023 19:51:59 -0400 Subject: [PATCH 05/13] Added Price Direction Class --- .vscode/launch.json | 26 ++++++++++++++++++++++++ .vscode/tasks.json | 41 ++++++++++++++++++++++++++++++++++++++ Core/Calculations/Slope.cs | 6 ++++-- Core/Domain/SlopeData.cs | 13 ++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1953d52 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/CoreTests/bin/Debug/net6.0/StockTracker.Core.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/CoreTests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..34587ff --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/CoreTests/StockTracker.Core.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/CoreTests/StockTracker.Core.Tests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/CoreTests/StockTracker.Core.Tests.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Core/Calculations/Slope.cs b/Core/Calculations/Slope.cs index b45fa69..22dcc02 100644 --- a/Core/Calculations/Slope.cs +++ b/Core/Calculations/Slope.cs @@ -9,18 +9,20 @@ namespace StockTracker.Core.Calculations public class Slope : BaseCalculator, ISlope { - public Slope(IList collection) : base((IList)collection) + public Slope(IList collection) : base(collection.Cast().ToList()) { } + /// /// Calculate the slope of the line of best fit for the prices in the SlopeData objects + /// Data needs to be in ascending order by date for the calculation to be correct /// public decimal Calculate() { // Get the list of SlopeData objects - IList slopeData = (IList)this.activities; + IList slopeData = this.activities.Cast().ToList(); // Calculate the mean (average) of the dates and the prices // We're converting the DateTime to ticks (a long representing 100-nanosecond intervals since 0001-01-01) diff --git a/Core/Domain/SlopeData.cs b/Core/Domain/SlopeData.cs index e7f1d4f..214cdda 100644 --- a/Core/Domain/SlopeData.cs +++ b/Core/Domain/SlopeData.cs @@ -1,7 +1,20 @@ +using System; +using StockTracker.Core.Interfaces; + namespace StockTracker.Core.Domain { public class SlopeData:BaseObject { + public SlopeData() + { + } + + public SlopeData(DateTime date, decimal price) + { + ActivityDate = date; + Price = price; + } + public decimal Price { get; set; } } } \ No newline at end of file From 993a34e1d5c1f9219e19eff9e1be9b9ea70e1cac Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sat, 17 Jun 2023 22:10:19 -0400 Subject: [PATCH 06/13] Directional Movement Calculator --- Core/Calculations/DirectionalMovement.cs | 43 +++++++++++++++++++ .../Response/DirectionalResponse.cs | 15 +++++++ Core/Domain/DirectionalMovementData.cs | 21 +++++++++ CoreTests/DirectionMovementTest.cs | 36 ++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 Core/Calculations/DirectionalMovement.cs create mode 100644 Core/Calculations/Response/DirectionalResponse.cs create mode 100644 Core/Domain/DirectionalMovementData.cs create mode 100644 CoreTests/DirectionMovementTest.cs diff --git a/Core/Calculations/DirectionalMovement.cs b/Core/Calculations/DirectionalMovement.cs new file mode 100644 index 0000000..0f13bb5 --- /dev/null +++ b/Core/Calculations/DirectionalMovement.cs @@ -0,0 +1,43 @@ +using System; +using StockTracker.Core.Calculations.Response; +using StockTracker.Core.Domain; +using StockTracker.Core.Interfaces.Calculations; +using System.Collections.Generic; + +namespace StockTracker.Core.Calculations +{ + + public class DirectionalMovement :ICalculate + { + private IList _directionalMovementData; + + public DirectionalMovement(IList directionalMovementData){ + _directionalMovementData = directionalMovementData; + } + + public List Calculate() + { + List directionalResponses = new List(); + + foreach (var highLowData in _directionalMovementData) + { + directionalResponses.Add(CalculateDirectionalMovement(highLowData)); + } + + return directionalResponses; + } + + private DirectionalResponse CalculateDirectionalMovement(DirectionalMovementData directionalMovement) + { + decimal HighMinusLow = directionalMovement.High - directionalMovement.Low; + decimal HighMinusPreviousHigh = Math.Abs(directionalMovement.High - directionalMovement.PreviousHigh); + decimal PreviousLowMinusLow = Math.Abs(directionalMovement.PreviousLow - directionalMovement.Low); + + decimal PositiveDirectionalMovement = HighMinusLow > HighMinusPreviousHigh ? HighMinusLow : HighMinusPreviousHigh; + decimal NegativeDirectionalMovement = HighMinusLow > PreviousLowMinusLow ? HighMinusLow : PreviousLowMinusLow; + + return new DirectionalResponse(directionalMovement.ActivityDate, PositiveDirectionalMovement, NegativeDirectionalMovement); + } + } + +} \ No newline at end of file diff --git a/Core/Calculations/Response/DirectionalResponse.cs b/Core/Calculations/Response/DirectionalResponse.cs new file mode 100644 index 0000000..b066325 --- /dev/null +++ b/Core/Calculations/Response/DirectionalResponse.cs @@ -0,0 +1,15 @@ +using System; + +namespace StockTracker.Core.Calculations.Response{ + public class DirectionalResponse:BaseResponse{ + public DirectionalResponse(DateTime activityDate, decimal positiveDirectionalMovement, decimal negativeDirectionalMovement) + { + ActivityDate = activityDate; + PositiveDirectionalMovement = positiveDirectionalMovement; + NegativeDirectionalMovement = negativeDirectionalMovement; + } + + public decimal PositiveDirectionalMovement { get; set; } + public decimal NegativeDirectionalMovement { get; set; } + } +} \ No newline at end of file diff --git a/Core/Domain/DirectionalMovementData.cs b/Core/Domain/DirectionalMovementData.cs new file mode 100644 index 0000000..c56026b --- /dev/null +++ b/Core/Domain/DirectionalMovementData.cs @@ -0,0 +1,21 @@ +using System; +namespace StockTracker.Core.Domain +{ + public class DirectionalMovementData:BaseObject + { + public DirectionalMovementData(DateTime ActivityDate, decimal High, decimal Low, decimal PreviousHigh, decimal PreviousLow) + { + this.ActivityDate = ActivityDate; + this.High = High; + this.Low = Low; + this.PreviousHigh = PreviousHigh; + this.PreviousLow = PreviousLow; + } + + public decimal High { get; set; } + public decimal Low { get; set; } + public decimal PreviousHigh { get; set; } + public decimal PreviousLow { get; set; } + } + +} \ No newline at end of file diff --git a/CoreTests/DirectionMovementTest.cs b/CoreTests/DirectionMovementTest.cs new file mode 100644 index 0000000..daa154c --- /dev/null +++ b/CoreTests/DirectionMovementTest.cs @@ -0,0 +1,36 @@ +using NUnit.Framework; +using StockTracker.Core.Calculations; +using StockTracker.Core.Calculations.Response; +using StockTracker.Core.Domain; +using System; +using System.Collections.Generic; + +namespace StockTracker.Core.UnitTests.Calculations +{ + public class DirectionalMovementTests + { + [Test] + public void Calculate_ReturnsListOfDirectionalResponseWithCorrectValues() + { + // Arrange + var directionalMovementData = new List + { + new DirectionalMovementData( DateTime.Now.Date, 100m, 50m, 90m, 40m), + new DirectionalMovementData( DateTime.Now.Date.AddDays(-1), 110m, 60m, 100m, 50m) + }; + var sut = new DirectionalMovement(directionalMovementData); + + // Act + var result = sut.Calculate(); + + // Assert + Assert.AreEqual(result[0].ActivityDate, DateTime.Now.Date.AddDays(-1)); + Assert.AreEqual(result[0].PositiveDirectionalMovement, 60); + Assert.AreEqual(result[0].NegativeDirectionalMovement, 10); + Assert.AreEqual(result[1].ActivityDate, DateTime.Now.Date); + Assert.AreEqual(result[1].PositiveDirectionalMovement, 50); + Assert.AreEqual(result[1].NegativeDirectionalMovement, 10); + } + + } +} From 2b6ddb51a4ec8459628519db8333f074c1ce6b88 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sun, 18 Jun 2023 19:20:00 -0400 Subject: [PATCH 07/13] Smoothing Library Added Smoothing Library for calculating ADX --- Core/Calculations/SmoothingForADX.cs | 95 ++++++++++++++++++++++++++++ Core/Domain/SmoothTRData.cs | 15 +++++ 2 files changed, 110 insertions(+) create mode 100644 Core/Calculations/SmoothingForADX.cs create mode 100644 Core/Domain/SmoothTRData.cs diff --git a/Core/Calculations/SmoothingForADX.cs b/Core/Calculations/SmoothingForADX.cs new file mode 100644 index 0000000..4829ad4 --- /dev/null +++ b/Core/Calculations/SmoothingForADX.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using StockTracker.Core.Calculations.Response; +using StockTracker.Core.Interfaces; +using StockTracker.Core.Interfaces.Calculations; + +namespace StockTracker.Core.Calculations +{ + public class SmoothingForADX:BaseCalculator, ICalculate { + private ushort _numberOfPeriods; + private string _columnToSmooth, _columnPrevValue; + private int startPosition = 0; + + /// + /// Intialize Object + /// + /// Data to calculate averages from + public SmoothingForADX(IList activities) : base(activities) + { + } + + /// + /// The number of items to average + /// + public ushort NumberOfPeriods { get => _numberOfPeriods; set => _numberOfPeriods = value; } + /// + /// String that represents the name of the property to calculate the average from + /// + public string ColumnToSmooth { get => _columnToSmooth; set => _columnToSmooth = value; } + /// + /// String that represents the name of the property to retrieve the previous periods EMA from. + /// Looks for this value in the previous row. + /// Set field to 0 if it has never been calculated. + /// + public string ColumnPrevSmoothedValue { get => _columnPrevValue; set => _columnPrevValue = value; } + /// + /// The poisition int the array to start calculating. Defaults to 0. + /// + public int StartPosition { get => startPosition; set => startPosition = value; } + + + /// + /// Calculates a weighted average giving more weight to current price movements + /// + /// A list of averages + public List Calculate() + { + List responses = new(); + + int startPos = StartPosition; + + decimal prevSymbol = activities[startPos].GetDecimalValue(ColumnPrevSmoothedValue); + + //never been calculated before + if (prevSymbol == 0) + { + //Check to see if the array has enough data to calculate an average + if (!ArrayValidforAverage(_numberOfPeriods, _columnToSmooth)) return responses; + + //if no EMA exists calculate a simple average as start + //and place it into the prevEma variable for the next calculation + Averages simpleAverage = new(activities, _numberOfPeriods, _columnToSmooth); + prevSymbol = simpleAverage.Calculate(); + startPos = _numberOfPeriods; // Move index to correct position in the array + } + else + { + startPos = 1; + } + + return SmoothValues(startPos, activities.Count, _columnToSmooth, prevSymbol); + } + + private static decimal Smooth(decimal currentValue, decimal previous) + { + //Create the weighted Smoothed Average + return ((previous *13) + currentValue) / 14; + } + + private List SmoothValues(int start, int end, string columnToAverage, decimal lastValue) + { + List responses = new(); + decimal holdValue = lastValue; + + for(int i = start; i < end; i++) { + //Create the weighted Average + decimal ema = Smooth(activities[i].GetDecimalValue(columnToAverage), holdValue); + holdValue = ema; + responses.Add(new AverageResponse(activities[i].ActivityDate, ema)); + } + + return responses; + } + } +} diff --git a/Core/Domain/SmoothTRData.cs b/Core/Domain/SmoothTRData.cs new file mode 100644 index 0000000..4e813da --- /dev/null +++ b/Core/Domain/SmoothTRData.cs @@ -0,0 +1,15 @@ +using System; +using StockTracker.Core.Calculations.Response; + +namespace StockTracker.Core.Domain{ + public class SmoothTRData:BaseObject{ + public SmoothTRData(DateTime date, decimal trueRangeValue, decimal smoothedTrueRangeValue){ + ActivityDate = date; + TrueRange = trueRangeValue; + SmoothedTrueRange = smoothedTrueRangeValue; + } + public SmoothTRData(){} + public decimal TrueRange { get; set; } + public decimal SmoothedTrueRange { get; set; } + } +} \ No newline at end of file From 085eb4f62ddb98c9f94ca5af6c9f15423d651342 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sun, 18 Jun 2023 19:52:20 -0400 Subject: [PATCH 08/13] Fixed Broken Unit Tests Fixed Unit Tests --- Core/Calculations/DirectionalMovement.cs | 25 ++++++++++++++++++------ CoreTests/DirectionMovementTest.cs | 10 +++++----- CoreTests/MovingAverageTests.cs | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Core/Calculations/DirectionalMovement.cs b/Core/Calculations/DirectionalMovement.cs index 0f13bb5..8a43473 100644 --- a/Core/Calculations/DirectionalMovement.cs +++ b/Core/Calculations/DirectionalMovement.cs @@ -3,6 +3,7 @@ using StockTracker.Core.Domain; using StockTracker.Core.Interfaces.Calculations; using System.Collections.Generic; +using System.Linq; namespace StockTracker.Core.Calculations { @@ -12,7 +13,9 @@ public class DirectionalMovement :ICalculate private IList _directionalMovementData; public DirectionalMovement(IList directionalMovementData){ - _directionalMovementData = directionalMovementData; + _directionalMovementData = (from dm in directionalMovementData + orderby dm.ActivityDate + select dm).ToList(); } public List Calculate() @@ -29,15 +32,25 @@ public List Calculate() private DirectionalResponse CalculateDirectionalMovement(DirectionalMovementData directionalMovement) { - decimal HighMinusLow = directionalMovement.High - directionalMovement.Low; - decimal HighMinusPreviousHigh = Math.Abs(directionalMovement.High - directionalMovement.PreviousHigh); - decimal PreviousLowMinusLow = Math.Abs(directionalMovement.PreviousLow - directionalMovement.Low); + decimal HighMinusPreviousHigh = directionalMovement.High - directionalMovement.PreviousHigh; + decimal PreviousLowMinusLow = directionalMovement.PreviousLow - directionalMovement.Low; - decimal PositiveDirectionalMovement = HighMinusLow > HighMinusPreviousHigh ? HighMinusLow : HighMinusPreviousHigh; - decimal NegativeDirectionalMovement = HighMinusLow > PreviousLowMinusLow ? HighMinusLow : PreviousLowMinusLow; + decimal PositiveDirectionalMovement = 0M; + decimal NegativeDirectionalMovement = 0M; + + if (HighMinusPreviousHigh > 0 && HighMinusPreviousHigh > PreviousLowMinusLow) + { + PositiveDirectionalMovement = HighMinusPreviousHigh; + } + + if (PreviousLowMinusLow > 0 && PreviousLowMinusLow > HighMinusPreviousHigh) + { + NegativeDirectionalMovement = PreviousLowMinusLow; + } return new DirectionalResponse(directionalMovement.ActivityDate, PositiveDirectionalMovement, NegativeDirectionalMovement); } + } } \ No newline at end of file diff --git a/CoreTests/DirectionMovementTest.cs b/CoreTests/DirectionMovementTest.cs index daa154c..f284521 100644 --- a/CoreTests/DirectionMovementTest.cs +++ b/CoreTests/DirectionMovementTest.cs @@ -15,7 +15,7 @@ public void Calculate_ReturnsListOfDirectionalResponseWithCorrectValues() // Arrange var directionalMovementData = new List { - new DirectionalMovementData( DateTime.Now.Date, 100m, 50m, 90m, 40m), + new DirectionalMovementData( DateTime.Now.Date, 100m, 50m, 90m, 70m), new DirectionalMovementData( DateTime.Now.Date.AddDays(-1), 110m, 60m, 100m, 50m) }; var sut = new DirectionalMovement(directionalMovementData); @@ -25,11 +25,11 @@ public void Calculate_ReturnsListOfDirectionalResponseWithCorrectValues() // Assert Assert.AreEqual(result[0].ActivityDate, DateTime.Now.Date.AddDays(-1)); - Assert.AreEqual(result[0].PositiveDirectionalMovement, 60); - Assert.AreEqual(result[0].NegativeDirectionalMovement, 10); + Assert.AreEqual(result[0].PositiveDirectionalMovement, 10); + Assert.AreEqual(result[0].NegativeDirectionalMovement, 0); Assert.AreEqual(result[1].ActivityDate, DateTime.Now.Date); - Assert.AreEqual(result[1].PositiveDirectionalMovement, 50); - Assert.AreEqual(result[1].NegativeDirectionalMovement, 10); + Assert.AreEqual(result[1].PositiveDirectionalMovement, 0); + Assert.AreEqual(result[1].NegativeDirectionalMovement, 20); } } diff --git a/CoreTests/MovingAverageTests.cs b/CoreTests/MovingAverageTests.cs index 7665a3e..d663ab8 100644 --- a/CoreTests/MovingAverageTests.cs +++ b/CoreTests/MovingAverageTests.cs @@ -49,7 +49,7 @@ public void ChecMovingkAverageCal() List responses = averages.Calculate(); - Assert.AreEqual(7, responses.Count); + Assert.AreEqual(8, responses.Count); Assert.AreEqual(DateTime.Now.AddDays(2).Date, responses[0].ActivityDate); decimal avg = (decimal) Math.Round( (stockHistory[0].GetDecimalValue("Close") + stockHistory[1].GetDecimalValue("Close") + stockHistory[2].GetDecimalValue("Close")) / 3,2); From 731735a9b7c6cd598fbd3f119e15d7c83bb84184 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Sun, 18 Jun 2023 20:27:44 -0400 Subject: [PATCH 09/13] Update EMATest.cs Fixed Broken Tests --- CoreTests/EMATest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CoreTests/EMATest.cs b/CoreTests/EMATest.cs index 1923f37..9e0355a 100644 --- a/CoreTests/EMATest.cs +++ b/CoreTests/EMATest.cs @@ -63,9 +63,9 @@ public void CheckExponetialMovingkAverageWithHistory() List responses = averages.Calculate(); - Assert.AreEqual(15, responses.Count); - Assert.AreEqual(new DateTime(2017, 12, 29, 0, 0, 0), responses[6].ActivityDate); - Assert.AreEqual((decimal)37.66, Math.Round(responses[6].GetDecimalValue("Value"), 2)); + Assert.AreEqual(11, responses.Count); + Assert.AreEqual(new DateTime(2018, 01, 05, 0, 0, 0), responses[6].ActivityDate); + Assert.AreEqual(39.47m, Math.Round(responses[6].GetDecimalValue("Value"), 2)); } catch (Exception e) From 786e2456edf9c8d76d2b09214f42da44fcf14b21 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Tue, 20 Jun 2023 15:15:56 -0400 Subject: [PATCH 10/13] Update README.md Updates to Readme --- README.md | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 35bb562..3d19205 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,30 @@ # StockTracker -A .NET library for calculating common technical analysis indicators for stock analysis. +A .NET library for calculating common technical indicators for stock analysis. Library is currently in .NET 6. -This library supports: +## Available Indicators -Simple Moving Averages -Exponetial Moving Averages (EMA) -Moving Average Convergence Divergence (MACD) -Realitive Strength Index (RSI) -Hammer Pattern Detection +- Simple Moving Averages +- Exponetial Moving Averages (EMA) +- Moving Average Convergence Divergence (MACD) +- Realitive Strength Index (RSI) +- Slope (Price Direction) +- True Range +- Directional Movement +## Available Candlestick Patterns +- Hammer Pattern Detection + +## Installation + +## Project Structure + +L Core + L Analyzers - This directory contains Candlestick Patterns + L Calculators - Classes that calculate the values for a technical indicator + L Response - Classes that define the return from calculators + L Domain - Data Structures to use for inputs into calculators + L Interfaces - Contracts implemented by the Calculators +L CoreTests - NUnit testing suite for the Core components + +## Building the Project From 16bcc6f2e77dbd6cf26be988901f5094b358f049 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Thu, 18 Jul 2024 18:20:51 -0400 Subject: [PATCH 11/13] Updated to .NET 8 --- .vscode/launch.json | 2 +- .vscode/settings.json | 1 + Core/.vscode/settings.json | 1 + Core/Analyzers/SupportResistance.cs | 2 +- Core/Calculations/BaseCalculator.cs | 7 +- Core/Calculations/Macd.cs | 8 ++ Core/Calculations/RealitiveStrengthIndex.cs | 139 +++++--------------- Core/Calculations/Slope.cs | 3 +- Core/Core.sln | 25 ++++ Core/StockTracker.Core.csproj | 2 +- CoreTests/RSITests.cs | 4 +- CoreTests/StockTracker.Core.Tests.csproj | 2 +- 12 files changed, 80 insertions(+), 116 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 Core/.vscode/settings.json create mode 100644 Core/Core.sln diff --git a/.vscode/launch.json b/.vscode/launch.json index 1953d52..f577f0a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/CoreTests/bin/Debug/net6.0/StockTracker.Core.Tests.dll", + "program": "${workspaceFolder}/CoreTests/bin/Debug/net8.0/StockTracker.Core.Tests.dll", "args": [], "cwd": "${workspaceFolder}/CoreTests", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Core/.vscode/settings.json b/Core/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/Core/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Core/Analyzers/SupportResistance.cs b/Core/Analyzers/SupportResistance.cs index ac4eff1..227430e 100644 --- a/Core/Analyzers/SupportResistance.cs +++ b/Core/Analyzers/SupportResistance.cs @@ -14,7 +14,7 @@ public SupportResistance() private Decimal[] valuesArray; private Decimal factor = 0.05M; - public List SeccurityActivity { + public List SecurityActivity { get { return activities; diff --git a/Core/Calculations/BaseCalculator.cs b/Core/Calculations/BaseCalculator.cs index a18927e..d39f2c9 100644 --- a/Core/Calculations/BaseCalculator.cs +++ b/Core/Calculations/BaseCalculator.cs @@ -22,13 +22,14 @@ public void Initialize() // Initialize the calculator } - protected bool ArrayValidforAverage(int requiredNumberOfElements, string columnToAverage) + protected virtual bool ArrayValidforAverage(int requiredNumberOfElements, string columnToAverage) { - // Is the array too small? - if (activities.Count < requiredNumberOfElements || requiredNumberOfElements == 0) return false; // Is the column name blank if (String.IsNullOrEmpty(columnToAverage)) return false; + // Is the array too small? + if (activities.Count < requiredNumberOfElements || requiredNumberOfElements == 0) return false; + return true; } diff --git a/Core/Calculations/Macd.cs b/Core/Calculations/Macd.cs index f3b651d..28d759a 100644 --- a/Core/Calculations/Macd.cs +++ b/Core/Calculations/Macd.cs @@ -93,6 +93,14 @@ public List CreateResponse() return responses; } + protected override bool ArrayValidforAverage(int requiredNumberOfElements, string columnToAverage) + { + //Array preloaded with data + if (activities[0].GetDecimalValue(EMA12Column) > 0 && activities[0].GetDecimalValue(EMA26Column) > 0) return true; + + return base.ArrayValidforAverage(requiredNumberOfElements, columnToAverage); + } + /// /// Calculates missing values for 12, 26 EMAs /// diff --git a/Core/Calculations/RealitiveStrengthIndex.cs b/Core/Calculations/RealitiveStrengthIndex.cs index 46404df..f94132c 100644 --- a/Core/Calculations/RealitiveStrengthIndex.cs +++ b/Core/Calculations/RealitiveStrengthIndex.cs @@ -1,136 +1,63 @@ using System; -using System.Collections; using System.Collections.Generic; using StockTracker.Core.Domain; -using StockTracker.Core.Interfaces; namespace StockTracker.Core.Calculations { - public class RealitiveStrengthIndex + public class RelativeStrengthIndex { protected readonly IList dataList; + private const int Periods = 14; // Standard period for RSI calculation - /// - /// Creates an instatnce of the RSI caluclation library - /// - /// - /// A list of RSI objects to update. If - /// no previous RSI data exists this object should have just its - /// activity date and close price populated. - /// - public RealitiveStrengthIndex(IList rsiData) + public RelativeStrengthIndex(IList rsiData) { - dataList = (IList) rsiData; + dataList = rsiData; } - /// - /// Calculate RSI based on the data provided in the list - /// public void Calculate() { - int itemCount = dataList.Count; + if (dataList.Count < Periods) return; // Ensure enough data - //Not enough data - if (itemCount < 1) return; - - CalculateGainLoss(); - CalculateIndex(); + CalculateInitialAverages(); + CalculateSubsequentValues(); } - /// - /// Updates the RSI list with the difference between the current periods - /// close and the previous periods close - /// - private void CalculateGainLoss() + private void CalculateInitialAverages() { - int itemCount = dataList.Count; - - for (int i = 1; i < itemCount; i++) + decimal totalGain = 0, totalLoss = 0; + for (int i = 1; i <= Periods; i++) { - RelativeStrength rSI = dataList[i]; - - //If both are zero the entry has never been set or trading was - //flata - if (rSI.Gain == 0 && rSI.Loss == 0) - { - decimal gl = (decimal)Math.Round(rSI.Close - dataList[i - 1].Close, 2); - - //Negative number signals loss - if (gl < 0) - { - rSI.Loss = Math.Abs(gl); - continue; - } - - //We don't want to record items where the difference is 0 - //So we test again - if (gl > 0) - { - rSI.Gain = gl; - } - } + var change = dataList[i].Close - dataList[i - 1].Close; + if (change > 0) totalGain += change; + else totalLoss -= change; // Losses are positive numbers } + + dataList[Periods - 1].AvgGain = totalGain / Periods; + dataList[Periods - 1].AvgLoss = totalLoss / Periods; } - private void CalculateIndex() + private void CalculateSubsequentValues() { - int itemCount = dataList.Count; - - for (int i = 0; i < itemCount; i++) + for (int i = Periods; i < dataList.Count; i++) { - RelativeStrength rSI = dataList[i]; - - //never been calculated before - if(i == 0 && rSI.AvgGain == 0 && rSI.AvgLoss == 0) - { - if (itemCount < 14) break; //Not enough data + var change = dataList[i].Close - dataList[i - 1].Close; + var gain = change > 0 ? change : 0; + var loss = change < 0 ? -change : 0; - //Since we don't have any previous data we start with a simple Average - Averages averages = new (ConvertArrayForAvg(), 14,"Gain"); - //Advance to the correct place in the array - //This is placing the average for the first 14 days in the 15 position - i = 14; - dataList[i].AvgGain = (decimal)Math.Round(averages.Calculate(),2); - averages = new (ConvertArrayForAvg(), 14, "Loss"); - dataList[i].AvgLoss = (decimal)Math.Round(averages.Calculate(),2); + // Apply smoothing formula + dataList[i].AvgGain = (dataList[i - 1].AvgGain * (Periods - 1) + gain) / Periods; + dataList[i].AvgLoss = (dataList[i - 1].AvgLoss * (Periods - 1) + loss) / Periods; - continue; + if (dataList[i].AvgLoss == 0) + { + dataList[i].RSIndex = 100; // If no losses, RSI is 100 + } + else + { + var rs = dataList[i].AvgGain / dataList[i].AvgLoss; + dataList[i].RSIndex = 100 - (100 / (1 + rs)); } - - dataList[i].AvgGain = CalulateWeightedAverage("AvgGain", "Gain", i); - dataList[i].AvgLoss = CalulateWeightedAverage("AvgLoss", "Loss", i); - - dataList[i].RSIndex = CalculateRsi(i); - } - } - - private decimal CalulateWeightedAverage(string AvgColumnName, string ColumnName, int postion) - { - return (decimal)Math.Round((((dataList[postion - 1].GetDecimalValue(AvgColumnName) * 13) + dataList[postion].GetDecimalValue(ColumnName)) / 14),2); - } - - private decimal CalculateRsi(int idx) - { - decimal rs = dataList[idx].AvgGain / dataList[idx].AvgLoss; - - //Convert Relative Strength into a number between 0 and 100 - return (decimal) Math.Round(100 - (100 / (rs + 1)), 0); - } - - private List ConvertArrayForAvg() - { - List tradingStructures = new(14); - short counter = 0; - - foreach(RelativeStrength rSI in dataList) - { - if (counter == 14) break; - - tradingStructures.Add(rSI); - counter++; } - - return tradingStructures; } } -} +} \ No newline at end of file diff --git a/Core/Calculations/Slope.cs b/Core/Calculations/Slope.cs index 22dcc02..a7cee71 100644 --- a/Core/Calculations/Slope.cs +++ b/Core/Calculations/Slope.cs @@ -14,10 +14,11 @@ public Slope(IList collection) : base(collection.Cast /// Calculate the slope of the line of best fit for the prices in the SlopeData objects /// Data needs to be in ascending order by date for the calculation to be correct + /// slope = Σ[(xi - x_mean) * (yi - y_mean)] / Σ[(xi - x_mean) ^ 2] /// public decimal Calculate() { diff --git a/Core/Core.sln b/Core/Core.sln new file mode 100644 index 0000000..cee9c86 --- /dev/null +++ b/Core/Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StockTracker.Core", "StockTracker.Core.csproj", "{1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BF4D7048-C901-43DA-B9FF-40682097AC91} + EndGlobalSection +EndGlobal diff --git a/Core/StockTracker.Core.csproj b/Core/StockTracker.Core.csproj index b4721df..a50b3e6 100644 --- a/Core/StockTracker.Core.csproj +++ b/Core/StockTracker.Core.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 StockTracker.Core diff --git a/CoreTests/RSITests.cs b/CoreTests/RSITests.cs index cfff729..a5ce728 100644 --- a/CoreTests/RSITests.cs +++ b/CoreTests/RSITests.cs @@ -10,7 +10,7 @@ namespace StockTracker.CoreTests public class RSITests { private IList rsiList; - private RealitiveStrengthIndex strengthIndex; + private RelativeStrengthIndex strengthIndex; public RSITests() { @@ -34,7 +34,7 @@ public void Setup() ); } - strengthIndex = new RealitiveStrengthIndex((IList)rsiList); + strengthIndex = new RelativeStrengthIndex(rsiList); strengthIndex.Calculate(); } diff --git a/CoreTests/StockTracker.Core.Tests.csproj b/CoreTests/StockTracker.Core.Tests.csproj index 8bcc1ce..36912bb 100644 --- a/CoreTests/StockTracker.Core.Tests.csproj +++ b/CoreTests/StockTracker.Core.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false From d0634d1bd25a1c0018d84a12948f015e01b80330 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Thu, 18 Jul 2024 19:13:57 -0400 Subject: [PATCH 12/13] Delete Core.sln Removed duplicate solution --- Core/Core.sln | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 Core/Core.sln diff --git a/Core/Core.sln b/Core/Core.sln deleted file mode 100644 index cee9c86..0000000 --- a/Core/Core.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StockTracker.Core", "StockTracker.Core.csproj", "{1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1614D27B-8CDF-4B2D-B3C8-C5294DAD6D78}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {BF4D7048-C901-43DA-B9FF-40682097AC91} - EndGlobalSection -EndGlobal From 3e006d6a8324ae1a4e248d95e542639d5d720aa9 Mon Sep 17 00:00:00 2001 From: Randy Costner Date: Thu, 18 Jul 2024 19:14:30 -0400 Subject: [PATCH 13/13] Unit Tests Fixed two tests that were failing because of rounding --- CoreTests/RSITests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CoreTests/RSITests.cs b/CoreTests/RSITests.cs index a5ce728..36cef09 100644 --- a/CoreTests/RSITests.cs +++ b/CoreTests/RSITests.cs @@ -64,7 +64,7 @@ public void TestAverageGains() { decimal avgGain = (decimal)Math.Round(((rsiList[14].AvgGain * 13) + rsiList[15].Gain) / 14 , 2); - Assert.AreEqual(avgGain, rsiList[15].AvgGain); + Assert.AreEqual(avgGain, Math.Round(rsiList[15].AvgGain, 2)); } [Test] @@ -72,7 +72,7 @@ public void TestAverageLoss() { decimal avgLoss = (decimal)Math.Round(((rsiList[14].AvgLoss * 13) + rsiList[15].Loss) / 14, 2); - Assert.AreEqual(avgLoss, rsiList[15].AvgLoss); + Assert.AreEqual(avgLoss, Math.Round(rsiList[15].AvgLoss,2)); } } }