如何在C#中更快地计算简单移动平均?

24

什么是计算简单移动平均的最快库/算法? 我编写了自己的代码,但在330,000项十进制数据集上运行太慢了。

  • 周期 / 时间(毫秒)
  • 20 / 300;
  • 60 / 1500;
  • 120 / 3500。

这是我的方法的代码:

public decimal MA_Simple(int period, int ii) {
    if (period != 0 && ii > period) {
        //stp.Start();
        decimal summ = 0;
        for (int i = ii; i > ii - period; i--) {
            summ = summ + Data.Close[i];
        }
        summ = summ / period;
        //stp.Stop();
        //if (ii == 1500) System.Windows.Forms.MessageBox.Show((stp.ElapsedTicks * 1000.0) / Stopwatch.Frequency + " ms");
        return summ;
    } else return -1;
}

Data.Close[] 是一个固定大小(1,000,000)的十进制数组。


3
你使用移动平均值的目的是什么?如果你正在对滑动窗口进行平均,那么你可以增量更新平均值,这样可以使速度更快。如果你正在计算随机窗口,你可以将数组预处理为累积和数组,以加速移动平均值的计算。优化取决于你的使用情况。 - nneonneo
对一个大数组进行累加会导致精度丢失,除非使用具有任意精度的数字库。 - Storstamp
decimal 具有 96 位精度,对于这种累积求和计算而言比 doublefloat 要表现出更好的性能。 - nneonneo
16个回答

1
// simple moving average
int moving_average(double *values, double *&averages, int size, int periods)
{
    double sum = 0;
    for (int i = 0; i < size; i ++)
        if (i < periods) {
            sum += values[i];
            averages[i] = (i == periods - 1) ? sum / (double)periods : 0;
        } else {
            sum = sum - values[i - periods] + values[i];
            averages[i] = sum / (double)periods;
        }
    return (size - periods + 1 > 0) ? size - periods + 1 : 0;
}

一个C函数,13行代码,简单移动平均。使用示例:

double *values = new double[10]; // the input
double *averages = new double[10]; // the output
values[0] = 55;
values[1] = 113;
values[2] = 92.6;
...
values[9] = 23;
moving_average(values, averages, 10, 5); // 5-day moving average

这看起来与 TA-Lib 所做的相似。似乎很优化。 - Jocelyn

1
/// <summary>
/// Fast low CPU usage moving average based on floating point math
/// Note: This algorithm algorithm compensates for floating point error by re-summing the buffer for every 1000 values
/// </summary>
public class FastMovingAverageDouble
{
    /// <summary>
    /// Adjust this as you see fit to suit the scenario
    /// </summary>
    const int MaximumWindowSize = 100;

    /// <summary>
    /// Adjust this as you see fit
    /// </summary>
    const int RecalculateEveryXValues = 1000;

    /// <summary>
    /// Initializes moving average for specified window size
    /// </summary>
    /// <param name="_WindowSize">Size of moving average window between 2 and MaximumWindowSize 
    /// Note: this value should not be too large and also bear in mind the possibility of overflow and floating point error as this class internally keeps a sum of the values within the window</param>
    public FastMovingAverageDouble(int _WindowSize)
    {
        if (_WindowSize < 2)
        {
            _WindowSize = 2;
        }
        else if (_WindowSize > MaximumWindowSize)
        {
            _WindowSize = MaximumWindowSize;
        }
        m_WindowSize = _WindowSize;
    }
    private object SyncRoot = new object();
    private Queue<double> Buffer = new Queue<double>();
    private int m_WindowSize;
    private double m_MovingAverage = 0d;
    private double MovingSum = 0d;
    private bool BufferFull;
    private int Counter = 0;

    /// <summary>
    /// Calculated moving average
    /// </summary>
    public double MovingAverage
    {
        get
        {
            lock (SyncRoot)
            {
                return m_MovingAverage;
            }
        }
    }

    /// <summary>
    /// Size of moving average window set by constructor during intialization
    /// </summary>
    public int WindowSize
    {
        get
        {
            return m_WindowSize;
        }
    }

    /// <summary>
    /// Add new value to sequence and recalculate moving average seee <see cref="MovingAverage"/>
    /// </summary>
    /// <param name="NewValue">New value to be added</param>
    public void AddValue(int NewValue)
    {
        lock (SyncRoot)
        {
            Buffer.Enqueue(NewValue);
            MovingSum += NewValue;
            if (!BufferFull)
            {
                int BufferSize = Buffer.Count;
                BufferFull = BufferSize == WindowSize;
                m_MovingAverage = MovingSum / BufferSize;
            }
            else
            {
                Counter += 1;
                if (Counter > RecalculateEveryXValues)
                {
                    MovingSum = 0;
                    foreach (double BufferValue in Buffer)
                    {
                        MovingSum += BufferValue;
                    }
                    Counter = 0;
                }
                MovingSum -= Buffer.Dequeue();
                m_MovingAverage = MovingSum / WindowSize;
            }
        }
    }
}

0

既然没有人展示我的方法,我要建议一下。 我认为在大多数情况下,Linq 可以快速执行而不需要创建缓冲区或增加代码复杂性。考虑一个金融 _originalDataserie OHLC 开盘价、最高价、最低价、收盘价,我想要 sma 关闭的价格,它是一个 Ilist<double>

  double[] smaSerie = new double[_originalDataSeries.Count];
      for (int i = 0; i < _originalDataSeries.Count;i++)
            {
                double sma = double.NaN;
                int period = 50;
              //  var rangeOfinterest = _originalDataSeries.CloseValues.AsParallel().Skip(i - period).Take(period).ToList();
                var rangeOfinterest = _originalDataSeries.CloseValues.Skip(i - period).Take(period).ToList();
                if (rangeOfinterest.Any())
                {
                    sma = rangeOfinterest.Average();                   
                }              
                 smaSerie[i] = sma;

            }

Sma计算了720个点:00:00:00.0075765

我无法确定注释中的并行版本是否表现更好,因为它需要将平均值作为并行实现,并用于_originalSerie并处理空范围,但如果您有百万点要显示一次,则可以通过这种方式进行改进。但在这种情况下,我会选择GPU计算,因为SMA适合进行GPU任务。


0
我的MovingAverage类实现如下:
  • 线程安全
  • 无锁
  • 限制为2的幂次方的窗口大小
以下是该类的代码:
using System;
using System.Linq;
using System.Threading;

public class MovingAverage
{
    private readonly int _mask;
    private readonly double[] _values;
    private int _nextIndex = -1;

    public MovingAverage(int windowSize)
    {
        _mask = windowSize - 1;
        if (windowSize == 0 || (windowSize & _mask) != 0)
        {
            throw new ArgumentException("Must be power of two", nameof(windowSize));
        }

        _values = Enumerable.Repeat(double.NaN, windowSize).ToArray();
    }

    public int WindowSize => _mask + 1;

    public bool Add(double newValue)
    {
        var index = Interlocked.Increment(ref _nextIndex) & _mask;
        _values[index] = newValue;
        return index == _mask;
    }

    public double ComputeAverage() => _values.TakeWhile(double.IsFinite)
        .DefaultIfEmpty(0)
        .Average();
}

这是NUnit测试。
using NUnit.Framework;

public class MovingAverageTest
{
    [Test]
    public void Should_compute_average()
    {
        var sut = new MovingAverage(4);

        Assert.That(sut.WindowSize, Is.EqualTo(4));

        Assert.That(sut.ComputeAverage(), Is.EqualTo(0));
        Assert.That(sut.Add(2), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(2));
        Assert.That(sut.Add(4), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(3));
        Assert.That(sut.Add(0), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(2));
        Assert.That(sut.Add(6), Is.True);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(3));
        Assert.That(sut.Add(6), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(4));
        Assert.That(sut.Add(0), Is.False);
        Assert.That(sut.Add(0), Is.False);
        Assert.That(sut.Add(0), Is.True);
        Assert.That(sut.Add(0), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(0));
        Assert.That(sut.Add(10), Is.False);
        Assert.That(sut.Add(10), Is.False);
        Assert.That(sut.Add(10), Is.True);
        Assert.That(sut.Add(10), Is.False);
        Assert.That(sut.ComputeAverage(), Is.EqualTo(10));
    }

    [Test]
    public void Should_check_windowsize_param()
    {
        Assert.That(() => new MovingAverage(3), Throws.ArgumentException);
    }
}

0
在实践中,我发现即使对于数百万个样本,这也是有效的方法。它计算了一个运行的移动平均值,并且比我尝试过的任何其他方法都要快。
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];
    }
}

我发现我经常需要访问历史记录。我通过跟踪平均值来实现这一点:

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;

}

现在在实践中,你的使用情况听起来像我的,需要跟踪多个时间框架:

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.

这可以按如下方式使用:

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")}");
    }
}

另一个要点是,您可以看到它被强类型化为十进制,这意味着对于其他数据类型需要进行完全重写。

为了处理这个问题,可以使类成为通用的,并使用接口来提供类型转换和所需的算术操作提供程序。

我有一个完整的工作示例,实际上是我用于处理数百万数据点的代码,以及关于Github here中交叉检测等的实现。与此问题和答案相关的代码:

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;

}

0

使用Dotnet Core 3和Linq进行测试:

int period = 20;
for(int k=0;data.Count()-period;k++){
   decimal summe = data.Skip(k).Take(period).Sum();
   summe /= (decimal)period;
}

它确实依赖于Linq及其内部优化,我没有计时。
使用Skip()和Take()作为移动平均的“范围之间”解决方案,然后将总和除以周期数量。
*for循环上限设置为避免不完整的求和操作。
参考(C#Microsoft):Skip(), Take(), Sum();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接