Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug in beta value computation #8466

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 51 additions & 18 deletions Indicators/Beta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider
/// </summary>
private readonly Symbol _targetSymbol;

/// <summary>
/// Stores the previous input data point.
/// </summary>
private IBaseDataBar _previousInput;

/// <summary>
/// RollingWindow of returns of the target symbol in the given period
/// </summary>
Expand All @@ -72,7 +77,7 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider
/// <summary>
/// Gets a flag indicating when the indicator is ready and fully initialized
/// </summary>
public override bool IsReady => _targetDataPoints.Samples >= WarmUpPeriod && _referenceDataPoints.Samples >= WarmUpPeriod;
public override bool IsReady => _targetReturns.Samples >= WarmUpPeriod && _referenceReturns.Samples >= WarmUpPeriod;

/// <summary>
/// Creates a new Beta indicator with the specified name, target, reference,
Expand All @@ -91,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;

Expand Down Expand Up @@ -142,30 +147,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)
if (input.Symbol != _targetSymbol && input.Symbol != _referenceSymbol)
{
_referenceDataPoints.Add(input.Close);
throw new ArgumentException($"The given symbol {input.Symbol} was not {_targetSymbol} or {_referenceSymbol} symbol");
}
else

if (_previousInput == null)
{
throw new ArgumentException("The given symbol was not target or reference symbol");
_previousInput = input;
return decimal.Zero;
}

if (_targetDataPoints.Samples == _referenceDataPoints.Samples && _referenceDataPoints.Count > 1)
// Process data if symbol has changed and timestamps match
if (input.Symbol != _previousInput.Symbol && input.EndTime == _previousInput.EndTime)
{
_targetReturns.Add(GetNewReturn(_targetDataPoints));
_referenceReturns.Add(GetNewReturn(_referenceDataPoints));

ComputeBeta();
AddDataPoint(input);
AddDataPoint(_previousInput);

// Compute beta when both have at least "period" data points
if (IsReady)
{
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 +207,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 +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);
}

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