diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 91d29c9eec4a..43fdb623e3a5 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -13,10 +13,22 @@ jobs: # Only run on push events (not on pull_request) for security reasons in order to be able to use secrets if: ${{ github.event_name == 'push' }} steps: + - name: Validate API secrets + id: api-secrets + run: | + if [ -z "${{ secrets.QC_JOB_USER_ID }}" ] || [ -z "${{ secrets.QC_API_ACCESS_TOKEN }}" ] || [ -z "${{ secrets.QC_JOB_ORGANIZATION_ID }}" ]; then + echo "has_secrets=false" >> "$GITHUB_OUTPUT" + echo "::warning::Skipping API Tests because QC_JOB_USER_ID, QC_API_ACCESS_TOKEN, or QC_JOB_ORGANIZATION_ID is not configured." + else + echo "has_secrets=true" >> "$GITHUB_OUTPUT" + fi + - name: Checkout + if: steps.api-secrets.outputs.has_secrets == 'true' uses: actions/checkout@v2 - name: Liberate disk space + if: steps.api-secrets.outputs.has_secrets == 'true' uses: jlumbroso/free-disk-space@main with: tool-cache: true @@ -25,11 +37,13 @@ jobs: swap-storage: false - name: Define docker helper + if: steps.api-secrets.outputs.has_secrets == 'true' run: | echo 'runInContainer() { docker exec test-container "$@"; }' > $HOME/ci_functions.sh echo "BASH_ENV=$HOME/ci_functions.sh" >> $GITHUB_ENV - name: Start container + if: steps.api-secrets.outputs.has_secrets == 'true' run: | docker run -d \ --workdir /__w/Lean/Lean \ @@ -43,6 +57,7 @@ jobs: tail -f /dev/null - name: Run API Tests + if: steps.api-secrets.outputs.has_secrets == 'true' run: | # Build runInContainer dotnet build /p:Configuration=Release /v:quiet /p:WarningLevel=1 QuantConnect.Lean.sln diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs index e51f0c0961b0..ac560ba05df2 100644 --- a/Algorithm/QCAlgorithm.Indicators.cs +++ b/Algorithm/QCAlgorithm.Indicators.cs @@ -3362,7 +3362,8 @@ public void WarmUpIndicator(IEnumerable symbols, IndicatorBase(IEnumerable symbols, IndicatorBase ind { if (AssertIndicatorHasWarmupPeriod(indicator)) { - IndicatorHistory(indicator, symbols, 0, resolution, selector); + var end = GetIndicatorWarmUpHistoryEndTime(); + IndicatorHistory(indicator, symbols, end, end, resolution, selector); } } @@ -3547,6 +3549,19 @@ private bool AssertIndicatorHasWarmupPeriod(IIndicator indicator) return true; } + private DateTime GetIndicatorWarmUpHistoryEndTime() + { + // During Initialize, if algorithm warm-up is configured, requesting indicator warm-up history + // up to Time causes an overlap with the replayed warm-up stream at the warm-up boundary. + // We end at the warm-up start frontier instead to avoid duplicate boundary updates. + if (!_locked && TryGetWarmupHistoryStartTime(out var warmupStart)) + { + return warmupStart; + } + + return Time; + } + private void WarmUpIndicatorImpl(IEnumerable symbols, TimeSpan period, Action handler, IEnumerable history, bool identityConsolidator) where T : class, IBaseData { diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs index dd0764671c4d..58c6a20d9687 100644 --- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs +++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Moq; using NUnit.Framework; using Python.Runtime; @@ -642,6 +643,57 @@ public void IndicatorHistoryShouldIncludeValidIndicatorsAndExplicitlyIncludedPro } } + [Test] + public void WarmUpIndicatorDuringInitializeDoesNotCreateBoundaryDuplicateWithAlgorithmWarmup() + { + _algorithm.SetWarmUp(30); + + const int windowSize = 6; + var warmedIndicator = new MidPrice(2); + warmedIndicator.Window.Size = windowSize; + var referenceIndicator = new MidPrice(2); + referenceIndicator.Window.Size = windowSize; + + _algorithm.RegisterIndicator(_equity, warmedIndicator, Resolution.Minute); + _algorithm.RegisterIndicator(_equity, referenceIndicator, Resolution.Minute); + + _algorithm.WarmUpIndicator(_equity, warmedIndicator, Resolution.Minute); + + var warmupStart = GetWarmupStartTime(_algorithm); + var history = _algorithm.History(new[] { _equity }, warmupStart, _algorithm.Time, Resolution.Minute); + + var warmedConsolidator = warmedIndicator.Consolidators.Single(); + var referenceConsolidator = referenceIndicator.Consolidators.Single(); + foreach (var slice in history) + { + if (slice.Bars.TryGetValue(_equity, out var bar)) + { + warmedConsolidator.Update(bar); + referenceConsolidator.Update(bar); + } + } + + Assert.AreEqual(windowSize, warmedIndicator.Window.Count); + Assert.AreEqual(windowSize, referenceIndicator.Window.Count); + for (var i = 0; i < windowSize; i++) + { + Assert.AreEqual(referenceIndicator.Window[i].EndTime, warmedIndicator.Window[i].EndTime); + Assert.AreEqual(referenceIndicator.Window[i].Value, warmedIndicator.Window[i].Value); + } + } + + private static DateTime GetWarmupStartTime(QCAlgorithm algorithm) + { + var method = typeof(QCAlgorithm).GetMethod("TryGetWarmupHistoryStartTime", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(method, "Could not find TryGetWarmupHistoryStartTime private method"); + + var args = new object[] { default(DateTime) }; + var hasWarmup = (bool)method.Invoke(algorithm, args); + Assert.IsTrue(hasWarmup, "Expected warm-up to be configured"); + + return (DateTime)args[0]; + } + private enum TestIndicatorType { TypeA,