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

151 changes: 151 additions & 0 deletions Algorithm.CSharp/AddBetaIndicatorNewAssetsRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 System.Collections.Generic;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using QuantConnect.Brokerages;


namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression test to explain how Beta indicator works
/// </summary>
public class AddBetaIndicatorNewAssetsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private Beta _beta;
private SimpleMovingAverage _sma;
private decimal _lastSMAValue;

public override void Initialize()
{
SetStartDate(2015, 05, 08);
SetEndDate(2017, 06, 15);
SetCash(10000);

AddCrypto("BTCUSD", Resolution.Daily);
AddEquity("SPY", Resolution.Daily);

EnableAutomaticIndicatorWarmUp = true;
_beta = B("BTCUSD", "SPY", 3, Resolution.Daily);
_sma = SMA("SPY", 3, Resolution.Daily);
_lastSMAValue = 0;

if (!_beta.IsReady)
{
throw new RegressionTestException("Beta indicator was expected to be ready");
}
}

public override void OnData(Slice slice)
{
var price = Securities["BTCUSD"].Price;

if (!Portfolio.Invested)
{
var quantityToBuy = (int)(Portfolio.Cash * 0.05m / price);
Buy("BTCUSD", quantityToBuy);
}

if (Math.Abs(_beta.Current.Value) > 2)
{
Liquidate("BTCUSD");
Log("Liquidated BTCUSD due to high Beta");
}

Log($"Beta between BTCUSD and SPY is: {_beta.Current.Value}");
}

public override void OnOrderEvent(OrderEvent orderEvent)
{
var order = Transactions.GetOrderById(orderEvent.OrderId);
var goUpwards = _lastSMAValue < _sma.Current.Value;
_lastSMAValue = _sma.Current.Value;

if (order.Status == OrderStatus.Filled)
{
if (order.Type == OrderType.Limit && Math.Abs(_beta.Current.Value - 1) < 0.2m && goUpwards)
{
Transactions.CancelOpenOrders(order.Symbol);
}
}

if (order.Status == OrderStatus.Canceled)
{
Log(orderEvent.ToString());
}
}

public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public virtual List<Language> Languages { get; } = new() { Language.CSharp };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 5798;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 77;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "436"},
{"Average Win", "0.28%"},
{"Average Loss", "-0.01%"},
{"Compounding Annual Return", "1.926%"},
{"Drawdown", "1.000%"},
{"Expectancy", "1.650"},
{"Start Equity", "10000.00"},
{"End Equity", "10411.11"},
{"Net Profit", "4.111%"},
{"Sharpe Ratio", "0.332"},
{"Sortino Ratio", "0.313"},
{"Probabilistic Sharpe Ratio", "74.084%"},
{"Loss Rate", "90%"},
{"Win Rate", "10%"},
{"Profit-Loss Ratio", "25.26"},
{"Alpha", "0.003"},
{"Beta", "0.001"},
{"Annual Standard Deviation", "0.01"},
{"Annual Variance", "0"},
{"Information Ratio", "-0.495"},
{"Tracking Error", "0.111"},
{"Treynor Ratio", "2.716"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$87000.00"},
{"Lowest Capacity Asset", "BTCUSD 2XR"},
{"Portfolio Turnover", "2.22%"},
{"OrderListHash", "4fcffc45d82203bb6ded8a0e86070b4f"}
};
}
}
4 changes: 2 additions & 2 deletions Algorithm.CSharp/AddBetaIndicatorRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public override void Initialize()

if (!_beta.IsReady)
{
throw new RegressionTestException("_beta indicator was expected to be ready");
throw new RegressionTestException("Beta indicator was expected to be ready");
}
}

Expand All @@ -60,7 +60,7 @@ public override void OnData(Slice slice)
LimitOrder("IBM", 10, price * 0.1m);
StopMarketOrder("IBM", 10, price / 0.1m);
}

if (_beta.Current.Value < 0m || _beta.Current.Value > 2.80m)
{
throw new RegressionTestException($"_beta value was expected to be between 0 and 2.80 but was {_beta.Current.Value}");
Expand Down
132 changes: 112 additions & 20 deletions Indicators/Beta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
using System;
using QuantConnect.Data.Market;
using MathNet.Numerics.Statistics;
using QuantConnect.Securities;
using NodaTime;

namespace QuantConnect.Indicators
{
Expand Down Expand Up @@ -49,6 +51,36 @@ public class Beta : BarIndicator, IIndicatorWarmUpPeriodProvider
/// </summary>
private readonly Symbol _targetSymbol;

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

/// <summary>
/// Indicates whether the previous symbol is the target symbol.
/// </summary>
private bool _previousSymbolIsTarget;

/// <summary>
/// Indicates if the time zone for the target and reference are different.
/// </summary>
private bool _isTimezoneDifferent;

/// <summary>
/// Time zone of the target symbol.
/// </summary>
private DateTimeZone _targetTimeZone;

/// <summary>
/// Time zone of the reference symbol.
/// </summary>
private DateTimeZone _referenceTimeZone;

/// <summary>
/// The resolution of the data (e.g., daily, hourly, etc.).
/// </summary>
private Resolution _resolution;

/// <summary>
/// RollingWindow of returns of the target symbol in the given period
/// </summary>
Expand All @@ -72,7 +104,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.IsReady && _referenceReturns.IsReady;

/// <summary>
/// Creates a new Beta indicator with the specified name, target, reference,
Expand All @@ -88,10 +120,8 @@ public Beta(string name, Symbol targetSymbol, Symbol referenceSymbol, int period
// 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}");
throw new ArgumentException($"Period parameter for Beta indicator must be greater than 2 but was {period}.");
}

WarmUpPeriod = period + 1;
_referenceSymbol = referenceSymbol;
_targetSymbol = targetSymbol;

Expand All @@ -101,6 +131,11 @@ public Beta(string name, Symbol targetSymbol, Symbol referenceSymbol, int period
_targetReturns = new RollingWindow<double>(period);
_referenceReturns = new RollingWindow<double>(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);
}

/// <summary>
Expand Down Expand Up @@ -142,30 +177,87 @@ 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 (_previousInput == null)
{
_referenceDataPoints.Add(input.Close);
_previousInput = input;
_previousSymbolIsTarget = input.Symbol == _targetSymbol;
var timeDifference = input.EndTime - input.Time;
_resolution = timeDifference.TotalHours > 1 ? Resolution.Daily : timeDifference.ToHigherResolutionEquivalent(false);
return decimal.Zero;
}
else

var inputEndTime = input.EndTime;
var previousInputEndTime = _previousInput.EndTime;

if (_isTimezoneDifferent)
{
throw new ArgumentException("The given symbol was not target or reference symbol");
inputEndTime = inputEndTime.ConvertToUtc(_previousSymbolIsTarget ? _referenceTimeZone : _targetTimeZone);
previousInputEndTime = previousInputEndTime.ConvertToUtc(_previousSymbolIsTarget ? _targetTimeZone : _referenceTimeZone);
}

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

AddDataPoint(input);
AddDataPoint(_previousInput);
ComputeBeta();
}
_previousInput = input;
_previousSymbolIsTarget = input.Symbol == _targetSymbol;
return _beta;
}

/// <summary>
/// Truncates the given DateTime based on the specified resolution (Daily, Hourly, Minute, or Second).
/// </summary>
/// <param name="date">The DateTime to truncate.</param>
/// <returns>A DateTime truncated to the specified resolution.</returns>
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;
}
}

/// <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));
}
}
else
{
throw new ArgumentException($"The given symbol {input.Symbol} was not {_targetSymbol} or {_referenceSymbol} symbol");
}
}

/// <summary>
/// Computes the returns with the new given data point and the last given data point
/// </summary>
Expand All @@ -174,7 +266,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,17 +281,17 @@ 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>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
_previousInput = null;
_targetDataPoints.Reset();
_referenceDataPoints.Reset();

_targetReturns.Reset();
_referenceReturns.Reset();
_beta = 0;
Expand Down
Loading