Skip to content

Commit

Permalink
Fix bug in beta calculation
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
JosueNina committed Dec 13, 2024
1 parent 8febb9a commit 1d09f84
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 25 deletions.
62 changes: 46 additions & 16 deletions Indicators/Beta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider
/// </summary>
public int WarmUpPeriod { get; private set; }

private IBaseDataBar _previousInput;

/// <summary>
/// Gets a flag indicating when the indicator is ready and fully initialized
/// </summary>
Expand Down Expand Up @@ -142,30 +144,58 @@ public Beta(string name, int period, Symbol targetSymbol, Symbol referenceSymbol
/// <returns>The beta value of the target used in relation with the reference</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="input">The input value for this symbol</param>
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));
}
}
}

/// <summary>
/// Computes the returns with the new given data point and the last given data point
/// </summary>
Expand All @@ -174,7 +204,7 @@ protected override decimal ComputeNextValue(IBaseDataBar input)
/// <returns>The returns with the new given data point</returns>
private static double GetNewReturn(RollingWindow<decimal> rollingWindow)
{
return (double) ((rollingWindow[0].SafeDivision(rollingWindow[1]) - 1));
return (double)((rollingWindow[0].SafeDivision(rollingWindow[1]) - 1));
}

/// <summary>
Expand All @@ -189,7 +219,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);
}

/// <summary>
Expand Down
64 changes: 55 additions & 9 deletions Tests/Indicators/BetaIndicatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,16 +35,16 @@ public class BetaIndicatorTests : CommonIndicatorTests<IBaseDataBar>

protected override IndicatorBase<IBaseDataBar> 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--)
{
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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]
Expand All @@ -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<TradeBar>()
{
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<double>() { 10, 15, 90, 105 };
var closeSPX = new List<double>() { 35, 80, 37, 302 };
var priceChangesAAPL = new List<double>();
var priceChangesSPX = new List<double>();
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);
}
}
}

0 comments on commit 1d09f84

Please sign in to comment.