In practice, this is what I have found to work even for millions of samples. It computes a running moving average and is faster than any other method I have tried.
public class Sma
{
decimal mult = 0;
private decimal[] samples;
private readonly int max;
private decimal average;
public Sma(int period)
{
mult = 1m / period; //cache to avoid expensive division on each step.
samples = new decimal[period];
max = period - 1;
}
public decimal ComputeAverage(decimal value)
{
average -= samples[max];
var sample = value * mult;
average += sample;
Array.Copy(samples, 0, samples, 1, max);
samples[0] = sample;
return average = average - samples[0];
}
}
I found I often need access to history. I accomplish this by keeping track of the averages:
public class Sma
{
private readonly int max;
private decimal[] history;
public readonly int Period;
public int Counter = -1;
public SimpleSma RunningSma { get; }
public Sma(int period, int maxSamples)
{
this.Period = period;
this.RunningSma = new SimpleSma(period);
max = maxSamples - 1;
history = new decimal[maxSamples];
}
public decimal ComputeAverage(decimal value)
{
Counter++;
Array.Copy(history, 0, history, 1, max);
return history[0] = RunningSma.ComputeAverage(value);
}
public decimal Average => history[0];
public decimal this[int index] => history[index];
public int Length => history.Length;
}
Now in practice, your use case sounds like mine where you need track multiple time frames:
public class MtfSma // MultiTimeFrame Sma
{
public Dictionary<int, Sma> Smas { get; private set; }
public MtfSma(int[] periods, int maxHistorySize = 100)
{
Smas = periods.ToDictionary(x=> x, x=> new Sma(x, maxHistorySize));
}
}
A dictionary is no necessary, but is helpful to map an Sma to its period.
This can be used as follows:
IEnumerable<decimal> dataPoints = new List<Decimal>(); //330 000 data points.
foreach (var dataPoint in dataPoints)
{
foreach (var kvp in Smas)
{
var sma = kvp.Value;
var period = sma.Period;
var average = sma.Average; // or sma[0];
var lastAverage = sma[1];
Console.WriteLine($"Sma{period} [{sma.Counter}]: Current {average.ToString("n2")}, Previous {lastAverage.ToString("n2")}");
}
}
Another point is you can see this is strongly typed to decimal, which means a complete rewrite for other data types.
To handle this the classes can be made generic and use an interface to provide type conversions and the needed arithmetic operation providers.
I have a complete working example of the actual code I use, again for millions upon millions of data points, along with implementations for CrossOver detection, etc on Github here. The code relevant to this question and answer:
public interface INumericOperationsProvider<TNumeric>
where TNumeric : IConvertible
{
TNumeric Divide(TNumeric dividend, TNumeric divisor);
TNumeric Multiply(TNumeric multiplicand, TNumeric multiplier);
TNumeric Add(TNumeric operandA, TNumeric operandB);
TNumeric Subtract(TNumeric operandA, TNumeric operandB);
bool IsLessThan(TNumeric operandA, TNumeric operandB);
bool IsLessThanOrEqual(TNumeric operandA, TNumeric operandB);
bool IsEqual(TNumeric operandA, TNumeric operandB);
bool IsGreaterThanOrEqual(TNumeric operandA, TNumeric operandB);
bool IsGreaterThan(TNumeric operandA, TNumeric operandB);
TNumeric ToNumeric(sbyte value);
TNumeric ToNumeric(short value);
TNumeric ToNumeric(int value);
TNumeric ToNumeric(long value);
TNumeric ToNumeric(byte value);
TNumeric ToNumeric(ushort value);
TNumeric ToNumeric(uint value);
TNumeric ToNumeric(ulong value);
TNumeric ToNumeric(float value);
TNumeric ToNumeric(double value);
TNumeric ToNumeric(decimal value);
TNumeric ToNumeric(IConvertible value);
}
public abstract class OperationsProviderBase<TNumeric>
: INumericOperationsProvider<TNumeric>
where TNumeric : IConvertible
{
private static Type Type = typeof(TNumeric);
public abstract TNumeric Divide(TNumeric dividend, TNumeric divisor);
public abstract TNumeric Multiply(TNumeric multiplicand, TNumeric multiplier);
public abstract TNumeric Add(TNumeric operandA, TNumeric operandB);
public abstract TNumeric Subtract(TNumeric operandA, TNumeric operandB);
public TNumeric ToNumeric(sbyte value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(short value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(int value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(long value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(byte value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(ushort value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(uint value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(ulong value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(float value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(double value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(decimal value) => (TNumeric)Convert.ChangeType(value, Type);
public TNumeric ToNumeric(IConvertible value) => (TNumeric)Convert.ChangeType(value, Type);
public bool IsLessThan(TNumeric operandA, TNumeric operandB)
=> ((IComparable<TNumeric>)operandA).CompareTo(operandB) < 0;
public bool IsLessThanOrEqual(TNumeric operandA, TNumeric operandB)
=> ((IComparable<TNumeric>)operandA).CompareTo(operandB) <= 0;
public bool IsEqual(TNumeric operandA, TNumeric operandB)
=> ((IComparable<TNumeric>)operandA).CompareTo(operandB) == 0;
public bool IsGreaterThanOrEqual(TNumeric operandA, TNumeric operandB)
=> ((IComparable<TNumeric>)operandA).CompareTo(operandB) >= 0;
public bool IsGreaterThan(TNumeric operandA, TNumeric operandB)
=> ((IComparable<TNumeric>)operandA).CompareTo(operandB) > 0;
}
public class OperationsProviderFactory
{
public static OperationsProviderBase<TNumeric> GetProvider<TNumeric>()
where TNumeric : IConvertible
{
var name = typeof(TNumeric).Name;
switch (name)
{
case nameof(Decimal):
return new DecimalOperationsProvider() as OperationsProviderBase<TNumeric>;
case nameof(Single):
return new FloatOperationsProvider() as OperationsProviderBase<TNumeric>;
case nameof(Double):
return new DoubleOperationsProvider() as OperationsProviderBase<TNumeric>;
default:
throw new NotImplementedException();
}
}
}
public class DecimalOperationsProvider : OperationsProviderBase<decimal>
{
public override decimal Add(decimal a, decimal b)
=> a + b;
public override decimal Divide(decimal dividend, decimal divisor)
=> dividend / divisor;
public override decimal Multiply(decimal multiplicand, decimal multiplier)
=> multiplicand * multiplier;
public override decimal Subtract(decimal a, decimal b)
=> a - b;
}
public class FloatOperationsProvider : OperationsProviderBase<float>
{
public override float Add(float a, float b)
=> a + b;
public override float Divide(float dividend, float divisor)
=> dividend / divisor;
public override float Multiply(float multiplicand, float multiplier)
=> multiplicand * multiplier;
public override float Subtract(float a, float b)
=> a - b;
}
public class DoubleOperationsProvider : OperationsProviderBase<double>
{
public override double Add(double a, double b)
=> a + b;
public override double Divide(double dividend, double divisor)
=> dividend / divisor;
public override double Multiply(double multiplicand, double multiplier)
=> multiplicand * multiplier;
public override double Subtract(double a, double b)
=> a - b;
}
public interface ISma<TNumeric>
{
int Count { get; }
void AddSample(TNumeric sample);
void AddSample(IConvertible sample);
TNumeric Average { get; }
TNumeric[] History { get; }
}
public class SmaBase<T> : ISma<T>
where T : IConvertible
{
public int Count { get; private set; }
private int maxLen;
public T[] History { get; private set; }
public T Average { get; private set; } = default(T);
public INumericOperationsProvider<T> OperationsProvider { get; private set; }
public T SampleRatio { get; private set; }
public SmaBase(int count, INumericOperationsProvider<T> operationsProvider = null)
{
if (operationsProvider == null)
operationsProvider = OperationsProviderFactory.GetProvider<T>();
this.Count = count;
this.maxLen = Count - 1;
History = new T[count];
this.OperationsProvider = operationsProvider;
SampleRatio = OperationsProvider.Divide(OperationsProvider.ToNumeric(1), OperationsProvider.ToNumeric(count));
}
public void AddSample(T sample)
{
T sampleValue = OperationsProvider.Multiply(SampleRatio, sample);
if (maxLen==0)
{
History[0] = sample;
Average = sample;
}
else
{
var remValue = OperationsProvider.Multiply(SampleRatio, History[0]);
Average = OperationsProvider.Subtract(Average, remValue);
Average = OperationsProvider.Add(Average, sampleValue);
Array.Copy(History, 1, History, 0, Count - 1);
History[maxLen]= sample;
}
}
public void AddSample(IConvertible sample)
=> AddSample(OperationsProvider.ToNumeric(sample));
}
public class SmaOfDecimal : SmaBase<decimal>
{
public SmaOfDecimal(int count) : base(count)
{
}
}
public class MultiTimeFrameSma<TNumeric>
where TNumeric : IConvertible
{
public Dictionary<int, SmaBase<TNumeric>> SimpleMovingAverages;
public Dictionary<int, int> SimpleMovingAverageIndexes;
public int[] SimpleMovingAverageKeys;
private List<Action<TNumeric>> SampleActions;
public TNumeric[] Averages;
public int TotalSamples = 0;
public TNumeric LastSample;
public TNumeric[] History { get; private set; }
public int MaxSampleLength { get; private set; }
private int maxLen;
public MultiTimeFrameSma(int maximumMovingAverage) : this(Enumerable.Range(1, maximumMovingAverage))
{
}
public MultiTimeFrameSma(IEnumerable<int> movingAverageSizes)
{
SimpleMovingAverages = new Dictionary<int, SmaBase<TNumeric>>();
SimpleMovingAverageIndexes = new Dictionary<int, int>();
SimpleMovingAverageKeys = movingAverageSizes.ToArray();
MaxSampleLength = SimpleMovingAverageKeys.Max(x => x);
maxLen = MaxSampleLength - 1;
History = new TNumeric[MaxSampleLength];//new List<TNumeric>();
this.SampleActions = new List<Action<TNumeric>>();
var averages = new List<TNumeric>();
int i = 0;
foreach (var smaSize in movingAverageSizes.OrderBy(x => x))
{
var sma = new SmaBase<TNumeric>(smaSize);
SampleActions.Add((x) => { sma.AddSample(x); Averages[SimpleMovingAverageIndexes[sma.Count]] = sma.Average; });
SimpleMovingAverages.Add(smaSize, sma);
SimpleMovingAverageIndexes.Add(smaSize, i++);
averages.Add(sma.Average);
}
this.Averages = averages.ToArray();
}
public void AddSample(TNumeric value)
{
if (maxLen > 0)
{
Array.Copy(History, 1, History, 0, maxLen);
History[maxLen] = value;
}
else
{
History[0] = value;
}
LastSample = value;
SampleActions.ForEach(action => action(value));
TotalSamples++;
}
}
public class MultiTimeFrameCrossOver<TNumeric>
where TNumeric : IConvertible
{
public MultiTimeFrameSma<TNumeric> SimpleMovingAverages { get; }
public TNumeric[] History => SimpleMovingAverages.History;
public TNumeric[] Averages => SimpleMovingAverages.Averages;
public int TotalSamples => SimpleMovingAverages.TotalSamples;
public TNumeric LastSample => SimpleMovingAverages.LastSample;
private bool[][] matrix;
public MultiTimeFrameCrossOver(MultiTimeFrameSma<TNumeric> simpleMovingAverages)
{
this.SimpleMovingAverages = simpleMovingAverages;
int length = this.SimpleMovingAverages.Averages.Length;
this.matrix = SimpleMovingAverages.Averages.Select(avg => SimpleMovingAverages.Averages.Select(x => true).ToArray()).ToArray();
}
public void AddSample(TNumeric value)
{
SimpleMovingAverages.AddSample(value);
int max = SimpleMovingAverages.Averages.Length;
for (var maIndex = 0; maIndex < max; maIndex++)
{
IComparable<TNumeric> ma = (IComparable<TNumeric>)SimpleMovingAverages.Averages[maIndex];
var row = matrix[maIndex];
for (var otherIndex = 0; otherIndex < max; otherIndex++)
{
row[otherIndex] = ma.CompareTo(SimpleMovingAverages.Averages[otherIndex]) >= 0;
}
}
}
public bool[][] GetMatrix() => matrix;
}
decimal
has 96 bits of precision, which will perform a lot better thandouble
orfloat
for such a cumulative sum calculation. – Loge