From bbb4ec01e1b2b72d5e6980475ad0b6582fcbdf56 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 13 Dec 2024 16:34:08 -0500 Subject: [PATCH 1/2] Fix bug in beta calculation - Beta is fill-forwarded - A correct pair considered when they have different symbols and the same date - Processing occurs when there are at least period+1 correct pairs --- Indicators/Beta.cs | 65 +++++++++++++++++++------- Tests/Indicators/BetaIndicatorTests.cs | 64 +++++++++++++++++++++---- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/Indicators/Beta.cs b/Indicators/Beta.cs index 3484c1132f5b..a03697e2cafe 100644 --- a/Indicators/Beta.cs +++ b/Indicators/Beta.cs @@ -49,6 +49,11 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider /// private readonly Symbol _targetSymbol; + /// + /// Stores the previous input data point. + /// + private IBaseDataBar _previousInput; + /// /// RollingWindow of returns of the target symbol in the given period /// @@ -142,30 +147,58 @@ public Beta(string name, int period, Symbol targetSymbol, Symbol referenceSymbol /// The beta value of the target used in relation with the reference protected override decimal ComputeNextValue(IBaseDataBar input) { - var inputSymbol = input.Symbol; - if (inputSymbol == _targetSymbol) - { - _targetDataPoints.Add(input.Close); - } - else if(inputSymbol == _referenceSymbol) - { - _referenceDataPoints.Add(input.Close); - } - else + if (input.Symbol != _targetSymbol && input.Symbol != _referenceSymbol) { throw new ArgumentException("The given symbol was not target or reference symbol"); } - if (_targetDataPoints.Samples == _referenceDataPoints.Samples && _referenceDataPoints.Count > 1) + if (_previousInput == null) { - _targetReturns.Add(GetNewReturn(_targetDataPoints)); - _referenceReturns.Add(GetNewReturn(_referenceDataPoints)); + _previousInput = input; + return decimal.Zero; + } - ComputeBeta(); + // Process data if symbol has changed and timestamps match + if (input.Symbol.Value != _previousInput.Symbol.Value && input.EndTime == _previousInput.EndTime) + { + AddDataPoint(input); + AddDataPoint(_previousInput); + + // Compute beta when both have at least "period" data points + if ((_targetReturns.Count >= WarmUpPeriod - 1) && (_referenceReturns.Count >= WarmUpPeriod - 1)) + { + ComputeBeta(); + } } + _previousInput = input; return _beta; } + /// + /// 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) + { + if (input.Symbol == _targetSymbol) + { + _targetDataPoints.Add(input.Close); + if (_targetDataPoints.Count > 1) + { + _targetReturns.Add(GetNewReturn(_targetDataPoints)); + } + } + else if (input.Symbol == _referenceSymbol) + { + _referenceDataPoints.Add(input.Close); + if (_referenceDataPoints.Count > 1) + { + _referenceReturns.Add(GetNewReturn(_referenceDataPoints)); + } + } + } + /// /// Computes the returns with the new given data point and the last given data point /// @@ -174,7 +207,7 @@ protected override decimal ComputeNextValue(IBaseDataBar input) /// The returns with the new given data point private static double GetNewReturn(RollingWindow rollingWindow) { - return (double) ((rollingWindow[0].SafeDivision(rollingWindow[1]) - 1)); + return (double)((rollingWindow[0].SafeDivision(rollingWindow[1]) - 1)); } /// @@ -189,7 +222,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); + _beta = (decimal)(covariance / variance); } /// diff --git a/Tests/Indicators/BetaIndicatorTests.cs b/Tests/Indicators/BetaIndicatorTests.cs index 543a7434b6cd..499a71c42638 100644 --- a/Tests/Indicators/BetaIndicatorTests.cs +++ b/Tests/Indicators/BetaIndicatorTests.cs @@ -18,6 +18,8 @@ using QuantConnect.Data.Market; using QuantConnect.Indicators; using System; +using System.Collections.Generic; +using MathNet.Numerics.Statistics; using static QuantConnect.Tests.Indicators.TestHelper; namespace QuantConnect.Tests.Indicators @@ -33,16 +35,16 @@ public class BetaIndicatorTests : CommonIndicatorTests protected override IndicatorBase CreateIndicator() { - #pragma warning disable CS0618 +#pragma warning disable CS0618 var indicator = new Beta("testBetaIndicator", "AMZN 2T", "SPX 2T", 5); - #pragma warning restore CS0618 +#pragma warning restore CS0618 return indicator; } [Test] public override void TimeMovesForward() { - var indicator = new Beta("testBetaIndicator", Symbols.IBM, Symbols.SPY, 5); + var indicator = new Beta("testBetaIndicator", Symbols.IBM, Symbols.SPY, 5); for (var i = 10; i > 0; i--) { @@ -71,15 +73,15 @@ public override void WarmsUpProperly() 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 WorksWithLowValues() { - #pragma warning disable CS0618 +#pragma warning disable CS0618 Symbol = "SPX 2T"; - #pragma warning restore CS0618 +#pragma warning restore CS0618 base.WorksWithLowValues(); } @@ -173,13 +175,13 @@ public void EqualBetaValue() { var indicator = new Beta("testBetaIndicator", Symbols.AAPL, Symbols.SPX, 5); - 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.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) }); } - Assert.AreEqual(1, (double) indicator.Current.Value, 0.0001); + Assert.AreEqual(0, (double)indicator.Current.Value, 0.0001); } [Test] @@ -195,5 +197,49 @@ public void NotEqualBetaValue() Assert.AreNotEqual(1, (double)indicator.Current.Value); } + + [Test] + public void ValidateBetaCalculation() + { + var beta = new Beta(Symbols.AAPL, Symbols.SPX, 3); + + var values = new List() + { + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 10, Time = _reference.AddDays(1) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 35, Time = _reference.AddDays(1) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 2, Time = _reference.AddDays(2) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 2, Time = _reference.AddDays(2) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 15, Time = _reference.AddDays(3) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 80, Time = _reference.AddDays(3) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 4, Time = _reference.AddDays(4) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 4, Time = _reference.AddDays(4) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 37, Time = _reference.AddDays(5) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 90, Time = _reference.AddDays(5) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 105, Time = _reference.AddDays(6) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 302, Time = _reference.AddDays(6) }, + }; + + // Calculating beta manually using the formula: Beta = Covariance(AAPL, SPX) / Variance(SPX) + var closeAAPL = new List() { 10, 15, 90, 105 }; + var closeSPX = new List() { 35, 80, 37, 302 }; + var priceChangesAAPL = new List(); + var priceChangesSPX = new List(); + for (int i = 1; i < 4; i++) + { + priceChangesAAPL.Add((closeAAPL[i] - closeAAPL[i - 1]) / closeAAPL[i - 1]); + priceChangesSPX.Add((closeSPX[i] - closeSPX[i - 1]) / closeSPX[i - 1]); + } + var variance = priceChangesSPX.Variance(); + var covariance = priceChangesAAPL.Covariance(priceChangesSPX); + var expectedBeta = (decimal)(covariance / variance); + + // Calculating beta using the indicator + for (int i = 0; i < values.Count; i++) + { + beta.Update(values[i]); + } + + Assert.AreEqual(expectedBeta, beta.Current.Value); + } } } From 51bc6f432f5ebf3d5cb30553aa0db98688fbe54b Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 13 Dec 2024 17:42:13 -0500 Subject: [PATCH 2/2] Address review comments --- Indicators/Beta.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Indicators/Beta.cs b/Indicators/Beta.cs index a03697e2cafe..acb6bacd505d 100644 --- a/Indicators/Beta.cs +++ b/Indicators/Beta.cs @@ -77,7 +77,7 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider /// /// 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 => _targetReturns.Samples >= WarmUpPeriod && _referenceReturns.Samples >= WarmUpPeriod; /// /// Creates a new Beta indicator with the specified name, target, reference, @@ -96,7 +96,7 @@ public Beta(string name, Symbol targetSymbol, Symbol referenceSymbol, int period throw new ArgumentException($"Period parameter for Beta indicator must be greater than 2 but was {period}"); } - WarmUpPeriod = period + 1; + WarmUpPeriod = period; _referenceSymbol = referenceSymbol; _targetSymbol = targetSymbol; @@ -149,7 +149,7 @@ protected override decimal ComputeNextValue(IBaseDataBar input) { if (input.Symbol != _targetSymbol && input.Symbol != _referenceSymbol) { - 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"); } if (_previousInput == null) @@ -159,13 +159,13 @@ protected override decimal ComputeNextValue(IBaseDataBar input) } // Process data if symbol has changed and timestamps match - if (input.Symbol.Value != _previousInput.Symbol.Value && input.EndTime == _previousInput.EndTime) + if (input.Symbol != _previousInput.Symbol && input.EndTime == _previousInput.EndTime) { AddDataPoint(input); AddDataPoint(_previousInput); // Compute beta when both have at least "period" data points - if ((_targetReturns.Count >= WarmUpPeriod - 1) && (_referenceReturns.Count >= WarmUpPeriod - 1)) + if (IsReady) { ComputeBeta(); }