diff --git a/Algorithm.CSharp/RegressionTests/CorrelationLastComputedValueRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/CorrelationLastComputedValueRegressionAlgorithm.cs new file mode 100644 index 000000000000..0c07e9b85d5f --- /dev/null +++ b/Algorithm.CSharp/RegressionTests/CorrelationLastComputedValueRegressionAlgorithm.cs @@ -0,0 +1,136 @@ +/* + * 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.Generic; +using QuantConnect.Data; +using QuantConnect.Indicators; +using QuantConnect.Interfaces; + +namespace QuantConnect.Algorithm.CSharp.RegressionTests +{ + /// + /// Validates the indicator by ensuring no mismatch between the last computed value + /// and the expected value. Also verifies proper functionality across different time zones. + /// + public class CorrelationLastComputedValueRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Correlation _correlationPearson; + private decimal _lastCorrelationValue; + private decimal _totalCount; + private decimal _matchingCount; + + public override void Initialize() + { + SetStartDate(2015, 05, 08); + SetEndDate(2017, 06, 15); + + EnableAutomaticIndicatorWarmUp = true; + AddCrypto("BTCUSD", Resolution.Daily); + AddEquity("SPY", Resolution.Daily); + + _correlationPearson = C("BTCUSD", "SPY", 3, CorrelationType.Pearson, Resolution.Daily); + if (!_correlationPearson.IsReady) + { + throw new RegressionTestException("Correlation indicator was expected to be ready"); + } + _lastCorrelationValue = _correlationPearson.Current.Value; + _totalCount = 0; + _matchingCount = 0; + } + + public override void OnData(Slice slice) + { + if (_lastCorrelationValue == _correlationPearson[1].Value) + { + _matchingCount++; + } + Debug($"CorrelationPearson between BTCUSD and SPY - Current: {_correlationPearson[0].Value}, Previous: {_correlationPearson[1].Value}"); + _lastCorrelationValue = _correlationPearson.Current.Value; + _totalCount++; + } + + public override void OnEndOfAlgorithm() + { + if (_totalCount == 0) + { + throw new RegressionTestException("No data points were processed."); + } + if (_totalCount != _matchingCount) + { + throw new RegressionTestException("Mismatch in the last computed CorrelationPearson values."); + } + Debug($"{_totalCount} data points were processed, {_matchingCount} matched the last computed value."); + } + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// 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 => 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 }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 5798; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 72; + + /// + /// 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.00"}, + {"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", "-0.616"}, + {"Tracking Error", "0.111"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"} + }; + } +} diff --git a/Indicators/Beta.cs b/Indicators/Beta.cs index 480f4b8e7094..c1303021ba3b 100644 --- a/Indicators/Beta.cs +++ b/Indicators/Beta.cs @@ -16,8 +16,6 @@ using System; using QuantConnect.Data.Market; using MathNet.Numerics.Statistics; -using QuantConnect.Securities; -using NodaTime; namespace QuantConnect.Indicators { @@ -32,58 +30,8 @@ namespace QuantConnect.Indicators /// The indicator only updates when both assets have a price for a time step. When a bar is missing for one of the assets, /// the indicator value fills forward to improve the accuracy of the indicator. /// - public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider + public class Beta : DualSymbolIndicator { - /// - /// RollingWindow to store the data points of the target symbol - /// - private readonly RollingWindow _targetDataPoints; - - /// - /// RollingWindow to store the data points of the reference symbol - /// - private readonly RollingWindow _referenceDataPoints; - - /// - /// Symbol of the reference used - /// - private readonly Symbol _referenceSymbol; - - /// - /// Symbol of the target used - /// - private readonly Symbol _targetSymbol; - - /// - /// Stores the previous input data point. - /// - private IBaseDataBar _previousInput; - - /// - /// Indicates whether the previous symbol is the target symbol. - /// - private bool _previousSymbolIsTarget; - - /// - /// Indicates if the time zone for the target and reference are different. - /// - private bool _isTimezoneDifferent; - - /// - /// Time zone of the target symbol. - /// - private DateTimeZone _targetTimeZone; - - /// - /// Time zone of the reference symbol. - /// - private DateTimeZone _referenceTimeZone; - - /// - /// The resolution of the data (e.g., daily, hourly, etc.). - /// - private Resolution _resolution; - /// /// RollingWindow of returns of the target symbol in the given period /// @@ -94,16 +42,6 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider /// private readonly RollingWindow _referenceReturns; - /// - /// Beta of the target used in relation with the reference - /// - private decimal _beta; - - /// - /// Required period, in data points, for the indicator to be ready and fully initialized. - /// - public int WarmUpPeriod { get; private set; } - /// /// Gets a flag indicating when the indicator is ready and fully initialized /// @@ -118,27 +56,17 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider /// The period of this indicator /// The reference symbol of this indicator public Beta(string name, Symbol targetSymbol, Symbol referenceSymbol, int period) - : base(name) + : base(name, targetSymbol, referenceSymbol, 2) { // Assert the period is greater than two, otherwise the beta can not be computed if (period < 2) { throw new ArgumentException($"Period parameter for Beta indicator must be greater than 2 but was {period}."); } - _referenceSymbol = referenceSymbol; - _targetSymbol = targetSymbol; - - _targetDataPoints = new RollingWindow(2); - _referenceDataPoints = new RollingWindow(2); _targetReturns = new RollingWindow(period); _referenceReturns = new RollingWindow(period); - _beta = 0; - var dataFolder = MarketHoursDatabase.FromDataFolder(); - _targetTimeZone = dataFolder.GetExchangeHours(_targetSymbol.ID.Market, _targetSymbol, _targetSymbol.ID.SecurityType).TimeZone; - _referenceTimeZone = dataFolder.GetExchangeHours(_referenceSymbol.ID.Market, _referenceSymbol, _referenceSymbol.ID.SecurityType).TimeZone; - _isTimezoneDifferent = _targetTimeZone != _referenceTimeZone; - WarmUpPeriod = period + 1 + (_isTimezoneDifferent ? 1 : 0); + WarmUpPeriod = period + 1 + (IsTimezoneDifferent ? 1 : 0); } /// @@ -167,97 +95,32 @@ public Beta(string name, int period, Symbol targetSymbol, Symbol referenceSymbol { } - /// - /// Computes the next value for this indicator from the given state. - /// - /// As this indicator is receiving data points from two different symbols, - /// it's going to compute the next value when the amount of data points - /// of each of them is the same. Otherwise, it will return the last beta - /// value computed - /// - /// The input value of this indicator on this time step. - /// It can be either from the target or the reference symbol - /// The beta value of the target used in relation with the reference - protected override decimal ComputeNextValue(IBaseDataBar input) - { - if (_previousInput == null) - { - _previousInput = input; - _previousSymbolIsTarget = input.Symbol == _targetSymbol; - var timeDifference = input.EndTime - input.Time; - _resolution = timeDifference.TotalHours > 1 ? Resolution.Daily : timeDifference.ToHigherResolutionEquivalent(false); - return decimal.Zero; - } - - var inputEndTime = input.EndTime; - var previousInputEndTime = _previousInput.EndTime; - - if (_isTimezoneDifferent) - { - inputEndTime = inputEndTime.ConvertToUtc(_previousSymbolIsTarget ? _referenceTimeZone : _targetTimeZone); - previousInputEndTime = previousInputEndTime.ConvertToUtc(_previousSymbolIsTarget ? _targetTimeZone : _referenceTimeZone); - } - - // Process data if symbol has changed and timestamps match - if (input.Symbol != _previousInput.Symbol && TruncateToResolution(inputEndTime) == TruncateToResolution(previousInputEndTime)) - { - AddDataPoint(input); - AddDataPoint(_previousInput); - ComputeBeta(); - } - _previousInput = input; - _previousSymbolIsTarget = input.Symbol == _targetSymbol; - return _beta; - } - - /// - /// Truncates the given DateTime based on the specified resolution (Daily, Hourly, Minute, or Second). - /// - /// The DateTime to truncate. - /// A DateTime truncated to the specified resolution. - private DateTime TruncateToResolution(DateTime date) - { - switch (_resolution) - { - case Resolution.Daily: - return date.Date; - case Resolution.Hour: - return date.Date.AddHours(date.Hour); - case Resolution.Minute: - return date.Date.AddHours(date.Hour).AddMinutes(date.Minute); - case Resolution.Second: - return date; - default: - return date; - } - } - /// /// Adds the closing price to the corresponding symbol's data set (target or reference). /// Computes returns when there are enough data points for each symbol. /// /// The input value for this symbol - private void AddDataPoint(IBaseDataBar input) + protected override void AddDataPoint(IBaseDataBar input) { - if (input.Symbol == _targetSymbol) + if (input.Symbol == TargetSymbol) { - _targetDataPoints.Add(input.Close); - if (_targetDataPoints.Count > 1) + TargetDataPoints.Add(input.Close); + if (TargetDataPoints.Count > 1) { - _targetReturns.Add(GetNewReturn(_targetDataPoints)); + _targetReturns.Add(GetNewReturn(TargetDataPoints)); } } - else if (input.Symbol == _referenceSymbol) + else if (input.Symbol == ReferenceSymbol) { - _referenceDataPoints.Add(input.Close); - if (_referenceDataPoints.Count > 1) + ReferenceDataPoints.Add(input.Close); + if (ReferenceDataPoints.Count > 1) { - _referenceReturns.Add(GetNewReturn(_referenceDataPoints)); + _referenceReturns.Add(GetNewReturn(ReferenceDataPoints)); } } else { - throw new ArgumentException($"The given symbol {input.Symbol} was not {_targetSymbol} or {_referenceSymbol} symbol"); + throw new ArgumentException($"The given symbol {input.Symbol} was not {TargetSymbol} or {ReferenceSymbol} symbol"); } } @@ -276,7 +139,7 @@ private static double GetNewReturn(RollingWindow rollingWindow) /// Computes the beta value of the target in relation with the reference /// using the target and reference returns /// - private void ComputeBeta() + protected override void ComputeIndicator() { var varianceComputed = _referenceReturns.Variance(); var covarianceComputed = _targetReturns.Covariance(_referenceReturns); @@ -284,7 +147,7 @@ private void ComputeBeta() // Avoid division with NaN or by zero var variance = !varianceComputed.IsNaNOrZero() ? varianceComputed : 1; var covariance = !covarianceComputed.IsNaNOrZero() ? covarianceComputed : 0; - _beta = (decimal)(covariance / variance); + IndicatorValue = (decimal)(covariance / variance); } /// @@ -292,12 +155,8 @@ private void ComputeBeta() /// public override void Reset() { - _previousInput = null; - _targetDataPoints.Reset(); - _referenceDataPoints.Reset(); _targetReturns.Reset(); _referenceReturns.Reset(); - _beta = 0; base.Reset(); } } diff --git a/Indicators/Correlation.cs b/Indicators/Correlation.cs index 6418b6705ebe..2a8a67773f53 100644 --- a/Indicators/Correlation.cs +++ b/Indicators/Correlation.cs @@ -30,53 +30,21 @@ namespace QuantConnect.Indicators /// Commonly, the SPX index is employed as the benchmark for the overall market when calculating correlation, /// ensuring a consistent and reliable reference point. This helps traders and investors make informed decisions /// regarding the risk and behavior of the target security in relation to market trends. + /// + /// The indicator only updates when both assets have a price for a time step. When a bar is missing for one of the assets, + /// the indicator value fills forward to improve the accuracy of the indicator. /// - public class Correlation : BarIndicator, IIndicatorWarmUpPeriodProvider + public class Correlation : DualSymbolIndicator { - /// - /// RollingWindow to store the data points of the target symbol - /// - private readonly RollingWindow _targetDataPoints; - - /// - /// RollingWindow to store the data points of the reference symbol - /// - private readonly RollingWindow _referenceDataPoints; - - /// - /// Correlation of the target used in relation with the reference - /// - private decimal _correlation; - - /// - /// Period required for calcualte correlation - /// - private readonly decimal _period; - /// /// Correlation type /// private readonly CorrelationType _correlationType; - /// - /// Symbol of the reference used - /// - private readonly Symbol _referenceSymbol; - - /// - /// Symbol of the target used - /// - private readonly Symbol _targetSymbol; - - /// - /// Required period, in data points, for the indicator to be ready and fully initialized. - /// - public int WarmUpPeriod { get; private set; } - /// /// Gets a flag indicating when the indicator is ready and fully initialized /// - public override bool IsReady => _targetDataPoints.Samples >= WarmUpPeriod && _referenceDataPoints.Samples >= WarmUpPeriod; + public override bool IsReady => TargetDataPoints.IsReady && ReferenceDataPoints.IsReady; /// /// Creates a new Correlation indicator with the specified name, target, reference, @@ -88,25 +56,15 @@ public class Correlation : BarIndicator, IIndicatorWarmUpPeriodProvider /// The reference symbol of this indicator /// Correlation type public Correlation(string name, Symbol targetSymbol, Symbol referenceSymbol, int period, CorrelationType correlationType = CorrelationType.Pearson) - : base(name) + : base(name, targetSymbol, referenceSymbol, period) { // Assert the period is greater than two, otherwise the correlation can not be computed if (period < 2) { throw new ArgumentException($"Period parameter for Correlation indicator must be greater than 2 but was {period}"); } - - WarmUpPeriod = period + 1; - _period = period; - - _referenceSymbol = referenceSymbol; - _targetSymbol = targetSymbol; - + WarmUpPeriod = period + (IsTimezoneDifferent ? 1 : 0); _correlationType = correlationType; - - _targetDataPoints = new RollingWindow(period); - _referenceDataPoints = new RollingWindow(period); - } /// @@ -123,71 +81,46 @@ public Correlation(Symbol targetSymbol, Symbol referenceSymbol, int period, Corr } /// - /// Computes the next value for this indicator from the given state. - /// - /// As this indicator is receiving data points from two different symbols, - /// it's going to compute the next value when the amount of data points - /// of each of them is the same. Otherwise, it will return the last correlation - /// value computed + /// Adds the closing price to the target or reference symbol's data set. /// - /// The input value of this indicator on this time step. - /// It can be either from the target or the reference symbol - /// The correlation value of the target used in relation with the reference - protected override decimal ComputeNextValue(IBaseDataBar input) + /// The input value for this symbol + /// Thrown if the input symbol is not the target or reference symbol. + protected override void AddDataPoint(IBaseDataBar input) { - var inputSymbol = input.Symbol; - if (inputSymbol == _targetSymbol) + if (input.Symbol == TargetSymbol) { - _targetDataPoints.Add((double)input.Value); + TargetDataPoints.Add((double)input.Close); } - else if (inputSymbol == _referenceSymbol) + else if (input.Symbol == ReferenceSymbol) { - _referenceDataPoints.Add((double)input.Value); + ReferenceDataPoints.Add((double)input.Close); } else { - throw new ArgumentException("The given symbol was not target or reference symbol"); + throw new ArgumentException($"The given symbol {input.Symbol} was not {TargetSymbol} or {ReferenceSymbol} symbol"); } - ComputeCorrelation(); - return _correlation; } /// /// Computes the correlation value usuing symbols values /// correlation values assing into _correlation property /// - private void ComputeCorrelation() + protected override void ComputeIndicator() { - if (_targetDataPoints.Count < _period || _referenceDataPoints.Count < _period) - { - _correlation = 0; - return; - } var newCorrelation = 0d; if (_correlationType == CorrelationType.Pearson) { - newCorrelation = MathNet.Numerics.Statistics.Correlation.Pearson(_targetDataPoints, _referenceDataPoints); + newCorrelation = MathNet.Numerics.Statistics.Correlation.Pearson(TargetDataPoints, ReferenceDataPoints); } if (_correlationType == CorrelationType.Spearman) { - newCorrelation = MathNet.Numerics.Statistics.Correlation.Spearman(_targetDataPoints, _referenceDataPoints); + newCorrelation = MathNet.Numerics.Statistics.Correlation.Spearman(TargetDataPoints, ReferenceDataPoints); } if (newCorrelation.IsNaNOrZero()) { newCorrelation = 0; } - _correlation = Extensions.SafeDecimalCast(newCorrelation); - } - - /// - /// Resets this indicator to its initial state - /// - public override void Reset() - { - _targetDataPoints.Reset(); - _referenceDataPoints.Reset(); - _correlation = 0; - base.Reset(); + IndicatorValue = Extensions.SafeDecimalCast(newCorrelation); } } } diff --git a/Indicators/DualSymbolIndicator.cs b/Indicators/DualSymbolIndicator.cs new file mode 100644 index 000000000000..57f3664bb59d --- /dev/null +++ b/Indicators/DualSymbolIndicator.cs @@ -0,0 +1,212 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using QuantConnect.Data.Market; +using QuantConnect.Securities; +using NodaTime; +using QuantConnect.Data; + +namespace QuantConnect.Indicators +{ + /// + /// Base class for indicators that work with two different symbols and calculate an indicator based on them. + /// + /// Type of the data points stored in the rolling windows for each symbol (e.g., double, decimal, etc.) + public abstract class DualSymbolIndicator : BarIndicator, IIndicatorWarmUpPeriodProvider + { + /// + /// Time zone of the target symbol. + /// + private readonly DateTimeZone _targetTimeZone; + + /// + /// Time zone of the reference symbol. + /// + private readonly DateTimeZone _referenceTimeZone; + + /// + /// Stores the previous input data point. + /// + private IBaseDataBar _previousInput; + + /// + /// The resolution of the data (e.g., daily, hourly, etc.). + /// + private Resolution _resolution; + + /// + /// RollingWindow to store the data points of the target symbol + /// + protected RollingWindow TargetDataPoints { get; } + + /// + /// RollingWindow to store the data points of the reference symbol + /// + protected RollingWindow ReferenceDataPoints { get; } + + /// + /// Symbol of the reference used + /// + protected Symbol ReferenceSymbol { get; } + + /// + /// Symbol of the target used + /// + protected Symbol TargetSymbol { get; } + + /// + /// Indicates if the time zone for the target and reference are different. + /// + protected bool IsTimezoneDifferent { get; } + + /// + /// The most recently computed value of the indicator. + /// + protected decimal IndicatorValue { get; set; } + + /// + /// Required period, in data points, for the indicator to be ready and fully initialized. + /// + public int WarmUpPeriod { get; set; } + + /// + /// Initializes the dual symbol indicator. + /// + /// The constructor accepts a target symbol and a reference symbol. It also initializes + /// the time zones for both symbols and checks if they are different. + /// + /// + /// The name of the indicator. + /// The symbol of the target asset. + /// The symbol of the reference asset. + /// The period (number of data points) over which to calculate the indicator. + protected DualSymbolIndicator(string name, Symbol targetSymbol, Symbol referenceSymbol, int period) : base(name) + { + TargetDataPoints = new RollingWindow(period); + ReferenceDataPoints = new RollingWindow(period); + TargetSymbol = targetSymbol; + ReferenceSymbol = referenceSymbol; + + var dataFolder = MarketHoursDatabase.FromDataFolder(); + _targetTimeZone = dataFolder.GetExchangeHours(TargetSymbol.ID.Market, TargetSymbol, TargetSymbol.ID.SecurityType).TimeZone; + _referenceTimeZone = dataFolder.GetExchangeHours(ReferenceSymbol.ID.Market, ReferenceSymbol, ReferenceSymbol.ID.SecurityType).TimeZone; + IsTimezoneDifferent = _targetTimeZone != _referenceTimeZone; + } + + /// + /// Checks and computes the indicator if the input data matches. + /// This method ensures the input data points are from matching time periods and different symbols. + /// + /// The input data point (e.g., TradeBar for a symbol). + /// The most recently computed value of the indicator. + protected override decimal ComputeNextValue(IBaseDataBar input) + { + if (_previousInput == null) + { + _previousInput = input; + _resolution = GetResolution(input); + return decimal.Zero; + } + + var isMatchingTime = CompareEndTimes(input.EndTime, _previousInput.EndTime); + + if (input.Symbol != _previousInput.Symbol && isMatchingTime) + { + AddDataPoint(input); + AddDataPoint(_previousInput); + ComputeIndicator(); + } + _previousInput = input; + return IndicatorValue; + } + + /// + /// Performs the specific computation for the indicator. + /// + protected abstract void ComputeIndicator(); + + /// + /// Determines the resolution of the input data based on the time difference between its start and end times. + /// Returns if the difference exceeds 1 hour; otherwise, calculates a higher equivalent resolution. + /// + private Resolution GetResolution(IBaseData input) + { + var timeDifference = input.EndTime - input.Time; + return timeDifference.TotalHours > 1 ? Resolution.Daily : timeDifference.ToHigherResolutionEquivalent(false); + } + + /// + /// Truncates the given DateTime based on the specified resolution (Daily, Hourly, Minute, or Second). + /// + /// The DateTime to truncate. + /// A DateTime truncated to the specified resolution. + private DateTime AdjustDateToResolution(DateTime date) + { + switch (_resolution) + { + case Resolution.Daily: + return date.Date; + case Resolution.Hour: + return date.Date.AddHours(date.Hour); + case Resolution.Minute: + return date.Date.AddHours(date.Hour).AddMinutes(date.Minute); + case Resolution.Second: + return date; + default: + return date; + } + } + + /// + /// Compares the end times of two data points to check if they are in the same time period. + /// Adjusts for time zone differences if necessary. + /// + /// The end time of the current data point. + /// The end time of the previous data point. + /// True if the end times match after considering time zones and resolution. + private bool CompareEndTimes(DateTime currentEndTime, DateTime previousEndTime) + { + var previousSymbolIsTarget = _previousInput.Symbol == TargetSymbol; + if (IsTimezoneDifferent) + { + currentEndTime = currentEndTime.ConvertToUtc(previousSymbolIsTarget ? _referenceTimeZone : _targetTimeZone); + previousEndTime = previousEndTime.ConvertToUtc(previousSymbolIsTarget ? _targetTimeZone : _referenceTimeZone); + } + return AdjustDateToResolution(currentEndTime) == AdjustDateToResolution(previousEndTime); + } + + /// + /// Adds the closing price to the corresponding symbol's data set (target or reference). + /// This method stores the data points for each symbol and performs specific calculations + /// based on the symbol. For instance, it computes returns in the case of the Beta indicator. + /// + /// The input value for this symbol + /// Thrown if the input symbol does not match either the target or reference symbol. + protected abstract void AddDataPoint(IBaseDataBar input); + + /// + /// Resets this indicator to its initial state + /// + public override void Reset() + { + _previousInput = null; + IndicatorValue = 0; + TargetDataPoints.Reset(); + ReferenceDataPoints.Reset(); + base.Reset(); + } + } +} diff --git a/Tests/Indicators/AlphaIndicatorTests.cs b/Tests/Indicators/AlphaIndicatorTests.cs index 0bf9b9581d95..a4fe873e5316 100644 --- a/Tests/Indicators/AlphaIndicatorTests.cs +++ b/Tests/Indicators/AlphaIndicatorTests.cs @@ -393,5 +393,27 @@ public void NullRiskFreeRate() } } + + [Test] + public override void TracksPreviousState() + { + var period = 5; + var indicator = new Alpha(Symbols.AAPL, Symbols.SPX, period); + var previousValue = indicator.Current.Value; + + // Update the indicator and verify the previous values + for (var i = 1; i < 2 * period; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 1000 + i * 10, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 1000 + (i * 15), Time = startTime, EndTime = endTime }); + // Verify the previous value matches the indicator's previous value + Assert.AreEqual(previousValue, indicator.Previous.Value); + + // Update previousValue to the current value for the next iteration + previousValue = indicator.Current.Value; + } + } } } diff --git a/Tests/Indicators/BetaIndicatorTests.cs b/Tests/Indicators/BetaIndicatorTests.cs index 80d1cf804241..399720695213 100644 --- a/Tests/Indicators/BetaIndicatorTests.cs +++ b/Tests/Indicators/BetaIndicatorTests.cs @@ -258,5 +258,27 @@ public void BetaWithDifferentTimeZones() } Assert.AreEqual(1, (double)indicator.Current.Value); } + + [Test] + public override void TracksPreviousState() + { + var period = 5; + var indicator = new Beta(Symbols.SPY, Symbols.AAPL, period); + var previousValue = indicator.Current.Value; + + // Update the indicator and verify the previous values + for (var i = 1; i < 2 * period; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 1000 + i * 10, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 1000 + (i * 15), Time = startTime, EndTime = endTime }); + // Verify the previous value matches the indicator's previous value + Assert.AreEqual(previousValue, indicator.Previous.Value); + + // Update previousValue to the current value for the next iteration + previousValue = indicator.Current.Value; + } + } } } diff --git a/Tests/Indicators/CommonIndicatorTests.cs b/Tests/Indicators/CommonIndicatorTests.cs index dc98e8788adf..7ae094b2ddab 100644 --- a/Tests/Indicators/CommonIndicatorTests.cs +++ b/Tests/Indicators/CommonIndicatorTests.cs @@ -92,7 +92,7 @@ public virtual void TimeMovesForward() var input = GetInput(startDate, i); indicator.Update(input); } - + Assert.AreEqual(1, indicator.Samples); } @@ -142,6 +142,28 @@ indicator is BarIndicator || } } + [Test] + public virtual void TracksPreviousState() + { + var indicator = CreateIndicator(); + var period = (indicator as IIndicatorWarmUpPeriodProvider)?.WarmUpPeriod; + + var startDate = new DateTime(2024, 1, 1); + var previousValue = indicator.Current.Value; + + // Update the indicator and verify the previous values + for (var i = 0; i < 2 * period; i++) + { + indicator.Update(GetInput(startDate, i)); + + // Verify the previous value matches the indicator's previous value + Assert.AreEqual(previousValue, indicator.Previous.Value); + + // Update previousValue to the current value for the next iteration + previousValue = indicator.Current.Value; + } + } + [Test] public virtual void WorksWithLowValues() { diff --git a/Tests/Indicators/CorrelationPearsonTests.cs b/Tests/Indicators/CorrelationPearsonTests.cs index a82bb9a4443a..1ab579f9767d 100644 --- a/Tests/Indicators/CorrelationPearsonTests.cs +++ b/Tests/Indicators/CorrelationPearsonTests.cs @@ -24,25 +24,25 @@ namespace QuantConnect.Tests.Indicators { [TestFixture, Parallelizable(ParallelScope.Fixtures)] public class CorrelationPearsonTests : CommonIndicatorTests - { + { protected override string TestFileName => "spy_qqq_corr.csv"; - + private DateTime _reference = new DateTime(2020, 1, 1); protected CorrelationType _correlationType { get; set; } = CorrelationType.Pearson; - protected override string TestColumnName => (_correlationType==CorrelationType.Pearson)?"Correlation_Pearson":"Correlation_Spearman"; + protected override string TestColumnName => (_correlationType == CorrelationType.Pearson) ? "Correlation_Pearson" : "Correlation_Spearman"; protected override IndicatorBase CreateIndicator() { - #pragma warning disable CS0618 - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.SPY, "QQQ RIWIV7K5Z9LX", 252, _correlationType); - #pragma warning restore CS0618 +#pragma warning disable CS0618 + var indicator = new Correlation("testCorrelationIndicator", Symbols.SPY, "QQQ RIWIV7K5Z9LX", 252, _correlationType); +#pragma warning restore CS0618 return indicator; } [Test] public override void TimeMovesForward() { - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); + var indicator = new Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); for (var i = 10; i > 0; i--) { @@ -56,7 +56,7 @@ public override void TimeMovesForward() [Test] public override void WarmsUpProperly() { - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); + var indicator = new Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); var period = (indicator as IIndicatorWarmUpPeriodProvider)?.WarmUpPeriod; if (!period.HasValue) @@ -70,14 +70,14 @@ public override void WarmsUpProperly() indicator.Update(new TradeBar() { Symbol = Symbols.IBM, Low = 1, High = 2, Volume = 100, Close = 500, Time = _reference.AddDays(1 + i) }); indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 500, Time = _reference.AddDays(1 + i) }); } - - Assert.AreEqual(2*period.Value, indicator.Samples); + + Assert.AreEqual(2 * period.Value, indicator.Samples); } [Test] public override void AcceptsRenkoBarsAsInput() { - var indicator = CreateIndicator(); + var indicator = new Correlation(Symbols.SPY, "QQQ RIWIV7K5Z9LX", 70, _correlationType); var firstRenkoConsolidator = new RenkoConsolidator(10m); var secondRenkoConsolidator = new RenkoConsolidator(10m); firstRenkoConsolidator.DataConsolidated += (sender, renkoBar) => @@ -153,7 +153,7 @@ public override void AcceptsVolumeRenkoBarsAsInput() [Test] public void AcceptsQuoteBarsAsInput() { - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); + var indicator = new Correlation("testCorrelationIndicator", Symbols.IBM, Symbols.SPY, 5, _correlationType); for (var i = 10; i > 0; i--) { @@ -167,12 +167,14 @@ public void AcceptsQuoteBarsAsInput() [Test] public void EqualCorrelationValue() { - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.AAPL, Symbols.SPX, 3, _correlationType); + var indicator = new Correlation("testCorrelationIndicator", Symbols.AAPL, Symbols.SPX, 3, _correlationType); - for (int i = 0 ; i < 3 ; i++) + for (int i = 0; i < 3; i++) { - indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = i + 1 ,Time = _reference.AddDays(1 + i) }); - indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = _reference.AddDays(1 + i) }); + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = startTime, EndTime = endTime }); } Assert.AreEqual(1, (double)indicator.Current.Value); @@ -181,15 +183,32 @@ public void EqualCorrelationValue() [Test] public void NotEqualCorrelationValue() { - var indicator = new QuantConnect.Indicators.Correlation("testCorrelationIndicator", Symbols.AAPL, Symbols.SPX, 3, _correlationType); + var indicator = new Correlation("testCorrelationIndicator", Symbols.AAPL, Symbols.SPX, 3, _correlationType); for (int i = 0; i < 3; i++) { - indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = _reference.AddDays(1 + i) }); - indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = i + 2, Time = _reference.AddDays(1 + i) }); + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = i + 2, Time = startTime, EndTime = endTime }); } Assert.AreNotEqual(0, (double)indicator.Current.Value); } + + [Test] + public void CorrelationWithDifferentTimeZones() + { + var indicator = new Correlation(Symbols.SPY, Symbols.BTCUSD, 3); + + for (int i = 0; i < 10; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.BTCUSD, Low = 1, High = 2, Volume = 100, Close = i + 1, Time = startTime, EndTime = endTime }); + } + Assert.AreEqual(1, (double)indicator.Current.Value); + } } }