diff --git a/Algorithm.CSharp/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.cs b/Algorithm.CSharp/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.cs
new file mode 100644
index 000000000000..79c53a1356df
--- /dev/null
+++ b/Algorithm.CSharp/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.cs
@@ -0,0 +1,147 @@
+/*
+ * 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 QuantConnect.Data;
+using QuantConnect.Data.Consolidators;
+using QuantConnect.Indicators;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Asserts that indicators registered with a consolidator are automatically warmed up when
+ /// 'Settings.AutomaticIndicatorWarmUp' is enabled, without requiring a manual history replay
+ ///
+ public class AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private Symbol _spy;
+ private AverageTrueRange _atr;
+ private RelativeStrengthIndex _rsi;
+
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 08);
+ SetEndDate(2013, 10, 09);
+
+ Settings.AutomaticIndicatorWarmUp = true;
+ _spy = AddEquity("SPY", Resolution.Minute).Symbol;
+
+ // Test case 1: bar indicator registered with a consolidator, previously required a manual history replay
+ _atr = new AverageTrueRange(10);
+ RegisterIndicator(_spy, _atr, new TradeBarConsolidator(TimeSpan.FromMinutes(30)));
+ AssertIsReady(_atr, expected: true);
+
+ // Test case 2: data point indicator registered with a consolidator and a selector
+ _rsi = new RelativeStrengthIndex(14);
+ RegisterIndicator(_spy, _rsi, new TradeBarConsolidator(TimeSpan.FromMinutes(30)), Field.Close);
+ AssertIsReady(_rsi, expected: true);
+
+ // Test case 3: non time based consolidators cannot be automatically warmed up, the indicator is
+ // registered but left cold
+ var renkoRsi = new RelativeStrengthIndex(14);
+ RegisterIndicator(_spy, renkoRsi, new RenkoConsolidator(1m), Field.Close);
+ AssertIsReady(renkoRsi, expected: false);
+
+ // Test case 4: with the setting disabled nothing is warmed up
+ Settings.AutomaticIndicatorWarmUp = false;
+ var notWarmed = new AverageTrueRange(10);
+ RegisterIndicator(_spy, notWarmed, new TradeBarConsolidator(TimeSpan.FromMinutes(30)));
+ AssertIsReady(notWarmed, expected: false);
+ Settings.AutomaticIndicatorWarmUp = true;
+ }
+
+ private static void AssertIsReady(IIndicator indicator, bool expected)
+ {
+ if (indicator.IsReady != expected)
+ {
+ throw new RegressionTestException($"Expected {indicator.Name} IsReady to be {expected} but was {indicator.IsReady}");
+ }
+ }
+
+ ///
+ /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
+ ///
+ /// Slice object keyed by symbol containing the stock data
+ public override void OnData(Slice slice)
+ {
+ if (!Portfolio.Invested)
+ {
+ SetHoldings(_spy, 1);
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally { get; } = true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 1582;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 1500;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "1"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "98848.47"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "0"},
+ {"Tracking Error", "0"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$3.44"},
+ {"Estimated Strategy Capacity", "$31000000.00"},
+ {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
+ {"Portfolio Turnover", "50.43%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "00636a25aed88acd2171c6221c747716"}
+ };
+ }
+}
diff --git a/Algorithm.CSharp/AutomaticIndicatorWarmupDataTypeRegressionAlgorithm.cs b/Algorithm.CSharp/AutomaticIndicatorWarmupDataTypeRegressionAlgorithm.cs
index f6633f164a02..461cef703226 100644
--- a/Algorithm.CSharp/AutomaticIndicatorWarmupDataTypeRegressionAlgorithm.cs
+++ b/Algorithm.CSharp/AutomaticIndicatorWarmupDataTypeRegressionAlgorithm.cs
@@ -65,13 +65,12 @@ public override void Initialize()
// force spy for use Raw data mode so that it matches the used when unsubscribed which uses the universe settings
SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(spy).SetDataNormalizationMode(DataNormalizationMode.Raw);
- // Test case: custom IndicatorBase indicator using Future subscribed symbol
+ // Test case: custom IndicatorBase indicator using Future subscribed symbol.
+ // Since 'Settings.AutomaticIndicatorWarmUp' is enabled, registering with a consolidator warms the indicator up
var indicator = new CustomIndicator();
var consolidator = CreateConsolidator(TimeSpan.FromMinutes(2), typeof(QuoteBar));
RegisterIndicator(_symbol, indicator, consolidator);
- AssertIndicatorState(indicator, isReady: false);
- WarmUpIndicator(_symbol, indicator);
AssertIndicatorState(indicator, isReady: true);
// Test case: SimpleMovingAverage using Future Subscribed symbol (should use TradeBar)
@@ -151,7 +150,7 @@ protected override decimal ComputeNextValue(QuoteBar input)
///
/// Data Points count of the algorithm history
///
- public int AlgorithmHistoryDataPoints => 85;
+ public int AlgorithmHistoryDataPoints => 87;
///
/// Final status of the algorithm
diff --git a/Algorithm.CSharp/AutomaticIndicatorWarmupRegressionAlgorithm.cs b/Algorithm.CSharp/AutomaticIndicatorWarmupRegressionAlgorithm.cs
index ab849e12c7ce..e2fd2b64d680 100644
--- a/Algorithm.CSharp/AutomaticIndicatorWarmupRegressionAlgorithm.cs
+++ b/Algorithm.CSharp/AutomaticIndicatorWarmupRegressionAlgorithm.cs
@@ -44,19 +44,30 @@ public override void Initialize()
throw new RegressionTestException("Expected SMA to be warmed up");
}
- // Test case 2
+ // Test case 2: 'RegisterIndicator' respects the automatic indicator warm-up setting
var indicator = new CustomIndicator(10);
RegisterIndicator(_spy, indicator, Resolution.Minute, (Func) null);
- if (indicator.IsReady)
+ if (!indicator.IsReady)
+ {
+ throw new RegressionTestException("Expected CustomIndicator to be automatically warmed up when registered");
+ }
+
+ // Test case 3: with the setting disabled 'RegisterIndicator' does not warm up, 'WarmUpIndicator' does
+ Settings.AutomaticIndicatorWarmUp = false;
+ var notWarmedIndicator = new CustomIndicator(10);
+ RegisterIndicator(_spy, notWarmedIndicator, Resolution.Minute, (Func) null);
+
+ if (notWarmedIndicator.IsReady)
{
throw new RegressionTestException("Expected CustomIndicator Not to be warmed up");
}
- WarmUpIndicator(_spy, indicator);
- if (!indicator.IsReady)
+ WarmUpIndicator(_spy, notWarmedIndicator);
+ if (!notWarmedIndicator.IsReady)
{
throw new RegressionTestException("Expected CustomIndicator to be warmed up");
}
+ Settings.AutomaticIndicatorWarmUp = true;
}
///
@@ -70,7 +81,7 @@ public override void OnData(Slice slice)
var subscription = SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(_spy).First(config => config.TickType == TickType.Trade);
// we expect 1 consolidator per indicator
- if (subscription.Consolidators.Count != 2)
+ if (subscription.Consolidators.Count != 3)
{
throw new RegressionTestException($"Unexpected consolidator count for subscription: {subscription.Consolidators.Count}");
}
@@ -113,7 +124,7 @@ protected override decimal ComputeNextValue(IReadOnlyWindow
///
/// Data Points count of the algorithm history
///
- public int AlgorithmHistoryDataPoints => 40;
+ public int AlgorithmHistoryDataPoints => 60;
///
/// Final status of the algorithm
diff --git a/Algorithm.Python/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.py b/Algorithm.Python/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.py
new file mode 100644
index 000000000000..9d0340789134
--- /dev/null
+++ b/Algorithm.Python/AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm.py
@@ -0,0 +1,57 @@
+# 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.
+
+from AlgorithmImports import *
+
+###
+### Asserts that indicators registered with a consolidator are automatically warmed up when
+### 'settings.automatic_indicator_warm_up' is enabled, without requiring a manual history replay
+###
+class AutomaticIndicatorWarmupConsolidatorRegressionAlgorithm(QCAlgorithm):
+ def initialize(self):
+ self.set_start_date(2013, 10, 8)
+ self.set_end_date(2013, 10, 9)
+
+ self.settings.automatic_indicator_warm_up = True
+ self._spy = self.add_equity("SPY", Resolution.MINUTE).symbol
+
+ # Test case 1: bar indicator registered with a consolidator, previously required a manual history replay
+ self._atr = AverageTrueRange(10)
+ self.register_indicator(self._spy, self._atr, TradeBarConsolidator(timedelta(minutes=30)))
+ self.assert_is_ready(self._atr, True)
+
+ # Test case 2: data point indicator registered with a consolidator and a selector
+ self._rsi = RelativeStrengthIndex(14)
+ self.register_indicator(self._spy, self._rsi, TradeBarConsolidator(timedelta(minutes=30)), lambda bar: bar.close)
+ self.assert_is_ready(self._rsi, True)
+
+ # Test case 3: non time based consolidators cannot be automatically warmed up, the indicator is
+ # registered but left cold
+ renko_rsi = RelativeStrengthIndex(14)
+ self.register_indicator(self._spy, renko_rsi, RenkoConsolidator(1), lambda bar: bar.close)
+ self.assert_is_ready(renko_rsi, False)
+
+ # Test case 4: with the setting disabled nothing is warmed up
+ self.settings.automatic_indicator_warm_up = False
+ not_warmed = AverageTrueRange(10)
+ self.register_indicator(self._spy, not_warmed, TradeBarConsolidator(timedelta(minutes=30)))
+ self.assert_is_ready(not_warmed, False)
+ self.settings.automatic_indicator_warm_up = True
+
+ def assert_is_ready(self, indicator, expected):
+ if indicator.is_ready != expected:
+ raise Exception(f"Expected {indicator.name} is_ready to be {expected} but was {indicator.is_ready}")
+
+ def on_data(self, data):
+ if not self.portfolio.invested:
+ self.set_holdings(self._spy, 1)
diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs
index e86ffd96b864..dee2866022d0 100644
--- a/Algorithm/QCAlgorithm.Indicators.cs
+++ b/Algorithm/QCAlgorithm.Indicators.cs
@@ -56,6 +56,11 @@ public partial class QCAlgorithm
"Period"
};
+ ///
+ /// True once the warning about registered indicators that cannot be automatically warmed up has been sent
+ ///
+ private bool _registeredIndicatorNoWarmUpWarningSent;
+
///
/// Gets whether or not WarmUpIndicator is allowed to warm up indicators
///
@@ -1178,7 +1183,7 @@ public Identity Identity(Symbol symbol, TimeSpan resolution, Func i
[DocumentationAttribute(ConsolidatingData)]
[DocumentationAttribute(Indicators)]
public void RegisterIndicator(Symbol symbol, IndicatorBase indicator, IDataConsolidator consolidator, Func selector = null)
+ {
+ RegisterIndicatorCore(symbol, indicator, consolidator, selector);
+
+ if (TryGetRegisteredIndicatorWarmUpPeriod(indicator, consolidator, out var barSpan))
+ {
+ if (barSpan.HasValue)
+ {
+ // wrap the decimal selector the same way the live consolidator handler does
+ Func warmUpSelector = null;
+ if (selector != null)
+ {
+ warmUpSelector = x => new IndicatorDataPoint(x.Symbol, x.EndTime, selector(x));
+ }
+ WarmUpConsolidatorRegisteredIndicator(symbol, indicator, consolidator, barSpan.Value, warmUpSelector);
+ }
+ else
+ {
+ // identity consolidator: warm up directly at the subscription resolution
+ WarmUpIndicator(symbol, indicator, (Resolution?)null, selector);
+ }
+ }
+ }
+
+ ///
+ /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates
+ /// from the consolidator, without triggering the automatic indicator warm-up. Used by the indicator creation
+ /// helpers, which handle warm-up themselves
+ ///
+ private void RegisterIndicatorCore(Symbol symbol, IndicatorBase indicator, IDataConsolidator consolidator, Func selector = null)
{
// default our selector to the Value property on BaseData
selector = selector ?? (x => x.Value);
@@ -3315,6 +3349,31 @@ public void RegisterIndicator(Symbol symbol, IndicatorBase indicator, Time
[DocumentationAttribute(Indicators)]
public void RegisterIndicator(Symbol symbol, IndicatorBase indicator, IDataConsolidator consolidator, Func selector = null)
where T : IBaseData
+ {
+ RegisterIndicatorCore(symbol, indicator, consolidator, selector);
+
+ if (TryGetRegisteredIndicatorWarmUpPeriod(indicator, consolidator, out var barSpan))
+ {
+ if (barSpan.HasValue)
+ {
+ WarmUpConsolidatorRegisteredIndicator(symbol, indicator, consolidator, barSpan.Value, selector);
+ }
+ else
+ {
+ // identity consolidator: warm up directly at the subscription resolution.
+ // Same as 'WarmUpIndicator(symbol, indicator, resolution)' but without its 'class' generic constraint
+ IndicatorHistory(indicator, new[] { symbol }, 0, (Resolution?)null, selector);
+ }
+ }
+ }
+
+ ///
+ /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates
+ /// from the consolidator, without triggering the automatic indicator warm-up. Used by the indicator creation
+ /// helpers, which handle warm-up themselves
+ ///
+ private void RegisterIndicatorCore(Symbol symbol, IndicatorBase indicator, IDataConsolidator consolidator, Func selector = null)
+ where T : IBaseData
{
// assign default using cast
var selectorToUse = selector ?? (x => (T)x);
@@ -3347,6 +3406,83 @@ public void RegisterIndicator(Symbol symbol, IndicatorBase indicator, IDat
};
}
+ ///
+ /// Determines whether an indicator explicitly registered with a consolidator should be automatically warmed up,
+ /// see , and gets the consolidator's bar span to warm up with.
+ /// A null with a true return value means the consolidator is an identity pass-through,
+ /// so the subscription resolution should be used
+ ///
+ private bool TryGetRegisteredIndicatorWarmUpPeriod(IndicatorBase indicator, IDataConsolidator consolidator, out TimeSpan? barSpan)
+ {
+ barSpan = null;
+ if (!Settings.AutomaticIndicatorWarmUp
+ || indicator is not IIndicatorWarmUpPeriodProvider warmUpPeriodProvider
+ || warmUpPeriodProvider.WarmUpPeriod <= 0)
+ {
+ // indicators which don't define a warm up period are silently skipped, unlike 'WarmUpIndicator',
+ // since the user did not explicitly request this specific indicator to be warmed up
+ return false;
+ }
+
+ var period = (consolidator as ConsolidatorBase)?.Period;
+ if (!period.HasValue)
+ {
+ // non time based consolidators, Renko for instance, don't have a period we can warm up from
+ if (!_registeredIndicatorNoWarmUpWarningSent)
+ {
+ _registeredIndicatorNoWarmUpWarningSent = true;
+ Debug($"Warning: automatic indicator warm-up is not supported for consolidators of type" +
+ $" '{consolidator.GetType().Name}' because their consolidation period is unknown." +
+ $" '{indicator.Name}' was registered but not warmed up, please warm it up manually if needed.");
+ }
+ return false;
+ }
+
+ if (period.Value != TimeSpan.Zero)
+ {
+ barSpan = period;
+ }
+ return true;
+ }
+
+ ///
+ /// Warms up an indicator that was registered with a consolidator by replaying historical data through
+ /// a temporary consolidator mirroring the registered one, so the indicator is fed the same bar type and
+ /// span it will receive live. This is what enables 'Settings.AutomaticIndicatorWarmUp' to cover
+ /// consolidator-registered indicators, which otherwise force a manual history replay
+ ///
+ private void WarmUpConsolidatorRegisteredIndicator(Symbol symbol, IndicatorBase indicator, IDataConsolidator consolidator, TimeSpan barSpan,
+ Func selector)
+ where T : IBaseData
+ {
+ var history = GetIndicatorWarmUpHistory(new[] { symbol }, indicator, barSpan, out _);
+ if (history == Enumerable.Empty())
+ {
+ return;
+ }
+
+ // assign default selector
+ selector ??= GetDefaultSelector();
+
+ // mirror the registered consolidator's input type instead of using it directly:
+ // pumping history through the live instance would replay stale bars into other attached handlers
+ var tickType = LeanData.GetCommonTickTypeForCommonDataTypes(consolidator.InputType, symbol.SecurityType);
+ var warmUpConsolidator = CreateConsolidator(barSpan, consolidator.InputType, tickType);
+ warmUpConsolidator.DataConsolidated += (_, consolidated) => indicator.Update(selector(consolidated));
+
+ foreach (var slice in history)
+ {
+ if (slice.TryGet(warmUpConsolidator.InputType, symbol, out var data))
+ {
+ warmUpConsolidator.Update(data);
+ }
+ }
+
+ // scan to flush the last consolidated bar. The security exists at this point, registering created it if missing
+ warmUpConsolidator.Scan(Securities[symbol].LocalTime);
+ warmUpConsolidator.Dispose();
+ }
+
///
/// Will unregister an indicator and it's associated consolidator instance so they stop receiving data updates
///
@@ -4381,7 +4517,7 @@ private void InitializeIndicator(IndicatorBase indicator, Re
var dataType = GetDataTypeFromSelector(selector);
foreach (var symbol in symbols)
{
- RegisterIndicator(symbol, indicator, ResolveConsolidator(symbol, resolution, dataType), selector);
+ RegisterIndicatorCore(symbol, indicator, ResolveConsolidator(symbol, resolution, dataType), selector);
}
if (Settings.AutomaticIndicatorWarmUp)
@@ -4396,7 +4532,7 @@ private void InitializeIndicator(IndicatorBase indicator, Resolution? reso
{
foreach (var symbol in symbols)
{
- RegisterIndicator(symbol, indicator, resolution, selector);
+ RegisterIndicatorCore(symbol, indicator, ResolveConsolidator(symbol, resolution, typeof(T)), selector);
}
if (Settings.AutomaticIndicatorWarmUp)
@@ -4407,12 +4543,12 @@ private void InitializeIndicator(IndicatorBase indicator, Resolution? reso
private void InitializeOptionIndicator(IndicatorBase indicator, Resolution? resolution, Symbol symbol, Symbol mirrorOption)
{
- RegisterIndicator(symbol, indicator, ResolveConsolidator(symbol, resolution, typeof(QuoteBar)));
- RegisterIndicator(symbol.Underlying, indicator, ResolveConsolidator(symbol.Underlying, resolution));
+ RegisterIndicatorCore(symbol, indicator, ResolveConsolidator(symbol, resolution, typeof(QuoteBar)));
+ RegisterIndicatorCore(symbol.Underlying, indicator, ResolveConsolidator(symbol.Underlying, resolution));
var symbols = new List { symbol, symbol.Underlying };
if (mirrorOption != null)
{
- RegisterIndicator(mirrorOption, indicator, ResolveConsolidator(mirrorOption, resolution, typeof(QuoteBar)));
+ RegisterIndicatorCore(mirrorOption, indicator, ResolveConsolidator(mirrorOption, resolution, typeof(QuoteBar)));
symbols.Add(mirrorOption);
}
diff --git a/Common/AlgorithmSettings.cs b/Common/AlgorithmSettings.cs
index 2cf382305795..6146e8f63297 100644
--- a/Common/AlgorithmSettings.cs
+++ b/Common/AlgorithmSettings.cs
@@ -36,7 +36,8 @@ public class AlgorithmSettings : IAlgorithmSettings
private static bool _defaultIgnoreUnknownAssetHoldings = Config.GetBool("ignore-unknown-asset-holdings", true);
///
- /// Gets whether or not WarmUpIndicator is allowed to warm up indicators
+ /// Gets whether or not indicators are automatically warmed up with historical data when created
+ /// through the indicator helper methods or registered through 'RegisterIndicator'
///
public bool AutomaticIndicatorWarmUp { get; set; }
diff --git a/Common/Data/Consolidators/ConsolidatorBase.cs b/Common/Data/Consolidators/ConsolidatorBase.cs
index 0b853310efcc..1efd29c02d28 100644
--- a/Common/Data/Consolidators/ConsolidatorBase.cs
+++ b/Common/Data/Consolidators/ConsolidatorBase.cs
@@ -47,6 +47,12 @@ protected set
///
public abstract IBaseData WorkingData { get; }
+ ///
+ /// Gets the period of time each consolidated bar spans, if this consolidator is time based, null otherwise.
+ /// A zero period means the consolidator emits each input as is, see
+ ///
+ public virtual TimeSpan? Period => null;
+
///
/// Gets the type consumed by this consolidator
///
diff --git a/Common/Data/Consolidators/IdentityDataConsolidator.cs b/Common/Data/Consolidators/IdentityDataConsolidator.cs
index a16cc9bc269b..2b06a3642801 100644
--- a/Common/Data/Consolidators/IdentityDataConsolidator.cs
+++ b/Common/Data/Consolidators/IdentityDataConsolidator.cs
@@ -51,6 +51,12 @@ public override Type OutputType
get { return typeof(T); }
}
+ ///
+ /// Gets the period of time each consolidated bar spans. This consolidator emits each input as is,
+ /// which is expressed as a zero period
+ ///
+ public override TimeSpan? Period => TimeSpan.Zero;
+
///
/// Updates this consolidator with the specified data
///
diff --git a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
index 8cdda4175d2b..429d69ce9b8f 100644
--- a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
+++ b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
@@ -28,12 +28,13 @@ public class MarketHourAwareConsolidator : ConsolidatorBase
{
private readonly bool _dailyStrictEndTimeEnabled;
private readonly bool _extendedMarketHours;
+ private readonly TimeSpan _period;
private bool _useStrictEndTime;
///
/// The consolidation period requested
///
- protected TimeSpan Period { get; }
+ public override TimeSpan? Period => _period;
///
/// The consolidator instance
@@ -75,7 +76,7 @@ public class MarketHourAwareConsolidator : ConsolidatorBase
public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, Resolution resolution, Type dataType, TickType tickType, bool extendedMarketHours)
{
_dailyStrictEndTimeEnabled = dailyStrictEndTimeEnabled;
- Period = resolution.ToTimeSpan();
+ _period = resolution.ToTimeSpan();
_extendedMarketHours = extendedMarketHours;
Consolidator = CreateConsolidator(resolution, dataType, tickType);
@@ -94,7 +95,7 @@ public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, Resolution re
public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, TimeSpan period, Type dataType, TickType tickType, bool extendedMarketHours)
{
_dailyStrictEndTimeEnabled = dailyStrictEndTimeEnabled;
- Period = period;
+ _period = period;
_extendedMarketHours = extendedMarketHours;
// when the period exactly matches a standard resolution, reuse the resolution based consolidation so its
@@ -123,23 +124,23 @@ protected virtual IDataConsolidator CreateConsolidator(Resolution resolution, Ty
{
return resolution == Resolution.Daily
? new TickConsolidator(DailyStrictEndTime)
- : new TickConsolidator(Period);
+ : new TickConsolidator(_period);
}
return resolution == Resolution.Daily
? new TickQuoteBarConsolidator(DailyStrictEndTime)
- : new TickQuoteBarConsolidator(Period);
+ : new TickQuoteBarConsolidator(_period);
}
if (dataType == typeof(TradeBar))
{
return resolution == Resolution.Daily
? new TradeBarConsolidator(DailyStrictEndTime)
- : new TradeBarConsolidator(Period);
+ : new TradeBarConsolidator(_period);
}
if (dataType == typeof(QuoteBar))
{
return resolution == Resolution.Daily
? new QuoteBarConsolidator(DailyStrictEndTime)
- : new QuoteBarConsolidator(Period);
+ : new QuoteBarConsolidator(_period);
}
throw new ArgumentNullException(nameof(dataType), $"{dataType.Name} not supported");
}
@@ -179,7 +180,7 @@ public override void Update(IBaseData data)
// the data resolution is hour and the exchange opens at any point in time over the data.Time to data.EndTime interval
if (_extendedMarketHours ||
ExchangeHours.IsOpen(data.Time, false) ||
- (Period == Time.OneDay && (data.EndTime - data.Time >= Time.OneHour) && ExchangeHours.IsOpen(data.Time, data.EndTime, false)))
+ (_period == Time.OneDay && (data.EndTime - data.Time >= Time.OneHour) && ExchangeHours.IsOpen(data.Time, data.EndTime, false)))
{
Consolidator.Update(data);
}
@@ -238,9 +239,9 @@ protected void Initialize(IBaseData data)
protected virtual CalendarInfo DailyStrictEndTime(DateTime dateTime)
{
// strict end times describe a single daily bar, so periods larger than a day fall back to standard period consolidation
- if (!_useStrictEndTime || Period > Time.OneDay)
+ if (!_useStrictEndTime || _period > Time.OneDay)
{
- return new(Period > Time.OneDay ? dateTime : dateTime.RoundDown(Period), Period);
+ return new(_period > Time.OneDay ? dateTime : dateTime.RoundDown(_period), _period);
}
return LeanData.GetDailyCalendar(dateTime, ExchangeHours, _extendedMarketHours);
}
@@ -253,9 +254,9 @@ protected virtual CalendarInfo IntradayCalendar(DateTime dateTime)
{
if (ExchangeHours == null || ExchangeHours.IsMarketAlwaysOpen)
{
- return new(dateTime.RoundDown(Period), Period);
+ return new(dateTime.RoundDown(_period), _period);
}
- return LeanData.GetIntradayCalendar(dateTime, Period, ExchangeHours, _extendedMarketHours);
+ return LeanData.GetIntradayCalendar(dateTime, _period, ExchangeHours, _extendedMarketHours);
}
///
@@ -263,7 +264,7 @@ protected virtual CalendarInfo IntradayCalendar(DateTime dateTime)
///
protected virtual bool UseStrictEndTime(Symbol symbol)
{
- return LeanData.UseStrictEndTime(_dailyStrictEndTimeEnabled, symbol, Period, ExchangeHours);
+ return LeanData.UseStrictEndTime(_dailyStrictEndTimeEnabled, symbol, _period, ExchangeHours);
}
///
diff --git a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
index 1f6f69e29c03..d1a57a70dcaa 100644
--- a/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
+++ b/Common/Data/Consolidators/PeriodCountConsolidatorBase.cs
@@ -289,9 +289,9 @@ public override void Reset()
protected bool IsTimeBased => !_maxCount.HasValue;
///
- /// Gets the time period for this consolidator
+ /// Gets the time period for this consolidator, null when in pure count mode
///
- protected TimeSpan? Period => _period;
+ public override TimeSpan? Period => _period;
///
/// Determines whether or not the specified data should be processed
diff --git a/Common/Interfaces/IAlgorithmSettings.cs b/Common/Interfaces/IAlgorithmSettings.cs
index 14667144c819..43e6bb4b7fc8 100644
--- a/Common/Interfaces/IAlgorithmSettings.cs
+++ b/Common/Interfaces/IAlgorithmSettings.cs
@@ -24,7 +24,8 @@ namespace QuantConnect.Interfaces
public interface IAlgorithmSettings
{
///
- /// Gets whether or not WarmUpIndicator is allowed to warm up indicators
+ /// Gets whether or not indicators are automatically warmed up with historical data when created
+ /// through the indicator helper methods or registered through 'RegisterIndicator'
///
bool AutomaticIndicatorWarmUp { get; set; }
diff --git a/Indicators/IndicatorBase.cs b/Indicators/IndicatorBase.cs
index b1942c117571..1d59d3bb08f9 100644
--- a/Indicators/IndicatorBase.cs
+++ b/Indicators/IndicatorBase.cs
@@ -27,6 +27,21 @@ namespace QuantConnect.Indicators
///
public abstract partial class IndicatorBase : WindowBase, IIndicator
{
+ ///
+ /// Tracks whether any indicator on the current thread is being updated. Indicators routinely read
+ /// each other's 'Current' while computing, e.g. MACD reads its EMAs before they are ready, so the
+ /// not-ready read warning only applies to reads happening outside of any indicator update
+ ///
+ [ThreadStatic]
+ private static int _updateScopeDepth;
+
+ ///
+ /// True once the not-ready 'Current' read warning is no longer applicable for this indicator,
+ /// either because it was already logged or because the indicator got ready. Keeps the hot path
+ /// to a single boolean check
+ ///
+ private bool _notReadyCurrentWarningDisabled;
+
///
/// The data consolidators associated with this indicator if any
///
@@ -34,6 +49,28 @@ public abstract partial class IndicatorBase : WindowBase, II
/// We need multiple consolitadors because some indicators consume data from multiple different symbols
public ISet Consolidators { get; } = new HashSet();
+ ///
+ /// Gets the current state of this indicator. If the state has not been updated
+ /// then the time on the value will equal DateTime.MinValue.
+ ///
+ /// Reading 'Current' while the indicator is not ready yields a meaningless value, a common
+ /// silent logic bug, so the first such user read logs a warning naming the samples needed vs received
+ public override IndicatorDataPoint Current
+ {
+ get
+ {
+ if (!_notReadyCurrentWarningDisabled)
+ {
+ ValidateCurrentAccess();
+ }
+ return base.Current;
+ }
+ protected set
+ {
+ base.Current = value;
+ }
+ }
+
///
/// Gets the previous state of this indicator. If the state has not been updated
/// then the time on the value will equal DateTime.MinValue.
@@ -100,6 +137,82 @@ protected virtual void OnUpdated(IndicatorDataPoint consolidated)
/// True if this indicator is ready, false otherwise
public abstract bool Update(IBaseData input);
+ ///
+ /// Updating an indicator with a raw value is not supported: an update always requires a time.
+ /// This overload exists to turn a common misuse, e.g. 'indicator.update(bar.close)' in Python, which
+ /// otherwise surfaces as a hard to understand runtime binding error, into a prescriptive error naming
+ /// the valid update forms. Python numeric values bind to it through their double conversion.
+ /// It takes a double on purpose: a decimal overload would make existing 'Update(IndicatorDataPoint)'
+ /// C# call sites ambiguous through the data point's implicit decimal conversion
+ ///
+ /// The value the indicator was incorrectly updated with
+ public bool Update(double value)
+ {
+ if (this is IndicatorBase)
+ {
+ throw new NotSupportedException($"{GetType().Name}.Update() requires a time for each new value." +
+ " Use one of the following methods instead: Update(DateTime, decimal), e.g. 'indicator.Update(bar.EndTime, bar.Close)', or Update(IndicatorDataPoint)");
+ }
+
+ var suggestions = new List
+ {
+ "Update(TradeBar)",
+ "Update(QuoteBar)"
+ };
+
+ if (this is IndicatorBase)
+ {
+ suggestions.Add("Update(Tick)");
+ }
+
+ throw new NotSupportedException($"{GetType().Name} does not support being updated with just a value: it requires a full data bar." +
+ $" Use one of the following methods instead: {string.Join(", ", suggestions)}");
+ }
+
+ ///
+ /// Flags the current thread as updating an indicator, see
+ ///
+ protected static void EnterUpdateScope()
+ {
+ _updateScopeDepth++;
+ }
+
+ ///
+ /// Removes the indicator updating flag from the current thread, see
+ ///
+ protected static void ExitUpdateScope()
+ {
+ _updateScopeDepth--;
+ }
+
+ ///
+ /// Logs a warning, once per indicator, when 'Current' is read outside of any indicator update
+ /// while this indicator is not ready yet
+ ///
+ private void ValidateCurrentAccess()
+ {
+ if (IsReady)
+ {
+ // once ready the warning no longer applies, disable the check so reads only cost a boolean check
+ _notReadyCurrentWarningDisabled = true;
+ return;
+ }
+
+ if (_updateScopeDepth != 0)
+ {
+ // internal read from another indicator's update, not a user read
+ return;
+ }
+
+ _notReadyCurrentWarningDisabled = true;
+ var samplesNeeded = (this as IIndicatorWarmUpPeriodProvider)?.WarmUpPeriod;
+ var samplesReceived = $"received {Samples} sample{(Samples == 1 ? string.Empty : "s")}";
+ var requirement = samplesNeeded > 0 ? $"{samplesReceived} but requires {samplesNeeded}" : samplesReceived;
+ Log.Error($"IndicatorBase.Current: The indicator '{Name}' is not ready yet, it has {requirement}." +
+ " Its value is not meaningful until 'IsReady' is true. Either check 'IsReady' before reading 'Current', or warm the indicator up," +
+ " e.g. with 'Settings.AutomaticIndicatorWarmUp = true' or 'algorithm.WarmUpIndicator()'. This message is only logged once per indicator.");
+ }
+
///
/// ToString Overload for Indicator Base
///
@@ -185,45 +298,53 @@ protected IndicatorBase(string name)
/// True if this indicator is ready, false otherwise
public override bool Update(IBaseData input)
{
- T _previousSymbolInput = default(T);
- if (_previousInput.TryGetValue(input.Symbol.ID, out _previousSymbolInput) && input.EndTime < _previousSymbolInput.EndTime)
- {
- if (!_loggedForwardOnlyIndicatorError)
- {
- _loggedForwardOnlyIndicatorError = true;
- // if we receive a time in the past, log once and return
- Log.Error($"IndicatorBase.Update(): This is a forward only indicator: {Name} Input: {input.EndTime:u} Previous: {_previousSymbolInput.EndTime:u}. It will not be updated with this input.");
- }
- return IsReady;
- }
- if (!ReferenceEquals(input, _previousSymbolInput))
+ EnterUpdateScope();
+ try
{
- // compute a new value and update our previous time
- Samples++;
-
- if (!(input is T))
+ T _previousSymbolInput = default(T);
+ if (_previousInput.TryGetValue(input.Symbol.ID, out _previousSymbolInput) && input.EndTime < _previousSymbolInput.EndTime)
{
- if (typeof(T) == typeof(IndicatorDataPoint))
+ if (!_loggedForwardOnlyIndicatorError)
{
- input = new IndicatorDataPoint(input.Symbol, input.EndTime, input.Value);
+ _loggedForwardOnlyIndicatorError = true;
+ // if we receive a time in the past, log once and return
+ Log.Error($"IndicatorBase.Update(): This is a forward only indicator: {Name} Input: {input.EndTime:u} Previous: {_previousSymbolInput.EndTime:u}. It will not be updated with this input.");
}
- else
+ return IsReady;
+ }
+ if (!ReferenceEquals(input, _previousSymbolInput))
+ {
+ // compute a new value and update our previous time
+ Samples++;
+
+ if (!(input is T))
{
- throw new ArgumentException($"IndicatorBase.Update() 'input' expected to be of type {typeof(T)} but is of type {input.GetType()}");
+ if (typeof(T) == typeof(IndicatorDataPoint))
+ {
+ input = new IndicatorDataPoint(input.Symbol, input.EndTime, input.Value);
+ }
+ else
+ {
+ throw new ArgumentException($"IndicatorBase.Update() 'input' expected to be of type {typeof(T)} but is of type {input.GetType()}");
+ }
}
- }
- _previousInput[input.Symbol.ID] = (T)input;
+ _previousInput[input.Symbol.ID] = (T)input;
- var nextResult = ValidateAndComputeNextValue((T)input);
- if (nextResult.Status == IndicatorStatus.Success)
- {
- Current = new IndicatorDataPoint(input.Symbol, input.EndTime, nextResult.Value);
+ var nextResult = ValidateAndComputeNextValue((T)input);
+ if (nextResult.Status == IndicatorStatus.Success)
+ {
+ Current = new IndicatorDataPoint(input.Symbol, input.EndTime, nextResult.Value);
- // let others know we've produced a new data point
- OnUpdated(Current);
+ // let others know we've produced a new data point
+ OnUpdated(Current);
+ }
}
+ return IsReady;
+ }
+ finally
+ {
+ ExitUpdateScope();
}
- return IsReady;
}
///
diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs
index dd0764671c4d..ccacaa39b96c 100644
--- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs
+++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs
@@ -21,6 +21,7 @@
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Data;
+using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Lean.Engine.DataFeeds;
@@ -802,6 +803,85 @@ def get_indicator(algo, symbol):
Assert.IsNotNull(lastInput);
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void RegisterIndicatorWithConsolidatorRespectsAutomaticIndicatorWarmUp(bool automaticIndicatorWarmUp)
+ {
+ _algorithm.Settings.AutomaticIndicatorWarmUp = automaticIndicatorWarmUp;
+
+ var indicator = new AverageTrueRange(10);
+ var consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(30));
+ _algorithm.RegisterIndicator(_equity, indicator, consolidator);
+
+ Assert.AreEqual(automaticIndicatorWarmUp, indicator.IsReady);
+ if (automaticIndicatorWarmUp)
+ {
+ Assert.GreaterOrEqual(indicator.Samples, indicator.WarmUpPeriod);
+ }
+ else
+ {
+ Assert.AreEqual(0, indicator.Samples);
+ }
+ }
+
+ [Test]
+ public void RegisterIndicatorWithConsolidatorAndSelectorAutomaticallyWarmsUpDataPointIndicator()
+ {
+ var indicator = new RelativeStrengthIndex(14);
+ var consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(30));
+ _algorithm.RegisterIndicator(_equity, indicator, consolidator, Field.Close);
+
+ Assert.IsTrue(indicator.IsReady);
+ Assert.GreaterOrEqual(indicator.Samples, indicator.WarmUpPeriod);
+ }
+
+ [Test]
+ public void RegisterIndicatorWithResolutionAutomaticallyWarmsUpIndicator()
+ {
+ var indicator = new SimpleMovingAverage(10);
+ _algorithm.RegisterIndicator(_equity, indicator, Resolution.Minute, (Func)null);
+
+ Assert.IsTrue(indicator.IsReady);
+ }
+
+ [Test]
+ public void RegisterIndicatorWithNonTimeBasedConsolidatorSkipsAutomaticWarmUp()
+ {
+ var indicator = new RelativeStrengthIndex(14);
+ var consolidator = new RenkoConsolidator(10m);
+ Assert.DoesNotThrow(() => _algorithm.RegisterIndicator(_equity, indicator, consolidator, Field.Close));
+
+ // the consolidation period cannot be inferred, so the indicator is registered but not warmed up
+ Assert.IsFalse(indicator.IsReady);
+ Assert.AreEqual(0, indicator.Samples);
+ }
+
+ [Test]
+ public void RegisterIndicatorWithConsolidatorWarmsUpForexIndicatorWithQuoteData()
+ {
+ // forex is quote only: warming up must not request trade data
+ _algorithm.SetDateTime(new DateTime(2014, 5, 9));
+ var eurusd = _algorithm.AddForex("EURUSD", Resolution.Minute, Market.Oanda).Symbol;
+
+ var indicator = new AverageTrueRange(10);
+ var consolidator = new QuoteBarConsolidator(TimeSpan.FromMinutes(30));
+ _algorithm.RegisterIndicator(eurusd, indicator, consolidator);
+
+ Assert.IsTrue(indicator.IsReady);
+ }
+
+ [Test]
+ public void IndicatorHelpersWarmUpForexIndicatorsWithQuoteData()
+ {
+ // forex is quote only: the ATR helper must warm up from quote bars without any manual history replay
+ _algorithm.SetDateTime(new DateTime(2014, 5, 9));
+ var eurusd = _algorithm.AddForex("EURUSD", Resolution.Minute, Market.Oanda).Symbol;
+
+ var indicator = _algorithm.ATR(eurusd, 10, resolution: Resolution.Minute);
+
+ Assert.IsTrue(indicator.IsReady);
+ }
+
// Some specific indicator helper methods tests
[TestCase("abands", "symbol, 2", false)]
[TestCase("ad", "symbol", false)]
diff --git a/Tests/Indicators/IndicatorTests.cs b/Tests/Indicators/IndicatorTests.cs
index a1114808a9f5..2f91a10030ed 100644
--- a/Tests/Indicators/IndicatorTests.cs
+++ b/Tests/Indicators/IndicatorTests.cs
@@ -20,6 +20,7 @@
using System.Linq.Expressions;
using System.Reflection;
using NUnit.Framework;
+using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
@@ -341,6 +342,131 @@ public void IndicatorShouldRetainSymbolWhenUpdatedWithDifferentDataType()
Assert.AreEqual(Symbols.SPY, target.Current.Symbol);
}
+ [Test]
+ public void ScalarUpdateThrowsPrescriptiveErrorForDataPointIndicators()
+ {
+ // common misuse: 'sma.update(bar.close)', the update requires a time
+ var indicator = new SimpleMovingAverage(3);
+ var exception = Assert.Throws(() => indicator.Update(1.23));
+ StringAssert.Contains("Update(DateTime, decimal)", exception.Message);
+ StringAssert.Contains("Update(IndicatorDataPoint)", exception.Message);
+ }
+
+ [Test]
+ public void ScalarUpdateThrowsPrescriptiveErrorForBarIndicators()
+ {
+ // common misuse: 'atr.update(bar.close)', bar indicators require the full bar
+ var indicator = new AverageTrueRange(10);
+ var exception = Assert.Throws(() => indicator.Update(1.23));
+ StringAssert.Contains("Update(TradeBar)", exception.Message);
+ StringAssert.Contains("Update(QuoteBar)", exception.Message);
+ }
+
+ [TestCase("RelativeStrengthIndex(14)", "Update(DateTime, decimal)")]
+ [TestCase("AverageTrueRange(10)", "Update(TradeBar)")]
+ public void ScalarUpdateThrowsPrescriptiveErrorFromPython(string indicator, string expectedSuggestion)
+ {
+ // reproduces the common misuse 'rsi.update(bar.close)': the raw value must bind to the
+ // Update(double) overload instead of failing with a cryptic runtime binding error
+ using (Py.GIL())
+ {
+ var testModule = PyModule.FromString("ScalarUpdateThrowsPrescriptiveErrorFromPython",
+ @$"
+from AlgorithmImports import *
+
+def run():
+ indicator = {indicator}
+ try:
+ indicator.update(70.5)
+ except Exception as e:
+ return str(e)
+ return None
+");
+ var errorMessage = testModule.GetAttr("run").Invoke().As();
+ Assert.IsNotNull(errorMessage);
+ StringAssert.Contains(expectedSuggestion, errorMessage);
+ }
+ }
+
+ [Test]
+ public void ReadingCurrentBeforeIndicatorIsReadyLogsOncePerIndicator()
+ {
+ var previousHandler = Log.LogHandler;
+ var testHandler = new QueueLogHandler();
+ Log.LogHandler = testHandler;
+ try
+ {
+ var indicator = new SimpleMovingAverage("SMA", 3);
+ indicator.Update(new IndicatorDataPoint(new DateTime(2023, 6, 12, 9, 30, 0), 1m));
+
+ var value = indicator.Current.Value;
+
+ var warnings = testHandler.Logs.Where(entry => entry.Message.Contains("is not ready")).ToList();
+ Assert.AreEqual(1, warnings.Count);
+ StringAssert.Contains("received 1 sample", warnings[0].Message);
+ StringAssert.Contains("requires 3", warnings[0].Message);
+ StringAssert.Contains("SMA", warnings[0].Message);
+
+ // reading again does not log again
+ value = indicator.Current.Value;
+ Assert.AreEqual(1, testHandler.Logs.Count(entry => entry.Message.Contains("is not ready")));
+ }
+ finally
+ {
+ Log.LogHandler = previousHandler;
+ }
+ }
+
+ [Test]
+ public void ReadingCurrentWhenReadyDoesNotLog()
+ {
+ var previousHandler = Log.LogHandler;
+ var testHandler = new QueueLogHandler();
+ Log.LogHandler = testHandler;
+ try
+ {
+ var indicator = new SimpleMovingAverage("SMA", 2);
+ var referenceDate = new DateTime(2023, 6, 12, 9, 30, 0);
+ indicator.Update(new IndicatorDataPoint(referenceDate, 1m));
+ indicator.Update(new IndicatorDataPoint(referenceDate.AddMinutes(1), 2m));
+ Assert.IsTrue(indicator.IsReady);
+
+ var value = indicator.Current.Value;
+
+ Assert.IsFalse(testHandler.Logs.Any(entry => entry.Message.Contains("is not ready")));
+ }
+ finally
+ {
+ Log.LogHandler = previousHandler;
+ }
+ }
+
+ [Test]
+ public void InternalCurrentReadsWhileUpdatingDoNotLog()
+ {
+ var previousHandler = Log.LogHandler;
+ var testHandler = new QueueLogHandler();
+ Log.LogHandler = testHandler;
+ try
+ {
+ // MACD reads its internal EMAs 'Current' values on every update, while they are not ready yet.
+ // Those internal reads must not trigger the not-ready warning
+ var indicator = new MovingAverageConvergenceDivergence(2, 3, 2);
+ var referenceDate = new DateTime(2023, 6, 12, 9, 30, 0);
+ for (var i = 0; i < 2; i++)
+ {
+ indicator.Update(new IndicatorDataPoint(referenceDate.AddMinutes(i), i));
+ }
+ Assert.IsFalse(indicator.IsReady);
+
+ Assert.IsFalse(testHandler.Logs.Any(entry => entry.Message.Contains("is not ready")));
+ }
+ finally
+ {
+ Log.LogHandler = previousHandler;
+ }
+ }
+
private static void TestComparisonOperators()
{
var indicator = new TestIndicator();