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);
+ }
}
}