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..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..208123c2d0f2 --- /dev/null +++ b/Common/Data/Consolidators/ConsolidatorBase.cs @@ -0,0 +1,105 @@ +/* + * 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; + +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; + + /// + /// 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); + } + } + + /// + /// 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); + 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() + { + ResetWindow(); + } + } +} diff --git a/Common/Data/Consolidators/DataConsolidator.cs b/Common/Data/Consolidators/DataConsolidator.cs index 8f5f57e46169..147e9df2b112 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,20 @@ 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; } /// - /// Resets the consolidator + /// Event invocator for the DataConsolidated event. Fires the event and updates the rolling window. /// - 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 27676b2aeb4a..cfe6ae5108ea 100644 --- a/Common/Data/Consolidators/MarketHourAwareConsolidator.cs +++ b/Common/Data/Consolidators/MarketHourAwareConsolidator.cs @@ -15,6 +15,7 @@ using System; using NodaTime; +using QuantConnect.Indicators; using QuantConnect.Util; using QuantConnect.Securities; using QuantConnect.Data.Market; @@ -25,7 +26,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 { private readonly bool _dailyStrictEndTimeEnabled; private readonly bool _extendedMarketHours; @@ -39,7 +40,7 @@ public class MarketHourAwareConsolidator : IDataConsolidator /// /// The consolidator instance /// - protected IDataConsolidator Consolidator { get; } + private ConsolidatorBase Consolidator { get; } /// /// The associated security exchange hours instance @@ -51,26 +52,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. @@ -116,7 +111,7 @@ public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, Resolution re { throw new ArgumentNullException(nameof(dataType), $"{dataType.Name} not supported"); } - Consolidator.DataConsolidated += ForwardConsolidatedBar; + ((IDataConsolidator)Consolidator).DataConsolidated += ForwardConsolidatedBar; } /// @@ -128,7 +123,7 @@ public MarketHourAwareConsolidator(bool dailyStrictEndTimeEnabled, Resolution re /// Updates this consolidator with the specified data /// /// The new data for the consolidator - public virtual void Update(IBaseData data) + public override void Update(IBaseData data) { Initialize(data); @@ -147,7 +142,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); } @@ -155,21 +150,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; + ((IDataConsolidator)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(); } /// @@ -214,6 +211,7 @@ protected virtual bool UseStrictEndTime(Symbol symbol) 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..3f068f78f951 100644 --- a/Common/Data/Consolidators/SequentialConsolidator.cs +++ b/Common/Data/Consolidators/SequentialConsolidator.cs @@ -14,6 +14,7 @@ */ using System; +using QuantConnect.Indicators; namespace QuantConnect.Data.Consolidators { @@ -22,7 +23,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 +42,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 +53,7 @@ public IBaseData WorkingData /// /// Gets the type consumed by this consolidator /// - public Type InputType + public override Type InputType { get { return First.InputType; } } @@ -71,7 +61,7 @@ public Type InputType /// /// Gets the type produced by this consolidator /// - public Type OutputType + public override Type OutputType { get { return Second.OutputType; } } @@ -80,7 +70,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 +79,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); } @@ -127,15 +117,15 @@ 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(); @@ -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..2ba4891b3e67 100644 --- a/Common/Python/DataConsolidatorPythonWrapper.cs +++ b/Common/Python/DataConsolidatorPythonWrapper.cs @@ -23,58 +23,32 @@ 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; /// /// 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 +56,62 @@ public event DataConsolidatedHandler DataConsolidated /// /// Represents a custom python consolidator public DataConsolidatorPythonWrapper(PyObject consolidator) - : base(consolidator, true) { + _pythonWrapper = new BasePythonWrapper(consolidator, true); + var pythonEvent = _pythonWrapper.GetEvent("DataConsolidated"); + pythonEvent += new DataConsolidatedHandler((_, bar) => OnDataConsolidated(bar)); } /// /// 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() + { + _pythonWrapper.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/Common/WindowBase.cs b/Common/WindowBase.cs new file mode 100644 index 000000000000..5aa7281fa347 --- /dev/null +++ b/Common/WindowBase.cs @@ -0,0 +1,100 @@ +/* + * 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 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/Indicators/IndicatorBase.cs b/Indicators/IndicatorBase.cs index d601d4228873..f16ca6c31021 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(Indicator.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..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() {