Skip to content

Commit

Permalink
Fix bug in beta value computation (#8466)
Browse files Browse the repository at this point in the history
* 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

* Address review comments

* Add time zone handling and resolution-based truncation

* Fix regression test for Beta indicator

* Handle resolution for beta indicator

- Remove effectiveResolution and Beta constructor parameter for
  resolution.
- Streamlined resolution handling logic for Beta indicator.
- Fixed issues with regression test for Beta.

* Fix issue with period and WarmUpPeriod

* Update unit tests for Alpha indicator

* Fixing minor issues

* Add a variable to track if the previous symbol is the target

* Add regression test for Beta calculation between BTCUSD and SPY

* Add an extra period if the TZ are different
  • Loading branch information
JosueNina authored Dec 20, 2024
1 parent d4fd936 commit 3b559a5
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 48 deletions.
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

0 comments on commit 3b559a5

Please sign in to comment.