From f3ea223f2aafda12f978dff851283b10c48ea230 Mon Sep 17 00:00:00 2001
From: JosueNina <36119850+JosueNina@users.noreply.github.com>
Date: Thu, 2 Jan 2025 08:42:01 -0500
Subject: [PATCH] Refactor Correlation and beta indicators (#8485)
* WIP: Refactor Correlation Indicator
* Simplified comparison logic and improved abstraction
* Refactor Correlation and Beta indicators
- Created a base class to handle indicators with dual-symbol
  functionality.
- Refactored the Beta and Correlation indicators to inherit from the new
  base class.
- Updated unit tests.
- Added a new regression test to validate the latest computed value.
* Addressed review comments
* Update regression test
* Add new unit test to CommonIndicatorTests
* Addressed new review comments
---
 ...ionLastComputedValueRegressionAlgorithm.cs | 136 +++++++++++
 Indicators/Beta.cs                            | 171 ++------------
 Indicators/Correlation.cs                     | 107 ++-------
 Indicators/DualSymbolIndicator.cs             | 212 ++++++++++++++++++
 Tests/Indicators/AlphaIndicatorTests.cs       |  22 ++
 Tests/Indicators/BetaIndicatorTests.cs        |  22 ++
 Tests/Indicators/CommonIndicatorTests.cs      |  24 +-
 Tests/Indicators/CorrelationPearsonTests.cs   |  57 +++--
 8 files changed, 488 insertions(+), 263 deletions(-)
 create mode 100644 Algorithm.CSharp/RegressionTests/CorrelationLastComputedValueRegressionAlgorithm.cs
 create mode 100644 Indicators/DualSymbolIndicator.cs
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);
+        }
     }
 }