diff --git a/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs b/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs
new file mode 100644
index 000000000000..103c41a2718e
--- /dev/null
+++ b/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs
@@ -0,0 +1,162 @@
+/*
+ * 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.Data.Market;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm asserting that consolidators expose a built-in rolling window
+ ///
+ public class ConsolidatorRollingWindowRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private TradeBarConsolidator _consolidator;
+ private int _consolidationCount;
+
+ ///
+ /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
+ ///
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 07);
+ SetEndDate(2013, 10, 11);
+
+ AddEquity("SPY", Resolution.Minute);
+
+ _consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(10));
+ _consolidator.DataConsolidated += OnDataConsolidated;
+ SubscriptionManager.AddConsolidator("SPY", _consolidator);
+ }
+
+ private void OnDataConsolidated(object sender, TradeBar bar)
+ {
+ _consolidationCount++;
+
+ if (_consolidator.Current != _consolidator[0])
+ {
+ throw new RegressionTestException("Expected Current to be the same as Window[0]");
+ }
+
+ // Window[0] must always be the bar just consolidated
+ var currentBar = (TradeBar)_consolidator[0];
+ if (currentBar.Time != bar.Time)
+ {
+ throw new RegressionTestException($"Expected consolidator[0].Time == {bar.Time} but was {currentBar.Time}");
+ }
+ if (currentBar.Close != bar.Close)
+ {
+ throw new RegressionTestException($"Expected consolidator[0].Close == {bar.Close} but was {currentBar.Close}");
+ }
+
+ // After the second consolidation the previous bar must be accessible at index 1
+ if (_consolidator.Window.Count >= 2)
+ {
+ var previous = (TradeBar)_consolidator[1];
+ if (_consolidator.Previous != _consolidator[1])
+ {
+ throw new RegressionTestException("Expected Previous to be the same as Window[1]");
+ }
+ if (previous.Time >= bar.Time)
+ {
+ throw new RegressionTestException($"consolidator[1].Time ({previous.Time}) should be earlier than consolidator[0].Time ({bar.Time})");
+ }
+ if (previous.Close <= 0)
+ {
+ throw new RegressionTestException("consolidator[1].Close should be greater than zero");
+ }
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (_consolidationCount == 0)
+ {
+ throw new RegressionTestException("Expected at least one consolidation but got zero");
+ }
+
+ // Default window size is 2, it must be full
+ if (_consolidator.Window.Count != 2)
+ {
+ throw new RegressionTestException(
+ $"Expected window count of 2 but was {_consolidator.Window.Count}");
+ }
+ }
+
+ ///
+ /// 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 => 3943;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// 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", "0"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "100000"},
+ {"End Equity", "100000"},
+ {"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", "-8.91"},
+ {"Tracking Error", "0.223"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$0.00"},
+ {"Estimated Strategy Capacity", "$0"},
+ {"Lowest Capacity Asset", ""},
+ {"Portfolio Turnover", "0%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
+ };
+ }
+}
diff --git a/Algorithm.Python/ConsolidatorRollingWindowRegressionAlgorithm.py b/Algorithm.Python/ConsolidatorRollingWindowRegressionAlgorithm.py
new file mode 100644
index 000000000000..1b4b4de69d1e
--- /dev/null
+++ b/Algorithm.Python/ConsolidatorRollingWindowRegressionAlgorithm.py
@@ -0,0 +1,67 @@
+# 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 *
+
+###
+### Regression algorithm asserting that consolidators expose a built-in rolling window
+###
+class ConsolidatorRollingWindowRegressionAlgorithm(QCAlgorithm):
+
+ def initialize(self):
+ self.set_start_date(2013, 10, 7)
+ self.set_end_date(2013, 10, 11)
+
+ self.add_equity("SPY", Resolution.MINUTE)
+
+ self._consolidation_count = 0
+ self._consolidator = TradeBarConsolidator(timedelta(minutes=10))
+ self._consolidator.data_consolidated += self._on_data_consolidated
+ self.subscription_manager.add_consolidator("SPY", self._consolidator)
+
+ def _on_data_consolidated(self, sender, bar):
+ self._consolidation_count += 1
+
+ if self._consolidator.current != self._consolidator[0]:
+ raise AssertionError("Expected current to be the same as window[0]")
+
+ # consolidator[0] must always match the bar just fired
+ currentBar = self._consolidator[0]
+ if currentBar.time != bar.time:
+ raise AssertionError(f"Expected consolidator[0].time == {bar.time} but was {currentBar.time}")
+ if currentBar.value != bar.close:
+ raise AssertionError(f"Expected consolidator[0].value == {bar.close} but was {currentBar.value}")
+
+ # After the second consolidation the previous bar must be at index 1
+ if self._consolidator.window.count >= 2:
+ previous = self._consolidator[1]
+ if self._consolidator.previous != self._consolidator[1]:
+ raise AssertionError("Expected previous to be the same as window[1]")
+ if previous.time >= bar.time:
+ raise AssertionError(
+ f"consolidator[1].time ({previous.time}) should be earlier "
+ f"than consolidator[0].time ({bar.time})"
+ )
+ if previous.value <= 0:
+ raise AssertionError("consolidator[1].value should be greater than zero")
+
+ def on_data(self, data):
+ pass
+
+ def on_end_of_algorithm(self):
+ if self._consolidation_count == 0:
+ raise AssertionError("Expected at least one consolidation but got zero")
+
+ # Default window size is 2, it must be full
+ if self._consolidator.window.count != 2:
+ raise AssertionError(f"Expected window count of 2 but was {self._consolidator.window.count}")
diff --git a/Common/Data/Consolidators/BaseTimelessConsolidator.cs b/Common/Data/Consolidators/BaseTimelessConsolidator.cs
index 5fd297bdc09f..a61f4a0deb33 100644
--- a/Common/Data/Consolidators/BaseTimelessConsolidator.cs
+++ b/Common/Data/Consolidators/BaseTimelessConsolidator.cs
@@ -23,7 +23,7 @@ namespace QuantConnect.Data.Consolidators
/// Represents a timeless consolidator which depends on the given values. This consolidator
/// is meant to consolidate data into bars that do not depend on time, e.g., RangeBar's.
///
- public abstract class BaseTimelessConsolidator : IDataConsolidator
+ public abstract class BaseTimelessConsolidator : ConsolidatorBase, IDataConsolidator
where T : IBaseData
{
///
@@ -47,12 +47,6 @@ public abstract class BaseTimelessConsolidator : IDataConsolidator
///
protected virtual T CurrentBar { get; set; }
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- public IBaseData Consolidated { get; protected set; }
-
///
/// Gets a clone of the data being currently consolidated
///
@@ -202,10 +196,10 @@ public virtual void Dispose()
///
/// Resets the consolidator
///
- public virtual void Reset()
+ public override void Reset()
{
- Consolidated = null;
CurrentBar = default(T);
+ base.Reset();
}
///
diff --git a/Common/Data/Consolidators/ConsolidatorBase.cs b/Common/Data/Consolidators/ConsolidatorBase.cs
new file mode 100644
index 000000000000..e804a22ac931
--- /dev/null
+++ b/Common/Data/Consolidators/ConsolidatorBase.cs
@@ -0,0 +1,48 @@
+/*
+ * 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.
+*/
+
+namespace QuantConnect.Data.Consolidators
+{
+ ///
+ /// Provides a base implementation for consolidators, including a built-in rolling window
+ /// that stores the history of consolidated bars.
+ ///
+ public abstract class ConsolidatorBase : WindowBase
+ {
+ ///
+ /// Gets the most recently consolidated piece of data. This will be null if this consolidator
+ /// has not produced any data yet. Setting this property adds the value to the rolling window.
+ ///
+ public IBaseData Consolidated
+ {
+ get
+ {
+ return Window.Count > 0 ? Window[0] : null;
+ }
+ protected set
+ {
+ Window.Add(value);
+ }
+ }
+
+ ///
+ /// Resets this consolidator, clearing consolidated data and the rolling window.
+ ///
+ public virtual void Reset()
+ {
+ ResetWindow();
+ }
+ }
+}
diff --git a/Common/Data/Consolidators/DataConsolidator.cs b/Common/Data/Consolidators/DataConsolidator.cs
index 8f5f57e46169..04d97b76cbde 100644
--- a/Common/Data/Consolidators/DataConsolidator.cs
+++ b/Common/Data/Consolidators/DataConsolidator.cs
@@ -23,7 +23,7 @@ namespace QuantConnect.Data.Consolidators
/// and/or aggregated data.
///
/// The type consumed by the consolidator
- public abstract class DataConsolidator : IDataConsolidator
+ public abstract class DataConsolidator : ConsolidatorBase, IDataConsolidator
where TInput : IBaseData
{
///
@@ -52,15 +52,6 @@ public void Update(IBaseData data)
///
public event DataConsolidatedHandler DataConsolidated;
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- public IBaseData Consolidated
- {
- get; protected set;
- }
-
///
/// Gets a clone of the data being currently consolidated
///
@@ -74,7 +65,7 @@ public abstract IBaseData WorkingData
///
public Type InputType
{
- get { return typeof (TInput); }
+ get { return typeof(TInput); }
}
///
@@ -102,20 +93,11 @@ protected virtual void OnDataConsolidated(IBaseData consolidated)
var handler = DataConsolidated;
if (handler != null) handler(this, consolidated);
- // assign the Consolidated property after the event handlers are fired,
- // this allows the event handlers to look at the new consolidated data
- // and the previous consolidated data at the same time without extra bookkeeping
+ // assign Consolidated (and push to Window) after the event handlers fire,
+ // so handlers can compare the new bar against the previous one without extra bookkeeping
Consolidated = consolidated;
}
- ///
- /// Resets the consolidator
- ///
- public virtual void Reset()
- {
- Consolidated = null;
- }
-
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// 2
public void Dispose()
diff --git a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
index 27676b2aeb4a..17cb46ccf7c6 100644
--- a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
+++ b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
@@ -25,7 +25,7 @@ namespace QuantConnect.Data.Common
///
/// Consolidator for open markets bar only, extended hours bar are not consolidated.
///
- public class MarketHourAwareConsolidator : IDataConsolidator
+ public class MarketHourAwareConsolidator : ConsolidatorBase, IDataConsolidator
{
private readonly bool _dailyStrictEndTimeEnabled;
private readonly bool _extendedMarketHours;
@@ -51,12 +51,6 @@ public class MarketHourAwareConsolidator : IDataConsolidator
///
protected DateTimeZone DataTimeZone { get; set; }
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- public IBaseData Consolidated => Consolidator.Consolidated;
-
///
/// Gets the type consumed by this consolidator
///
@@ -164,12 +158,13 @@ public void Dispose()
///
/// Resets the consolidator
///
- public void Reset()
+ public override void Reset()
{
_useStrictEndTime = false;
ExchangeHours = null;
DataTimeZone = null;
Consolidator.Reset();
+ base.Reset();
}
///
@@ -214,6 +209,7 @@ protected virtual bool UseStrictEndTime(Symbol symbol)
protected virtual void ForwardConsolidatedBar(object sender, IBaseData consolidated)
{
DataConsolidated?.Invoke(this, consolidated);
+ Consolidated = consolidated;
}
}
}
diff --git a/Common/Data/Consolidators/RenkoConsolidator.cs b/Common/Data/Consolidators/RenkoConsolidator.cs
index b2ddf9d8fba9..68e06012a808 100644
--- a/Common/Data/Consolidators/RenkoConsolidator.cs
+++ b/Common/Data/Consolidators/RenkoConsolidator.cs
@@ -24,13 +24,12 @@ namespace QuantConnect.Data.Consolidators
///
/// This implementation replaced the original implementation that was shown to have inaccuracies in its representation
/// of Renko charts. The original implementation has been moved to .
- public class RenkoConsolidator : IDataConsolidator
+ public class RenkoConsolidator : ConsolidatorBase, IDataConsolidator
{
private bool _firstTick = true;
private RenkoBar _lastWicko;
private DataConsolidatedHandler _dataConsolidatedHandler;
private RenkoBar _currentBar;
- private IBaseData _consolidated;
///
/// Time of consolidated close.
@@ -94,16 +93,6 @@ public class RenkoConsolidator : IDataConsolidator
///
public Type OutputType => typeof(RenkoBar);
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- public IBaseData Consolidated
- {
- get { return _consolidated; }
- private set { _consolidated = value; }
- }
-
///
/// Event handler that fires when a new piece of data is produced
///
@@ -244,18 +233,18 @@ public void Dispose()
///
/// Resets the consolidator
///
- public void Reset()
+ public override void Reset()
{
_firstTick = true;
_lastWicko = null;
_currentBar = null;
- _consolidated = null;
CloseOn = default;
CloseRate = default;
HighRate = default;
LowRate = default;
OpenOn = default;
OpenRate = default;
+ base.Reset();
}
///
diff --git a/Common/Data/Consolidators/SequentialConsolidator.cs b/Common/Data/Consolidators/SequentialConsolidator.cs
index 6ce0fccd9e49..af742febc7da 100644
--- a/Common/Data/Consolidators/SequentialConsolidator.cs
+++ b/Common/Data/Consolidators/SequentialConsolidator.cs
@@ -22,7 +22,7 @@ namespace QuantConnect.Data.Consolidators
/// such that data flows from the First to Second consolidator. It's output comes
/// from the Second.
///
- public class SequentialConsolidator : IDataConsolidator
+ public class SequentialConsolidator : ConsolidatorBase, IDataConsolidator
{
///
/// Gets the first consolidator to receive data
@@ -41,17 +41,6 @@ public IDataConsolidator Second
get; private set;
}
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- /// For a SequentialConsolidator, this is the output from the 'Second' consolidator.
- ///
- public IBaseData Consolidated
- {
- get { return Second.Consolidated; }
- }
-
///
/// Gets a clone of the data being currently consolidated
///
@@ -131,6 +120,7 @@ protected virtual void OnDataConsolidated(IBaseData consolidated)
{
var handler = DataConsolidated;
if (handler != null) handler(this, consolidated);
+ Consolidated = consolidated;
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
@@ -145,10 +135,11 @@ public void Dispose()
///
/// Resets the consolidator
///
- public void Reset()
+ public override void Reset()
{
First.Reset();
Second.Reset();
+ base.Reset();
}
}
}
diff --git a/Common/Python/DataConsolidatorPythonWrapper.cs b/Common/Python/DataConsolidatorPythonWrapper.cs
index c2264726e217..85b7c16c8367 100644
--- a/Common/Python/DataConsolidatorPythonWrapper.cs
+++ b/Common/Python/DataConsolidatorPythonWrapper.cs
@@ -23,25 +23,16 @@ namespace QuantConnect.Python
///
/// Provides an Data Consolidator that wraps a object that represents a custom Python consolidator
///
- public class DataConsolidatorPythonWrapper : BasePythonWrapper, IDataConsolidator
+ public class DataConsolidatorPythonWrapper : ConsolidatorBase, IDataConsolidator
{
- internal PyObject Model => Instance;
-
- ///
- /// Gets the most recently consolidated piece of data. This will be null if this consolidator
- /// has not produced any data yet.
- ///
- public IBaseData Consolidated
- {
- get { return GetProperty(nameof(Consolidated)); }
- }
+ private readonly BasePythonWrapper _pythonWrapper;
///
/// Gets a clone of the data being currently consolidated
///
public IBaseData WorkingData
{
- get { return GetProperty(nameof(WorkingData)); }
+ get { return _pythonWrapper.GetProperty(nameof(WorkingData)); }
}
///
@@ -49,7 +40,7 @@ public IBaseData WorkingData
///
public Type InputType
{
- get { return GetProperty(nameof(InputType)); }
+ get { return _pythonWrapper.GetProperty(nameof(InputType)); }
}
///
@@ -57,7 +48,7 @@ public Type InputType
///
public Type OutputType
{
- get { return GetProperty(nameof(OutputType)); }
+ get { return _pythonWrapper.GetProperty(nameof(OutputType)); }
}
///
@@ -67,12 +58,12 @@ public event DataConsolidatedHandler DataConsolidated
{
add
{
- var eventHandler = GetEvent(nameof(DataConsolidated));
+ var eventHandler = _pythonWrapper.GetEvent(nameof(DataConsolidated));
eventHandler += value;
}
remove
{
- var eventHandler = GetEvent(nameof(DataConsolidated));
+ var eventHandler = _pythonWrapper.GetEvent(nameof(DataConsolidated));
eventHandler -= value;
}
}
@@ -82,8 +73,9 @@ public event DataConsolidatedHandler DataConsolidated
///
/// Represents a custom python consolidator
public DataConsolidatorPythonWrapper(PyObject consolidator)
- : base(consolidator, true)
{
+ _pythonWrapper = new BasePythonWrapper(consolidator, true);
+ DataConsolidated += (_, bar) => Consolidated = bar;
}
///
@@ -92,7 +84,7 @@ public DataConsolidatorPythonWrapper(PyObject consolidator)
/// The current time in the local time zone (same as )
public void Scan(DateTime currentLocalTime)
{
- InvokeMethod(nameof(Scan), currentLocalTime);
+ _pythonWrapper.InvokeMethod(nameof(Scan), currentLocalTime);
}
///
@@ -101,21 +93,24 @@ public void Scan(DateTime currentLocalTime)
/// The new data for the consolidator
public void Update(IBaseData data)
{
- InvokeMethod(nameof(Update), data);
+ _pythonWrapper.InvokeMethod(nameof(Update), data);
}
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// 2
- public void Dispose()
+ ///
+ /// Resets the consolidator
+ ///
+ public override void Reset()
{
+ _pythonWrapper.InvokeMethod(nameof(Reset));
+ base.Reset();
}
///
- /// Resets the consolidator
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
- public void Reset()
+ public void Dispose()
{
- InvokeMethod(nameof(Reset));
+ _pythonWrapper.Dispose();
}
}
}
diff --git a/Common/WindowBase.cs b/Common/WindowBase.cs
new file mode 100644
index 000000000000..80f9c5a09ed5
--- /dev/null
+++ b/Common/WindowBase.cs
@@ -0,0 +1,101 @@
+/*
+ * 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.Collections;
+using System.Collections.Generic;
+using QuantConnect.Indicators;
+
+namespace QuantConnect
+{
+ ///
+ /// Provides a base class for types that maintain a rolling window history of values.
+ /// This is the single source of truth for window logic shared between indicators and consolidators.
+ ///
+ /// The type of value stored in the rolling window
+ public abstract class WindowBase : IEnumerable
+ {
+ private RollingWindow _window;
+
+ ///
+ /// The default number of values to keep in the rolling window history
+ ///
+ public static int DefaultWindowSize { get; } = 2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected WindowBase() { }
+
+ ///
+ /// Initializes the rolling window with the given size.
+ ///
+ protected WindowBase(int windowSize)
+ {
+ _window = new RollingWindow(windowSize);
+ }
+
+ ///
+ /// A rolling window keeping a history of values. The most recent value is at index 0.
+ /// Uses lazy initialization to survive Python subclasses that do not call base constructors.
+ ///
+ public RollingWindow Window => _window ??= new RollingWindow(DefaultWindowSize);
+
+ ///
+ /// Gets the most recent value. The protected setter adds the value to the rolling window.
+ ///
+ public virtual T Current
+ {
+ get
+ {
+ return Window[0];
+ }
+ protected set
+ {
+ Window.Add(value);
+ }
+ }
+
+ ///
+ /// Gets the previous value, or default if fewer than two values have been produced.
+ ///
+ public virtual T Previous => Window.Count > 1 ? Window[1] : default;
+
+ ///
+ /// Indexes the history window, where index 0 is the most recent value.
+ ///
+ /// The index
+ /// The ith most recent value
+ public T this[int i] => Window[i];
+
+ ///
+ /// Returns an enumerator that iterates through the history window.
+ ///
+ public IEnumerator GetEnumerator() => Window.GetEnumerator();
+
+ ///
+ /// Returns an enumerator that iterates through the history window.
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ /// Resets the rolling window, clearing all stored values without allocating a new window
+ /// if it has not yet been created.
+ ///
+ protected void ResetWindow()
+ {
+ _window?.Reset();
+ }
+ }
+}
diff --git a/Indicators/IndicatorBase.cs b/Indicators/IndicatorBase.cs
index d601d4228873..73b18f50fd23 100644
--- a/Indicators/IndicatorBase.cs
+++ b/Indicators/IndicatorBase.cs
@@ -19,14 +19,13 @@
using QuantConnect.Logging;
using System.Collections.Generic;
using QuantConnect.Data.Consolidators;
-using System.Collections;
namespace QuantConnect.Indicators
{
///
/// Abstract Indicator base, meant to contain non-generic fields of indicator base to support non-typed inputs
///
- public abstract partial class IndicatorBase : IIndicator, IEnumerable
+ public abstract partial class IndicatorBase : WindowBase, IIndicator
{
///
/// The data consolidators associated with this indicator if any
@@ -35,27 +34,11 @@ public abstract partial class IndicatorBase : IIndicator, IEnumerable
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.
- ///
- public IndicatorDataPoint Current
- {
- get
- {
- return Window[0];
- }
- protected set
- {
- Window.Add(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.
///
- public IndicatorDataPoint Previous
+ public override IndicatorDataPoint Previous
{
get
{
@@ -83,11 +66,6 @@ public IndicatorDataPoint Previous
///
public event IndicatorUpdatedHandler Updated;
- ///
- /// A rolling window keeping a history of the indicator values of a given period
- ///
- public RollingWindow Window { get; }
-
///
/// Resets this indicator to its initial state
///
@@ -96,9 +74,8 @@ public IndicatorDataPoint Previous
///
/// Initializes a new instance of the Indicator class.
///
- protected IndicatorBase()
+ protected IndicatorBase() : base(Indicator.DefaultWindowSize)
{
- Window = new RollingWindow(Indicator.DefaultWindowSize);
Current = new IndicatorDataPoint(DateTime.MinValue, 0m);
}
@@ -129,45 +106,6 @@ protected virtual void OnUpdated(IndicatorDataPoint consolidated)
/// True if this indicator is ready, false otherwise
public abstract bool Update(IBaseData input);
- ///
- /// Indexes the history windows, where index 0 is the most recent indicator value.
- /// If index is greater or equal than the current count, it returns null.
- /// If the index is greater or equal than the window size, it returns null and resizes the windows to i + 1.
- ///
- /// The index
- /// the ith most recent indicator value
- public IndicatorDataPoint this[int i]
- {
- get
- {
- return Window[i];
- }
- }
-
- ///
- /// Returns an enumerator that iterates through the history window.
- ///
- ///
- /// A that can be used to iterate through the history window.
- ///
- /// 1
- public IEnumerator GetEnumerator()
- {
- return Window.GetEnumerator();
- }
-
- ///
- /// Returns an enumerator that iterates through the history window.
- ///
- ///
- /// An object that can be used to iterate through the history window.
- ///
- /// 2
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
-
///
/// ToString Overload for Indicator Base
///
diff --git a/Tests/Common/Data/ConsolidatorBaseTests.cs b/Tests/Common/Data/ConsolidatorBaseTests.cs
new file mode 100644
index 000000000000..7f0894d5c29a
--- /dev/null
+++ b/Tests/Common/Data/ConsolidatorBaseTests.cs
@@ -0,0 +1,157 @@
+/*
+ * 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 NUnit.Framework;
+using QuantConnect.Data;
+using QuantConnect.Data.Consolidators;
+using QuantConnect.Data.Market;
+using QuantConnect.Indicators;
+
+namespace QuantConnect.Tests.Common.Data
+{
+ [TestFixture]
+ public class ConsolidatorBaseTests
+ {
+ [TestCaseSource(nameof(WindowTestCases))]
+ public void WindowStoresConsolidatedBars(IDataConsolidator consolidator, IBaseData[] bars, decimal expectedWindow0, decimal expectedWindow1)
+ {
+ var windowConsolidator = (ConsolidatorBase)consolidator;
+
+ foreach (var bar in bars)
+ {
+ consolidator.Update(bar);
+ }
+
+ Assert.AreEqual(2, windowConsolidator.Window.Count);
+ Assert.AreEqual(expectedWindow0, windowConsolidator.Window[0].Value);
+ Assert.AreEqual(expectedWindow1, windowConsolidator.Window[1].Value);
+ Assert.AreEqual(windowConsolidator.Window[0], windowConsolidator.Consolidated);
+ Assert.AreEqual(expectedWindow0, windowConsolidator[0].Value);
+ Assert.AreEqual(expectedWindow1, windowConsolidator.Previous.Value);
+
+ consolidator.Dispose();
+ }
+
+ private static IEnumerable WindowTestCases()
+ {
+ var reference = new DateTime(2015, 4, 13);
+ var spy = Symbols.SPY;
+ var ibm = Symbols.IBM;
+
+ yield return new TestCaseData(
+ new TradeBarConsolidator(1),
+ new IBaseData[]
+ {
+ new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute },
+ new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute }
+ },
+ 20m, 10m
+ ).SetName("TradeBarConsolidator");
+
+ yield return new TestCaseData(
+ new QuoteBarConsolidator(1),
+ new IBaseData[]
+ {
+ new QuoteBar { Symbol = spy, Time = reference, Value = 10m, Period = Time.OneMinute },
+ new QuoteBar { Symbol = spy, Time = reference.AddMinutes(1), Value = 20m, Period = Time.OneMinute }
+ },
+ 20m, 10m
+ ).SetName("QuoteBarConsolidator");
+
+ yield return new TestCaseData(
+ new TickConsolidator(1),
+ new IBaseData[]
+ {
+ new Tick { Symbol = spy, Time = reference, Value = 10m, TickType = TickType.Trade },
+ new Tick { Symbol = spy, Time = reference.AddMinutes(1), Value = 20m, TickType = TickType.Trade }
+ },
+ 20m, 10m
+ ).SetName("TickConsolidator");
+
+ yield return new TestCaseData(
+ new TickQuoteBarConsolidator(1),
+ new IBaseData[]
+ {
+ new Tick { Symbol = spy, Time = reference, Value = 10m, TickType = TickType.Quote, BidPrice = 10m, AskPrice = 10m },
+ new Tick { Symbol = spy, Time = reference.AddMinutes(1), Value = 20m, TickType = TickType.Quote, BidPrice = 20m, AskPrice = 20m }
+ },
+ 20m, 10m
+ ).SetName("TickQuoteBarConsolidator");
+
+ yield return new TestCaseData(
+ new BaseDataConsolidator(1),
+ new IBaseData[]
+ {
+ new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute },
+ new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute }
+ },
+ 20m, 10m
+ ).SetName("BaseDataConsolidator");
+
+ yield return new TestCaseData(
+ new IdentityDataConsolidator(),
+ new IBaseData[]
+ {
+ new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute },
+ new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute }
+ },
+ 20m, 10m
+ ).SetName("IdentityDataConsolidator");
+
+ yield return new TestCaseData(
+ new ClassicRenkoConsolidator(10),
+ new IBaseData[]
+ {
+ new IndicatorDataPoint(spy, reference, 0m),
+ new IndicatorDataPoint(spy, reference.AddMinutes(1), 10m),
+ new IndicatorDataPoint(spy, reference.AddMinutes(2), 20m)
+ },
+ 20m, 10m
+ ).SetName("ClassicRenkoConsolidator");
+
+ yield return new TestCaseData(
+ new RenkoConsolidator(1m),
+ new IBaseData[]
+ {
+ new IndicatorDataPoint(spy, reference, 10m),
+ new IndicatorDataPoint(spy, reference.AddMinutes(1), 12.1m)
+ },
+ 12m, 11m
+ ).SetName("RenkoConsolidator");
+
+ yield return new TestCaseData(
+ new RangeConsolidator(100, x => x.Value, x => 0m),
+ new IBaseData[]
+ {
+ new IndicatorDataPoint(ibm, reference, 90m),
+ new IndicatorDataPoint(ibm, reference.AddMinutes(1), 94.5m)
+ },
+ 94.03m, 93.02m
+ ).SetName("RangeConsolidator");
+
+ yield return new TestCaseData(
+ new SequentialConsolidator(new TradeBarConsolidator(1), new TradeBarConsolidator(1)),
+ new IBaseData[]
+ {
+ new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute },
+ new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute }
+ },
+ 20m, 10m
+ ).SetName("SequentialConsolidator");
+ }
+ }
+}
diff --git a/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs b/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs
index 3ad59c56396b..030c1cdb0434 100644
--- a/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs
+++ b/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs
@@ -294,6 +294,25 @@ public void WorksWithDailyResolutionAndPreciseEndTimeFalse()
Assert.AreEqual(100, consolidatedData.High);
}
+ [Test]
+ public void WindowIsPopulatedOnConsolidation()
+ {
+ var symbol = Symbols.SPY;
+ using var consolidator = new MarketHourAwareConsolidator(false, Resolution.Daily, typeof(TradeBar), TickType.Trade, false);
+
+ consolidator.Update(new TradeBar() { Time = new DateTime(2015, 04, 13, 12, 0, 0), Period = Time.OneMinute, Symbol = symbol, Close = 100 });
+ consolidator.Scan(new DateTime(2015, 04, 14, 0, 0, 0));
+
+ Assert.AreEqual(1, consolidator.Window.Count);
+
+ consolidator.Update(new TradeBar() { Time = new DateTime(2015, 04, 14, 12, 0, 0), Period = Time.OneMinute, Symbol = symbol, Close = 200 });
+ consolidator.Scan(new DateTime(2015, 04, 15, 0, 0, 0));
+
+ Assert.AreEqual(2, consolidator.Window.Count);
+ Assert.AreEqual(200, ((TradeBar)consolidator.Window[0]).Close);
+ Assert.AreEqual(100, ((TradeBar)consolidator.Window[1]).Close);
+ }
+
protected override IDataConsolidator CreateConsolidator()
{
return new MarketHourAwareConsolidator(true, Resolution.Hour, typeof(TradeBar), TickType.Trade, false);
diff --git a/Tests/Python/DataConsolidatorPythonWrapperTests.cs b/Tests/Python/DataConsolidatorPythonWrapperTests.cs
index 91789712d556..11b66710296c 100644
--- a/Tests/Python/DataConsolidatorPythonWrapperTests.cs
+++ b/Tests/Python/DataConsolidatorPythonWrapperTests.cs
@@ -202,6 +202,85 @@ public void RunRegressionAlgorithm()
parameter.ExpectedFinalStatus);
}
+ [Test]
+ public void WindowIsPopulatedOnConsolidation()
+ {
+ using (Py.GIL())
+ {
+ using var wrapper = CreateFedPythonWrapper(1);
+ Assert.AreEqual(1, wrapper.Window.Count);
+ Assert.IsNotNull(wrapper.Consolidated);
+ Assert.AreEqual(wrapper.Consolidated, wrapper[0]);
+ }
+ }
+
+ [Test]
+ public void WindowKeepsPreviousConsolidatedBar()
+ {
+ using (Py.GIL())
+ {
+ using var wrapper = CreateFedPythonWrapper(1);
+ var firstConsolidated = wrapper.Consolidated;
+
+ FeedConsolidation(wrapper, 1);
+
+ Assert.AreEqual(2, wrapper.Window.Count);
+ Assert.AreNotEqual(firstConsolidated, wrapper[0]);
+ Assert.AreEqual(firstConsolidated, wrapper[1]);
+ Assert.AreEqual(firstConsolidated, wrapper.Previous);
+ }
+ }
+
+ [Test]
+ public void CanIterateOverConsolidatedBars()
+ {
+ using (Py.GIL())
+ {
+ using var wrapper = CreateFedPythonWrapper(2);
+ var bars = wrapper.ToList();
+ Assert.AreEqual(2, bars.Count);
+ Assert.AreEqual(wrapper[0], bars[0]);
+ Assert.AreEqual(wrapper[1], bars[1]);
+ }
+ }
+
+ [Test]
+ public void ResetClearsWindow()
+ {
+ using (Py.GIL())
+ {
+ using var wrapper = CreateFedPythonWrapper(1);
+ wrapper.Reset();
+ Assert.AreEqual(0, wrapper.Window.Count);
+ Assert.IsNull(wrapper.Consolidated);
+ }
+ }
+
+ private static DataConsolidatorPythonWrapper CreateFedPythonWrapper(int consolidations)
+ {
+ var module = PyModule.FromString(Guid.NewGuid().ToString(),
+ "from AlgorithmImports import *\n" +
+ "class CustomConsolidator(QuoteBarConsolidator):\n" +
+ " def __init__(self):\n" +
+ " super().__init__(timedelta(minutes=2))\n");
+
+ var wrapper = new DataConsolidatorPythonWrapper(module.GetAttr("CustomConsolidator").Invoke());
+ FeedConsolidation(wrapper, consolidations);
+ return wrapper;
+ }
+
+ private static void FeedConsolidation(DataConsolidatorPythonWrapper wrapper, int consolidations)
+ {
+ var offset = wrapper.Window.Count * 2;
+ var time = DateTime.Today;
+ for (var i = 0; i < consolidations; i++)
+ {
+ var bar = new QuoteBar { Time = time.AddMinutes(offset + i * 2), Symbol = Symbols.SPY, Bid = new Bar(1, 2, 0.75m, 1.25m), LastBidSize = 3, Value = 1, Period = TimeSpan.FromMinutes(1) };
+ wrapper.Update(bar);
+ wrapper.Scan(time.AddMinutes(offset + (i + 1) * 2));
+ }
+ }
+
[Test]
public void AttachAndTriggerEvent()
{