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..9a48536c05c2
--- /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
+ current_bar = self._consolidator[0]
+ if current_bar.time != bar.time:
+ raise AssertionError(f"Expected consolidator[0].time == {bar.time} but was {current_bar.time}")
+ if current_bar.value != bar.close:
+ raise AssertionError(f"Expected consolidator[0].value == {bar.close} but was {current_bar.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..a68aeddd03a0 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
where T : IBaseData
{
///
@@ -37,51 +37,31 @@ public abstract class BaseTimelessConsolidator : IDataConsolidator
///
protected Func VolumeSelector { get; set; }
- ///
- /// Event handler type for the IDataConsolidator.DataConsolidated event
- ///
- protected DataConsolidatedHandler DataConsolidatedHandler { get; set; }
-
///
/// Bar being created
///
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
///
- public abstract IBaseData WorkingData { get; }
+ public abstract override IBaseData WorkingData { get; }
///
/// Gets the type consumed by this consolidator
///
- public Type InputType => typeof(IBaseData);
+ public override Type InputType => typeof(IBaseData);
///
/// Gets which is the type emitted in the event.
///
- public virtual Type OutputType => typeof(T);
+ public override Type OutputType => typeof(T);
///
- /// Event handler that fires when a new piece of data is produced
+ /// Typed event handler that fires when a new piece of data is produced
///
public event EventHandler DataConsolidated;
- ///
- /// Event handler that fires when a new piece of data is produced
- ///
- event DataConsolidatedHandler IDataConsolidator.DataConsolidated
- {
- add { DataConsolidatedHandler += value; }
- remove { DataConsolidatedHandler -= value; }
- }
-
///
/// Initializes a new instance of the class.
///
@@ -141,7 +121,7 @@ private static Func TryToConvertSelector(PyObject selector,
/// Updates this consolidator with the specified data
///
/// The new data for the consolidator
- public void Update(IBaseData data)
+ public override void Update(IBaseData data)
{
var currentValue = Selector(data);
var volume = VolumeSelector(data);
@@ -185,34 +165,31 @@ public void Update(IBaseData data)
protected void OnDataConsolidated(T consolidated)
{
DataConsolidated?.Invoke(this, consolidated);
-
- DataConsolidatedHandler?.Invoke(this, consolidated);
-
- Consolidated = consolidated;
+ base.OnDataConsolidated(consolidated);
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// 2
- public virtual void Dispose()
+ public override void Dispose()
{
DataConsolidated = null;
- DataConsolidatedHandler = null;
+ base.Dispose();
}
///
/// Resets the consolidator
///
- public virtual void Reset()
+ public override void Reset()
{
- Consolidated = null;
CurrentBar = default(T);
+ base.Reset();
}
///
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public void Scan(DateTime currentLocalTime)
+ public override void Scan(DateTime currentLocalTime)
{
}
}
diff --git a/Common/Data/Consolidators/ConsolidatorBase.cs b/Common/Data/Consolidators/ConsolidatorBase.cs
new file mode 100644
index 000000000000..df4f9eb8430e
--- /dev/null
+++ b/Common/Data/Consolidators/ConsolidatorBase.cs
@@ -0,0 +1,121 @@
+/*
+ * 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 QuantConnect.Indicators;
+
+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, IDataConsolidator
+ {
+ private DataConsolidatedHandler _dataConsolidated;
+
+ private IBaseData _consolidated;
+
+ ///
+ /// 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,
+ /// setting it to null clears the window.
+ ///
+ public IBaseData Consolidated
+ {
+ get
+ {
+ return _consolidated;
+ }
+ protected set
+ {
+ _consolidated = value;
+ if (value == null)
+ {
+ ResetWindow();
+ }
+ else
+ {
+ Current = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets a clone of the data being currently consolidated
+ ///
+ public abstract IBaseData WorkingData { get; }
+
+ ///
+ /// Gets the type consumed by this consolidator
+ ///
+ public abstract Type InputType { get; }
+
+ ///
+ /// Gets the type produced by this consolidator
+ ///
+ public abstract Type OutputType { get; }
+
+ ///
+ /// Updates this consolidator with the specified data
+ ///
+ /// The new data for the consolidator
+ public abstract void Update(IBaseData data);
+
+ ///
+ /// Scans this consolidator to see if it should emit a bar due to time passing
+ ///
+ /// The current time in the local time zone (same as )
+ public abstract void Scan(DateTime currentLocalTime);
+
+ ///
+ /// Event handler that fires when a new piece of data is produced
+ ///
+ event DataConsolidatedHandler IDataConsolidator.DataConsolidated
+ {
+ add { _dataConsolidated += value; }
+ remove { _dataConsolidated -= value; }
+ }
+
+ ///
+ /// Event invocator for the DataConsolidated event. Fires the event and updates the rolling window.
+ ///
+ protected virtual void OnDataConsolidated(IBaseData consolidated)
+ {
+ _dataConsolidated?.Invoke(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
+ Consolidated = consolidated;
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public virtual void Dispose()
+ {
+ _dataConsolidated = null;
+ }
+
+ ///
+ /// Resets this consolidator, clearing consolidated data and the rolling window.
+ ///
+ public virtual void Reset()
+ {
+ Consolidated = null;
+ }
+ }
+}
diff --git a/Common/Data/Consolidators/DataConsolidator.cs b/Common/Data/Consolidators/DataConsolidator.cs
index 8f5f57e46169..2bfaec1017a9 100644
--- a/Common/Data/Consolidators/DataConsolidator.cs
+++ b/Common/Data/Consolidators/DataConsolidator.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -23,14 +23,14 @@ namespace QuantConnect.Data.Consolidators
/// and/or aggregated data.
///
/// The type consumed by the consolidator
- public abstract class DataConsolidator : IDataConsolidator
+ public abstract class DataConsolidator : ConsolidatorBase
where TInput : IBaseData
{
///
/// Updates this consolidator with the specified data
///
/// The new data for the consolidator
- public void Update(IBaseData data)
+ public override void Update(IBaseData data)
{
if (!(data is TInput))
{
@@ -45,45 +45,27 @@ public void Update(IBaseData data)
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public abstract void Scan(DateTime currentLocalTime);
+ public abstract override void Scan(DateTime currentLocalTime);
///
/// Event handler that fires when a new piece of data is produced
///
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
///
- public abstract IBaseData WorkingData
- {
- get;
- }
+ public abstract override IBaseData WorkingData { get; }
///
/// Gets the type consumed by this consolidator
///
- public Type InputType
- {
- get { return typeof (TInput); }
- }
+ public override Type InputType => typeof(TInput);
///
/// Gets the type produced by this consolidator
///
- public abstract Type OutputType
- {
- get;
- }
+ public abstract override Type OutputType { get; }
///
/// Updates this consolidator with the specified data. This method is
@@ -92,35 +74,21 @@ public abstract Type OutputType
/// The new data for the consolidator
public abstract void Update(TInput data);
- ///
- /// Event invocator for the DataConsolidated event. This should be invoked
- /// by derived classes when they have consolidated a new piece of data.
- ///
- /// The newly consolidated data
- protected virtual void OnDataConsolidated(IBaseData consolidated)
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// 2
+ public override void Dispose()
{
- 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
- Consolidated = consolidated;
+ DataConsolidated = null;
+ base.Dispose();
}
///
- /// Resets the consolidator
+ /// Event invocator for the DataConsolidated event
///
- public virtual void Reset()
+ protected override void OnDataConsolidated(IBaseData consolidated)
{
- Consolidated = null;
- }
-
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// 2
- public void Dispose()
- {
- DataConsolidated = null;
+ DataConsolidated?.Invoke(this, consolidated);
+ base.OnDataConsolidated(consolidated);
}
}
}
\ No newline at end of file
diff --git a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
index e1a6791239a1..e844439b278e 100644
--- a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
+++ b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs
@@ -24,7 +24,7 @@ namespace QuantConnect.Data.Consolidators
///
/// Consolidator for open markets bar only, extended hours bar are not consolidated.
///
- public class MarketHourAwareConsolidator : IDataConsolidator
+ public class MarketHourAwareConsolidator : ConsolidatorBase
{
private readonly bool _dailyStrictEndTimeEnabled;
private readonly bool _extendedMarketHours;
@@ -50,26 +50,20 @@ 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
///
- public Type InputType => Consolidator.InputType;
+ public override Type InputType => Consolidator.InputType;
///
/// Gets a clone of the data being currently consolidated
///
- public IBaseData WorkingData => Consolidator.WorkingData;
+ public override IBaseData WorkingData => Consolidator.WorkingData;
///
/// Gets the type produced by this consolidator
///
- public Type OutputType => Consolidator.OutputType;
+ public override Type OutputType => Consolidator.OutputType;
///
/// Initializes a new instance of the class.
@@ -181,7 +175,7 @@ protected virtual IDataConsolidator CreateConsolidator(Func
/// The new data for the consolidator
- public virtual void Update(IBaseData data)
+ public override void Update(IBaseData data)
{
Initialize(data);
@@ -200,7 +194,7 @@ public virtual void Update(IBaseData data)
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public void Scan(DateTime currentLocalTime)
+ public override void Scan(DateTime currentLocalTime)
{
Consolidator.Scan(currentLocalTime);
}
@@ -208,21 +202,23 @@ public void Scan(DateTime currentLocalTime)
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
- public void Dispose()
+ public override void Dispose()
{
Consolidator.DataConsolidated -= ForwardConsolidatedBar;
Consolidator.Dispose();
+ base.Dispose();
}
///
/// Resets the consolidator
///
- public void Reset()
+ public override void Reset()
{
_useStrictEndTime = false;
ExchangeHours = null;
DataTimeZone = null;
Consolidator.Reset();
+ base.Reset();
}
///
@@ -276,11 +272,13 @@ protected virtual bool UseStrictEndTime(Symbol symbol)
}
///
- /// Will forward the underlying consolidated bar to consumers on this object
+ /// Will forward the underlying consolidated bar to consumers on this object.
+ /// This wrapper keeps its own rolling window in addition to the inner consolidator's window.
///
protected virtual void ForwardConsolidatedBar(object sender, IBaseData consolidated)
{
DataConsolidated?.Invoke(this, consolidated);
+ base.OnDataConsolidated(consolidated);
}
}
}
diff --git a/Common/Data/Consolidators/RenkoConsolidator.cs b/Common/Data/Consolidators/RenkoConsolidator.cs
index b2ddf9d8fba9..b59042290e6a 100644
--- a/Common/Data/Consolidators/RenkoConsolidator.cs
+++ b/Common/Data/Consolidators/RenkoConsolidator.cs
@@ -24,13 +24,11 @@ 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
{
private bool _firstTick = true;
private RenkoBar _lastWicko;
- private DataConsolidatedHandler _dataConsolidatedHandler;
private RenkoBar _currentBar;
- private IBaseData _consolidated;
///
/// Time of consolidated close.
@@ -82,42 +80,23 @@ public class RenkoConsolidator : IDataConsolidator
///
/// Gets a clone of the data being currently consolidated
///
- public IBaseData WorkingData => _currentBar?.Clone();
+ public override IBaseData WorkingData => _currentBar?.Clone();
///
/// Gets the type consumed by this consolidator
///
- public Type InputType => typeof(IBaseData);
+ public override Type InputType => typeof(IBaseData);
///
/// Gets which is the type emitted in the event.
///
- public Type OutputType => typeof(RenkoBar);
+ public override 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
+ /// Typed event handler that fires when a new piece of data is produced
///
public event EventHandler DataConsolidated;
- ///
- /// Event handler that fires when a new piece of data is produced
- ///
- event DataConsolidatedHandler IDataConsolidator.DataConsolidated
- {
- add { _dataConsolidatedHandler += value; }
- remove { _dataConsolidatedHandler -= value; }
- }
-
///
/// Initializes a new instance of the class using the specified .
///
@@ -136,7 +115,7 @@ public RenkoConsolidator(decimal barSize)
/// Updates this consolidator with the specified data
///
/// The new data for the consolidator
- public void Update(IBaseData data)
+ public override void Update(IBaseData data)
{
var rate = data.Price;
@@ -229,33 +208,33 @@ public void Update(IBaseData data)
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public void Scan(DateTime currentLocalTime)
+ public override void Scan(DateTime currentLocalTime)
{
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// 2
- public void Dispose()
+ public override void Dispose()
{
DataConsolidated = null;
- _dataConsolidatedHandler = null;
+ base.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();
}
///
@@ -267,8 +246,7 @@ protected void OnDataConsolidated(RenkoBar consolidated)
{
DataConsolidated?.Invoke(this, consolidated);
_currentBar = consolidated;
- _dataConsolidatedHandler?.Invoke(this, consolidated);
- Consolidated = consolidated;
+ base.OnDataConsolidated(consolidated);
}
private void Rising(IBaseData data)
diff --git a/Common/Data/Consolidators/SequentialConsolidator.cs b/Common/Data/Consolidators/SequentialConsolidator.cs
index 6ce0fccd9e49..485cebe75a47 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
{
///
/// Gets the first consolidator to receive data
@@ -41,21 +41,10 @@ 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
///
- public IBaseData WorkingData
+ public override IBaseData WorkingData
{
get { return Second.WorkingData; }
}
@@ -63,7 +52,7 @@ public IBaseData WorkingData
///
/// Gets the type consumed by this consolidator
///
- public Type InputType
+ public override Type InputType
{
get { return First.InputType; }
}
@@ -71,7 +60,7 @@ public Type InputType
///
/// Gets the type produced by this consolidator
///
- public Type OutputType
+ public override Type OutputType
{
get { return Second.OutputType; }
}
@@ -80,7 +69,7 @@ public Type OutputType
/// Updates this consolidator with the specified data
///
/// The new data for the consolidator
- public void Update(IBaseData data)
+ public override void Update(IBaseData data)
{
First.Update(data);
}
@@ -89,7 +78,7 @@ public void Update(IBaseData data)
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public void Scan(DateTime currentLocalTime)
+ public override void Scan(DateTime currentLocalTime)
{
First.Scan(currentLocalTime);
}
@@ -118,7 +107,7 @@ public SequentialConsolidator(IDataConsolidator first, IDataConsolidator second)
first.DataConsolidated += (sender, consolidated) => second.Update(consolidated);
// wire up the second one's events to also fire this consolidator's event so consumers
- // can attach
+ // can attach. This wrapper also keeps its own window
second.DataConsolidated += (sender, consolidated) => OnDataConsolidated(consolidated);
}
@@ -127,28 +116,30 @@ public SequentialConsolidator(IDataConsolidator first, IDataConsolidator second)
/// by derived classes when they have consolidated a new piece of data.
///
/// The newly consolidated data
- protected virtual void OnDataConsolidated(IBaseData consolidated)
+ protected override void OnDataConsolidated(IBaseData consolidated)
{
- var handler = DataConsolidated;
- if (handler != null) handler(this, consolidated);
+ DataConsolidated?.Invoke(this, consolidated);
+ base.OnDataConsolidated(consolidated);
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// 2
- public void Dispose()
+ public override void Dispose()
{
First.Dispose();
Second.Dispose();
DataConsolidated = null;
+ base.Dispose();
}
///
/// Resets the consolidator
///
- public void Reset()
+ public override void Reset()
{
First.Reset();
Second.Reset();
+ base.Reset();
}
}
}
diff --git a/Common/Indicators/WindowBase.cs b/Common/Indicators/WindowBase.cs
new file mode 100644
index 000000000000..ca3319e2ef81
--- /dev/null
+++ b/Common/Indicators/WindowBase.cs
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+namespace QuantConnect.Indicators
+{
+ ///
+ /// 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 support Python subclasses that do not call base constructors.
+ ///
+ public virtual 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.
+ ///
+ protected void ResetWindow()
+ {
+ _window?.Reset();
+ }
+ }
+}
diff --git a/Common/Python/DataConsolidatorPythonWrapper.cs b/Common/Python/DataConsolidatorPythonWrapper.cs
index c2264726e217..cde0b04da3ed 100644
--- a/Common/Python/DataConsolidatorPythonWrapper.cs
+++ b/Common/Python/DataConsolidatorPythonWrapper.cs
@@ -23,58 +23,33 @@ 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
{
- 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;
+ private readonly DataConsolidatedHandler _pythonDataConsolidated;
///
/// Gets a clone of the data being currently consolidated
///
- public IBaseData WorkingData
+ public override IBaseData WorkingData
{
- get { return GetProperty(nameof(WorkingData)); }
+ get { return _pythonWrapper.GetProperty(nameof(WorkingData)); }
}
///
/// Gets the type consumed by this consolidator
///
- public Type InputType
+ public override Type InputType
{
- get { return GetProperty(nameof(InputType)); }
+ get { return _pythonWrapper.GetProperty(nameof(InputType)); }
}
///
/// Gets the type produced by this consolidator
///
- public Type OutputType
- {
- get { return GetProperty(nameof(OutputType)); }
- }
-
- ///
- /// Event handler that fires when a new piece of data is produced
- ///
- public event DataConsolidatedHandler DataConsolidated
+ public override Type OutputType
{
- add
- {
- var eventHandler = GetEvent(nameof(DataConsolidated));
- eventHandler += value;
- }
- remove
- {
- var eventHandler = GetEvent(nameof(DataConsolidated));
- eventHandler -= value;
- }
+ get { return _pythonWrapper.GetProperty(nameof(OutputType)); }
}
///
@@ -82,40 +57,66 @@ public event DataConsolidatedHandler DataConsolidated
///
/// Represents a custom python consolidator
public DataConsolidatorPythonWrapper(PyObject consolidator)
- : base(consolidator, true)
{
+ _pythonWrapper = new BasePythonWrapper(consolidator, true);
+ _pythonDataConsolidated = (_, bar) => OnDataConsolidated(bar);
+ var pythonEvent = _pythonWrapper.GetEvent("DataConsolidated");
+ pythonEvent += _pythonDataConsolidated;
}
///
/// Scans this consolidator to see if it should emit a bar due to time passing
///
/// The current time in the local time zone (same as )
- public void Scan(DateTime currentLocalTime)
+ public override void Scan(DateTime currentLocalTime)
{
- InvokeMethod(nameof(Scan), currentLocalTime);
+ _pythonWrapper.InvokeMethod(nameof(Scan), currentLocalTime);
}
///
/// Updates this consolidator with the specified data
///
/// The new data for the consolidator
- public void Update(IBaseData data)
+ public override 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 override void Dispose()
+ {
+ var pythonEvent = _pythonWrapper.GetEvent("DataConsolidated");
+ pythonEvent -= _pythonDataConsolidated;
+ _pythonWrapper.Dispose();
+ base.Dispose();
+ }
+
+ ///
+ /// Two wrappers are equal if they wrap the same Python object reference.
///
- public void Reset()
+ public override bool Equals(object obj)
{
- InvokeMethod(nameof(Reset));
+ if (obj is DataConsolidatorPythonWrapper other)
+ {
+ return _pythonWrapper.Equals(other._pythonWrapper);
+ }
+ return _pythonWrapper.Equals(obj);
}
+
+ ///
+ /// Hash code based on the underlying Python object reference.
+ ///
+ public override int GetHashCode() => _pythonWrapper.GetHashCode();
}
}
diff --git a/Indicators/Indicator.cs b/Indicators/Indicator.cs
index c38249eb480e..81a499239394 100644
--- a/Indicators/Indicator.cs
+++ b/Indicators/Indicator.cs
@@ -21,11 +21,6 @@ namespace QuantConnect.Indicators
///
public abstract class Indicator : IndicatorBase
{
- ///
- /// The default size of the history window for the indicator
- ///
- public static int DefaultWindowSize { get; } = 2;
-
///
/// Initializes a new instance of the Indicator class using the specified name.
///
diff --git a/Indicators/IndicatorBase.cs b/Indicators/IndicatorBase.cs
index d601d4228873..b1942c117571 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,33 +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
- {
- get
- {
- return Window.Count > 1 ? Window[1] : new IndicatorDataPoint(DateTime.MinValue, 0);
- }
- }
+ public override IndicatorDataPoint Previous => base.Previous ?? new IndicatorDataPoint(DateTime.MinValue, 0);
///
/// Gets a name for this indicator
@@ -83,11 +60,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 +68,8 @@ public IndicatorDataPoint Previous
///
/// Initializes a new instance of the Indicator class.
///
- protected IndicatorBase()
+ protected IndicatorBase() : base(DefaultWindowSize)
{
- Window = new RollingWindow(Indicator.DefaultWindowSize);
Current = new IndicatorDataPoint(DateTime.MinValue, 0m);
}
@@ -129,45 +100,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..30374bca0871
--- /dev/null
+++ b/Tests/Common/Data/ConsolidatorBaseTests.cs
@@ -0,0 +1,229 @@
+/*
+ * 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();
+ }
+
+ [Test]
+ public void HandlerSeesPreviousConsolidatedBarWhileReceivingTheNewOne()
+ {
+ var reference = new DateTime(2015, 4, 13);
+ var spy = Symbols.SPY;
+ var consolidator = new IdentityDataConsolidator();
+
+ IBaseData eventArgument = null;
+ IBaseData consolidatedInsideHandler = null;
+ consolidator.DataConsolidated += (_, bar) =>
+ {
+ eventArgument = bar;
+ consolidatedInsideHandler = ((ConsolidatorBase)consolidator).Consolidated;
+ };
+
+ // First bar: inside the handler Consolidated is still null (no previous bar yet)
+ var first = new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute };
+ consolidator.Update(first);
+ Assert.AreEqual(first, eventArgument);
+ Assert.IsNull(consolidatedInsideHandler);
+
+ // Second bar: the handler receives the new bar as argument while Consolidated still holds the previous one
+ var second = new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute };
+ consolidator.Update(second);
+ Assert.AreEqual(second, eventArgument);
+ Assert.AreEqual(first, consolidatedInsideHandler);
+
+ // Once the handler returns, the window reflects the latest consolidated bar
+ Assert.AreEqual(second, ((ConsolidatorBase)consolidator).Consolidated);
+
+ consolidator.Dispose();
+ }
+
+ [Test]
+ public void CurrentAndPreviousAreNullBeforeFirstConsolidation()
+ {
+ var consolidator = new TradeBarConsolidator(1);
+ var windowConsolidator = (ConsolidatorBase)consolidator;
+
+ Assert.IsNull(windowConsolidator.Consolidated);
+ Assert.IsNull(windowConsolidator.Current);
+ Assert.IsNull(windowConsolidator.Previous);
+ Assert.IsNull(windowConsolidator[0]);
+ Assert.AreEqual(0, windowConsolidator.Window.Count);
+
+ consolidator.Dispose();
+ }
+
+ [Test]
+ public void ResetClearsWindowAndConsolidated()
+ {
+ var reference = new DateTime(2015, 4, 13);
+ var spy = Symbols.SPY;
+ var consolidator = new TradeBarConsolidator(1);
+ var windowConsolidator = (ConsolidatorBase)consolidator;
+
+ consolidator.Update(new TradeBar { Symbol = spy, Time = reference, Close = 10m, Value = 10m, Period = Time.OneMinute });
+ consolidator.Update(new TradeBar { Symbol = spy, Time = reference.AddMinutes(1), Close = 20m, Value = 20m, Period = Time.OneMinute });
+
+ Assert.AreEqual(2, windowConsolidator.Window.Count);
+ Assert.IsNotNull(windowConsolidator.Consolidated);
+
+ windowConsolidator.Reset();
+
+ Assert.AreEqual(0, windowConsolidator.Window.Count);
+ Assert.IsNull(windowConsolidator.Consolidated);
+ Assert.IsNull(windowConsolidator.Current);
+ Assert.IsNull(windowConsolidator.Previous);
+
+ 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 06065a620cf5..67a9a83ab99e 100644
--- a/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs
+++ b/Tests/Common/Data/MarketHourAwareConsolidatorTests.cs
@@ -71,6 +71,35 @@ public void MarketAlwaysOpen()
Assert.AreEqual(1, latestBar.Low);
}
+ [Test]
+ public void HandlerSeesPreviousConsolidatedBarWhileReceivingTheNewOne()
+ {
+ var symbol = Symbols.BTCUSD;
+ using var consolidator = new MarketHourAwareConsolidator(true, Resolution.Daily, typeof(TradeBar), TickType.Trade, false);
+
+ IBaseData eventArgument = null;
+ IBaseData consolidatedInsideHandler = null;
+ consolidator.DataConsolidated += (_, bar) =>
+ {
+ eventArgument = bar;
+ consolidatedInsideHandler = ((ConsolidatorBase)consolidator).Consolidated;
+ };
+
+ var time = new DateTime(2015, 04, 13, 10, 0, 0);
+ consolidator.Update(new TradeBar() { Time = time.Subtract(Time.OneMinute), Period = Time.OneMinute, Symbol = symbol, High = 100 });
+
+ time = new DateTime(2015, 04, 14, 0, 0, 0);
+ consolidator.Scan(time);
+
+ // The handler receives the new bar as argument while Consolidated still holds the previous
+ // state, which is null here since this is the first consolidation
+ Assert.IsNotNull(eventArgument);
+ Assert.IsNull(consolidatedInsideHandler);
+
+ // Once the handler returned, the window reflects the just-consolidated bar
+ Assert.AreEqual(eventArgument, ((ConsolidatorBase)consolidator).Consolidated);
+ }
+
[TestCase(true)]
[TestCase(false)]
public void Daily(bool strictEndTime)
@@ -379,6 +408,25 @@ private static void Feed(IDataConsolidator consolidator, Symbol symbol, DateTime
}
}
+ [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 cb91e6506f52..914e265feb11 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()
{