Skip to content

Align the AR and MA rows in AutoRegressiveIntegratedMovingAverage - #9714

Open
mkzung wants to merge 1 commit into
QuantConnect:masterfrom
mkzung:bug-9712-arima-lag-alignment
Open

Align the AR and MA rows in AutoRegressiveIntegratedMovingAverage#9714
mkzung wants to merge 1 commit into
QuantConnect:masterfrom
mkzung:bug-9712-arima-lag-alignment

Conversation

@mkzung

@mkzung mkzung commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Description

MovingAverageStep walked the lagged errors and indexed the AR lags with the same counter,
which runs off the end of lags whenever arOrder > maOrder. It now walks time and indexes
each array by its own offset.

Related Issue

Closes #9712

Motivation and Context

ARIMA(2, 0, 1) throws at any period. At period 50, 24 of the 80 order sets the constructor
accepts throw.

lags[i] is the row for time i + _arOrder and laggedErrors[j] the row for j + _maOrder,
so the counters agree only when the orders do.

The other direction does not throw, and that is the part worth looking hardest at. It pairs
an AR row from one bar with error terms from another, so the model gets fitted on rows that
never coexisted, and the fitted values move once they line up: ARIMA(1, 0, 2, 50) over a
deterministic series ends at 100.103983 before this change and 101.581063 after.

Two catch blocks added for #8039 also read their row width off row zero, so a fit that
failed for having no rows threw out of the handler meant to absorb it. Both widths follow
from the orders.

177 of 456 accepted order sets still throw at periods of 8 or less, so the issue is not fully
closed. period < Math.Max(arOrder, maOrder) is weaker than what the fit needs, by up to six
bars in the range I measured, and tightening it is a separate call.

Requires Documentation Change

No.

How Has This Been Tested?

AcceptsAnAutoRegressiveOrderAboveTheMovingAverageOrder covers (2, 0, 1), (3, 1, 1) and
(4, 0, 2). All three throw on master. A fourth case with maOrder > arOrder came out of the
draft because it passes there, so it would have been a test that never fails.

Every test in the file uses ARIMA(1, 0, 1, 50), the diagonal where the orders match and the
old bound is accidentally right, so the change is a no-op there. That configuration sums to
7090.1814358551 over 120 points of a deterministic series and ends at 103.9912907073, both
before and after.

My local NUnit host crashes in a Python.NET finalizer before any test runs, because
QuantConnect.pythonnet 2.0.65 wants Python 3.11 and this machine has 3.10, the same
limitation I noted on #9694. The figures above come from a console program linked against
QuantConnect.Indicators, which dumps the outcome of all 640 order-set and period
combinations on master and on this branch and diffs them: 116 newly work, 0 newly throw.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the code style of this project.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My branch follows the naming convention bug-<issue#>-<description> or feature-<issue#>-<description>

@mkzung

mkzung commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

One thing I should have put in the description: whether the new pairing fits a model, or only stops throwing.

Generated 400 points from x[t] = 0.6 x[t-1] - 0.3 x[t-2] + noise and fitted it with a window of 120, intercept off, reading ArParameters back:

truth                 ar1  0.6      ar2 -0.3
ARIMA(2, 0, 2, 120)   ar1  0.5882   ar2 -0.3114   residual error 0.01211
ARIMA(2, 0, 1, 120)   ar1  0.5703   ar2 -0.3145   residual error 0.01211
ARIMA(3, 0, 1, 120)   ar1  0.8144   ar2 -0.4298   residual error 0.01200

The first row is the diagonal this change does not touch. The second only runs with it, and recovers the same coefficients to two figures with the same residual error, so the rows are being paired in time rather than merely paired. The third is an over-specified AR(3) on AR(2) data, where the extra coefficient absorbs the difference.

Worth saying that this took two attempts. The first put the process on top of a level of 100 with no intercept, and every configuration returned coefficients summing to 1.0001: the fit was reproducing the level and the truth was invisible behind it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ARIMA throws IndexOutOfRangeException when arOrder exceeds maOrder

1 participant