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() {