Files
quantconnect--lean/Indicators/AutoRegressiveIntegratedMovingAverage.cs
2026-07-13 13:02:50 +08:00

396 lines
17 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using MathNet.Numerics;
using MathNet.Numerics.LinearAlgebra.Double;
using MathNet.Numerics.LinearRegression;
namespace QuantConnect.Indicators
{
/// <summary>
/// An Autoregressive Intergrated Moving Average (ARIMA) is a time series model which can be used to describe a set of data.
/// In particular,with Xₜ representing the series, the model assumes the data are of form
/// (after differencing <see cref="_diffOrder" /> times):
/// <para>
/// Xₜ = c + εₜ + ΣᵢφᵢXₜ₋ᵢ + Σᵢθᵢεₜ₋ᵢ
/// </para>
/// where the first sum has an upper limit of <see cref="_arOrder" /> and the second <see cref="_maOrder" />.
/// </summary>
public class AutoRegressiveIntegratedMovingAverage : TimeSeriesIndicator
{
private List<double> _residuals;
private readonly bool _intercept;
private bool _loggedOnceInMovingAverageStep;
private bool _loggedOnceInAutoRegressiveStep;
private readonly RollingWindow<double> _rollingData;
/// <summary>
/// Differencing coefficient (d). Determines how many times the series should be differenced before fitting the
/// model.
/// </summary>
private readonly int _diffOrder;
/// <summary>
/// AR coefficient -- p
/// </summary>
private readonly int _arOrder;
/// <summary>
/// MA Coefficient -- q
/// </summary>
private readonly int _maOrder;
/// <summary>
/// Whether or not to handle potential exceptions, returning a zero value. I.e, the values
/// provided as input are not valid by the Normal Equations direct regression method
/// </summary>
public bool HandleExceptions { get; set; } = true;
/// <summary>
/// Fitted AR parameters (φ terms).
/// </summary>
public double[] ArParameters { get; private set; }
/// <summary>
/// Fitted MA parameters (θ terms).
/// </summary>
public double[] MaParameters { get; private set; }
/// <summary>
/// Fitted intercept (c term).
/// </summary>
public double Intercept { get; private set; }
/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => _rollingData.IsReady;
/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public override int WarmUpPeriod { get; }
/// <summary>
/// The variance of the residuals (Var(ε)) from the first step of <see cref="TwoStepFit" />.
/// </summary>
public double ArResidualError { get; private set; }
/// <summary>
/// The variance of the residuals (Var(ε)) from the second step of <see cref="TwoStepFit" />.
/// </summary>
public double MaResidualError { get; private set; }
/// <summary>
/// Fits an ARIMA(arOrder,diffOrder,maOrder) model of form (after differencing it <see cref="_diffOrder" /> times):
/// <para>
/// Xₜ = c + εₜ + ΣᵢφᵢXₜ₋ᵢ + Σᵢθᵢεₜ₋ᵢ
/// </para>
/// where the first sum has an upper limit of <see cref="_arOrder" /> and the second <see cref="_maOrder" />.
/// This particular constructor fits the model by means of <see cref="TwoStepFit" /> for a specified name.
/// </summary>
/// <param name="name">The name of the indicator</param>
/// <param name="arOrder">AR order (p) -- defines the number of past values to consider in the AR component of the model.</param>
/// <param name="diffOrder">Difference order (d) -- defines how many times to difference the model before fitting parameters.</param>
/// <param name="maOrder">MA order -- defines the number of past values to consider in the MA component of the model.</param>
/// <param name="period">Size of the rolling series to fit onto</param>
/// <param name="intercept">Whether or not to include the intercept term</param>
public AutoRegressiveIntegratedMovingAverage(
string name,
int arOrder,
int diffOrder,
int maOrder,
int period,
bool intercept = true
)
: base(name)
{
if (arOrder < 0 || maOrder < 0)
{
throw new ArgumentException("AR/MA orders cannot be negative.");
}
if (arOrder == 0)
{
throw new ArgumentException("arOrder (p) must be greater than zero for all " +
"currently available fitting methods.");
}
if (period < Math.Max(arOrder, maOrder))
{
throw new ArgumentException("Period must exceed both arOrder and maOrder");
}
_arOrder = arOrder;
_maOrder = maOrder;
_diffOrder = diffOrder;
WarmUpPeriod = period;
_rollingData = new RollingWindow<double>(period);
_intercept = intercept;
}
/// <summary>
/// Fits an ARIMA(arOrder,diffOrder,maOrder) model of form (after differencing it <see cref="_diffOrder" /> times):
/// <para>
/// Xₜ = c + εₜ + ΣᵢφᵢXₜ₋ᵢ + Σᵢθᵢεₜ₋ᵢ
/// </para>
/// where the first sum has an upper limit of <see cref="_arOrder" /> and the second <see cref="_maOrder" />.
/// This particular constructor fits the model by means of <see cref="TwoStepFit" /> using ordinary least squares.
/// </summary>
/// <param name="arOrder">AR order (p) -- defines the number of past values to consider in the AR component of the model.</param>
/// <param name="diffOrder">Difference order (d) -- defines how many times to difference the model before fitting parameters.</param>
/// <param name="maOrder">MA order (q) -- defines the number of past values to consider in the MA component of the model.</param>
/// <param name="period">Size of the rolling series to fit onto</param>
/// <param name="intercept">Whether to include an intercept term (c)</param>
public AutoRegressiveIntegratedMovingAverage(
int arOrder,
int diffOrder,
int maOrder,
int period,
bool intercept
)
: this($"ARIMA(({arOrder}, {diffOrder}, {maOrder}), {period}, {intercept})", arOrder, diffOrder, maOrder,
period, intercept)
{
}
/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
base.Reset();
_rollingData.Reset();
}
/// <summary>
/// Forecasts the series of the fitted model one point ahead.
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>A new value for this indicator</returns>
protected override decimal ComputeNextValue(IndicatorDataPoint input)
{
_rollingData.Add((double)input.Value);
if (_rollingData.IsReady)
{
var arrayData = _rollingData.ToArray();
double[] diffHeads = default;
arrayData = _diffOrder > 0 ? DifferenceSeries(_diffOrder, arrayData, out diffHeads) : arrayData;
_diffHeads = diffHeads;
TwoStepFit(arrayData);
double summants = 0;
if (_arOrder > 0)
{
for (var i = 0; i < _arOrder; i++) // AR Parameters
{
summants += ArParameters[i] * arrayData[i];
}
}
if (_maOrder > 0)
{
for (var i = 0; i < _maOrder; i++) // MA Parameters
{
summants += MaParameters[i] * _residuals[_maOrder + i + 1];
}
}
summants += Intercept; // By default equals 0
if (_diffOrder > 0)
{
var dataCast = arrayData.ToList();
dataCast.Insert(0, summants); // Prepends
summants = InverseDifferencedSeries(dataCast.ToArray(), _diffHeads).First(); // Returns disintegrated series
}
return (decimal)summants;
}
return 0m;
}
/// <summary>
/// Fits the model by means of implementing the following pseudo-code algorithm (in the form of "if{then}"):
/// <code>
/// if diffOrder > 0 {Difference data diffOrder times}
/// if arOrder > 0 {Fit the AR model Xₜ = ΣᵢφᵢXₜ; ε's are set to residuals from fitting this.}
/// if maOrder > 0 {Fit the MA parameters left over Xₜ = c + εₜ + ΣᵢφᵢXₜ₋ᵢ + Σᵢθᵢεₜ₋ᵢ}
/// Return: φ and θ estimates.
/// </code>
/// http://mbhauser.com/informal-notes/two-step-arma-estimation.pdf
/// </summary>
private void TwoStepFit(double[] series) // Protected for any future inheritors (e.g., SARIMA)
{
_residuals = new List<double>();
double errorAr = 0;
double errorMa = 0;
var lags = _arOrder > 0 ? LaggedSeries(_arOrder, series) : new[] {series};
AutoRegressiveStep(lags, series, errorAr);
if (_maOrder <= 0)
{
return;
}
MovingAverageStep(lags, series, errorMa);
}
/// <summary>
/// Fits the moving average component in the <see cref="TwoStepFit"/> method.
/// </summary>
/// <param name="lags">An array of lagged data (<see cref="TimeSeriesIndicator.LaggedSeries"/>).</param>
/// <param name="data">The input series, differenced <see cref="_diffOrder"/> times.</param>
/// <param name="errorMa">The summed residuals (by default 0) associated with the MA component.</param>
private void MovingAverageStep(double[][] lags, double[] data, double errorMa)
{
var appendedData = new List<double[]>();
var laggedErrors = LaggedSeries(_maOrder, _residuals.ToArray());
for (var i = 0; i < laggedErrors.Length; i++)
{
var doubles = lags[i].ToList();
doubles.AddRange(laggedErrors[i]);
appendedData.Add(doubles.ToArray());
}
double[] maFits = default;
if (HandleExceptions)
{
try
{
maFits = Fit.MultiDim(appendedData.ToArray(), data.Skip(_maOrder).ToArray(),
method: DirectRegressionMethod.NormalEquations, intercept: _intercept);
}
catch (Exception ex)
{
if (!_loggedOnceInMovingAverageStep)
{
Logging.Log.Error($"AutoRegressiveIntegratedMovingAverage.MovingAverageStep(): {ex.Message}");
_loggedOnceInMovingAverageStep = true;
}
// The method Fit.MultiDim takes the appendedData array of mxn(m rows, n columns), computes its
// transpose of size nxm, and then multiplies the tranpose with the original matrix, so the
// resultant matrix is of size nxn. Then a linear system Ax=b is solved where A is the
// aforementioned matrix and b is the data. Thus, the size of the response x is n
//
// It's worth saying that if intercept flag is set to true, the number of columns of the initial
// matrix (appendedData) is increased in one. For more information, please see the implementation
// of Fit.MultiDim() method (Ctrl + right click)
var size = appendedData.ToArray()[0].Length + (_intercept ? 1 : 0);
maFits = new double[size];
}
}
else
{
maFits = Fit.MultiDim(appendedData.ToArray(), data.Skip(_maOrder).ToArray(),
method: DirectRegressionMethod.NormalEquations, intercept: _intercept);
}
for (var i = _maOrder; i < data.Length; i++) // Calculate the error assoc. with model.
{
var paramVector = _intercept
? Vector.Build.Dense(maFits.Skip(1).ToArray())
: Vector.Build.Dense(maFits);
var residual = data[i] - Vector.Build.Dense(appendedData[i - _maOrder]).DotProduct(paramVector);
errorMa += Math.Pow(residual, 2);
}
switch (_intercept)
{
case true:
MaResidualError = errorMa / (data.Length - Math.Max(_arOrder, _maOrder) - 1);
MaParameters = maFits.Skip(1 + _arOrder).ToArray();
ArParameters = maFits.Skip(1).Take(_arOrder).ToArray();
Intercept = maFits[0];
break;
default:
MaResidualError = errorMa / (data.Length - Math.Max(_arOrder, _maOrder) - 1);
MaParameters = maFits.Skip(_arOrder).ToArray();
ArParameters = maFits.Take(_arOrder).ToArray();
break;
}
}
/// <summary>
/// Fits the autoregressive component in the <see cref="TwoStepFit"/> method.
/// </summary>
/// <param name="lags">An array of lagged data (<see cref="TimeSeriesIndicator.LaggedSeries"/>).</param>
/// <param name="data">The input series, differenced <see cref="_diffOrder"/> times.</param>
/// <param name="errorAr">The summed residuals (by default 0) associated with the AR component.</param>
private void AutoRegressiveStep(double[][] lags, double[] data, double errorAr)
{
double[] arFits;
if (HandleExceptions)
{
try
{
// The function (lags[time][lagged X]) |---> ΣᵢφᵢXₜ₋ᵢ
arFits = Fit.MultiDim(lags, data.Skip(_arOrder).ToArray(),
method: DirectRegressionMethod.NormalEquations);
}
catch (Exception ex)
{
if (!_loggedOnceInAutoRegressiveStep)
{
Logging.Log.Error($"AutoRegressiveIntegratedMovingAverage.AutoRegressiveStep(): {ex.Message}");
_loggedOnceInAutoRegressiveStep = true;
}
// The method Fit.MultiDim takes the lags array of mxn(m rows, n columns), computes its
// transpose of size nxm, and then multiplies the tranpose with the original matrix, so the
// resultant matrix is of size nxn. Then a linear system Ax=b is solved where A is the
// aforementioned matrix and b is the data. Thus, the size of the response x is n
//
// For more information, please see the implementation of Fit.MultiDim() method (Ctrl + right click)
var size = lags.ToArray()[0].Length;
arFits = new double[size];
}
}
else
{
// The function (lags[time][lagged X]) |---> ΣᵢφᵢXₜ₋ᵢ
arFits = Fit.MultiDim(lags, data.Skip(_arOrder).ToArray(),
method: DirectRegressionMethod.NormalEquations);
}
var fittedVec = Vector.Build.Dense(arFits);
for (var i = 0; i < data.Length; i++) // Calculate the error assoc. with model.
{
if (i < _arOrder)
{
_residuals.Add(0); // 0-padding
continue;
}
var residual = data[i] - Vector.Build.Dense(lags[i - _arOrder]).DotProduct(fittedVec);
errorAr += Math.Pow(residual, 2);
_residuals.Add(residual);
}
ArResidualError = errorAr / (data.Length - _arOrder - 1);
if (_maOrder == 0)
{
ArParameters = arFits; // Will not be thrown out
}
}
}
}