diff --git a/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs b/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs new file mode 100644 index 000000000000..3359db0d41e8 --- /dev/null +++ b/Algorithm.CSharp/ConsolidatorRollingWindowRegressionAlgorithm.cs @@ -0,0 +1,153 @@ +/* + * 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++; + + // 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 (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..087321235b8e --- /dev/null +++ b/Algorithm.Python/ConsolidatorRollingWindowRegressionAlgorithm.py @@ -0,0 +1,62 @@ +# 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 + + # 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 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..7498d0aed3e3 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 /// @@ -188,7 +182,7 @@ protected void OnDataConsolidated(T consolidated) DataConsolidatedHandler?.Invoke(this, consolidated); - Consolidated = consolidated; + UpdateConsolidated(consolidated); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -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..2a219eee4a36 --- /dev/null +++ b/Common/Data/Consolidators/ConsolidatorBase.cs @@ -0,0 +1,91 @@ +/* + * 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.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 : IEnumerable + { + /// + /// The default number of consolidated bars to keep in the rolling window history + /// + public static int DefaultWindowSize { get; } = 2; + + private RollingWindow _window; + + /// + /// A rolling window keeping a history of the consolidated bars. The most recent bar is at index 0. + /// + public RollingWindow Window + { + get + { + if (_window == null) + { + _window = new RollingWindow(DefaultWindowSize); + } + return _window; + } + } + + /// + /// 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; } + + /// + /// Indexes the history window, where index 0 is the most recently consolidated bar. + /// + /// The index + /// The ith most recently consolidated bar + public IBaseData 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(); + + /// + /// Updates and adds the bar to the rolling window. + /// + protected void UpdateConsolidated(IBaseData consolidated) + { + Consolidated = consolidated; + Window.Add(consolidated); + } + + /// + /// Resets this consolidator, clearing consolidated data and the rolling window. + /// + public virtual void Reset() + { + Consolidated = null; + _window?.Reset(); + } + } +} diff --git a/Common/Data/Consolidators/DataConsolidator.cs b/Common/Data/Consolidators/DataConsolidator.cs index 8f5f57e46169..1146449fefd5 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,18 +93,9 @@ 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 - Consolidated = consolidated; - } - - /// - /// Resets the consolidator - /// - public virtual void Reset() - { - Consolidated = null; + // 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 + UpdateConsolidated(consolidated); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs index 27676b2aeb4a..3aaad931b889 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); + UpdateConsolidated(consolidated); } } } diff --git a/Common/Data/Consolidators/RenkoConsolidator.cs b/Common/Data/Consolidators/RenkoConsolidator.cs index b2ddf9d8fba9..652290804632 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(); } /// @@ -268,7 +257,7 @@ protected void OnDataConsolidated(RenkoBar consolidated) DataConsolidated?.Invoke(this, consolidated); _currentBar = consolidated; _dataConsolidatedHandler?.Invoke(this, consolidated); - Consolidated = consolidated; + UpdateConsolidated(consolidated); } private void Rising(IBaseData data) diff --git a/Common/Data/Consolidators/SequentialConsolidator.cs b/Common/Data/Consolidators/SequentialConsolidator.cs index 6ce0fccd9e49..a9643549257e 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); + UpdateConsolidated(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..9ce6c9912381 100644 --- a/Common/Python/DataConsolidatorPythonWrapper.cs +++ b/Common/Python/DataConsolidatorPythonWrapper.cs @@ -14,19 +14,42 @@ */ using System; +using System.Collections; +using System.Collections.Generic; using Python.Runtime; using QuantConnect.Data; using QuantConnect.Data.Consolidators; +using QuantConnect.Indicators; 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 : BasePythonWrapper, IDataConsolidator, IEnumerable { internal PyObject Model => Instance; + /// + /// A rolling window keeping a history of the consolidated bars. The most recent bar is at index 0. + /// + public RollingWindow Window { get; } = new RollingWindow(ConsolidatorBase.DefaultWindowSize); + + /// + /// Indexes the history window, where index 0 is the most recently consolidated bar. + /// + public IBaseData 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(); + /// /// Gets the most recently consolidated piece of data. This will be null if this consolidator /// has not produced any data yet. @@ -84,6 +107,7 @@ public event DataConsolidatedHandler DataConsolidated public DataConsolidatorPythonWrapper(PyObject consolidator) : base(consolidator, true) { + DataConsolidated += (_, bar) => Window.Add(bar); } /// @@ -116,6 +140,7 @@ public void Dispose() public void Reset() { InvokeMethod(nameof(Reset)); + Window.Reset(); } } }