mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 12:08:05 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,127 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BetaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BetaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BetaIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.AssetSource);
|
||||
Assert.Equal(SourceType.Close, indicator.MarketSource);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Beta Coefficient", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(2, BetaIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(2, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 14 };
|
||||
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Beta", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_Initialize_CreatesInternalBeta()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Beta", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data - need enough bars for warmup
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double beta = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(beta));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add initial bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(11, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var assetSources = new[]
|
||||
{
|
||||
SourceType.Open,
|
||||
SourceType.High,
|
||||
SourceType.Low,
|
||||
SourceType.Close,
|
||||
};
|
||||
|
||||
foreach (var source in assetSources)
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5, AssetSource = source, MarketSource = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"AssetSource {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class BetaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Asset Source", sortIndex: 2)]
|
||||
public SourceType AssetSource { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Market Source", sortIndex: 3)]
|
||||
public SourceType MarketSource { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Beta _beta = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _assetSelector = null!;
|
||||
private Func<IHistoryItem, double> _marketSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Beta({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/beta/Beta.Quantower.cs";
|
||||
|
||||
public BetaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Beta Coefficient";
|
||||
Description = "Measures the volatility of an asset in relation to the overall market.";
|
||||
|
||||
_series = new LineSeries(name: "Beta", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_beta = new Beta(Period);
|
||||
_assetSelector = AssetSource.GetPriceSelector();
|
||||
_marketSelector = MarketSource.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double assetVal = _assetSelector(item);
|
||||
double marketVal = _marketSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var assetInput = new TValue(time, assetVal);
|
||||
var marketInput = new TValue(time, marketVal);
|
||||
|
||||
TValue result = _beta.Update(assetInput, marketInput, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _beta.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BetaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Beta(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Beta(-1));
|
||||
|
||||
// Valid period should not throw
|
||||
var beta = new Beta(1);
|
||||
Assert.NotNull(beta);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ThrowsOnSingleInput()
|
||||
{
|
||||
var beta = new Beta(10);
|
||||
Assert.Throws<NotSupportedException>(() => beta.Update(new TValue(DateTime.UtcNow, 100)));
|
||||
Assert.Throws<NotSupportedException>(() => beta.Update(new TSeries()));
|
||||
Assert.Throws<NotSupportedException>(() => beta.Prime([1, 2, 3]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var beta = new Beta(10);
|
||||
|
||||
Assert.Equal(0, beta.Last.Value);
|
||||
Assert.False(beta.IsHot);
|
||||
Assert.Contains("Beta", beta.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(11, beta.WarmupPeriod); // period + 1 for first return
|
||||
|
||||
beta.Update(100, 100);
|
||||
beta.Update(101, 101);
|
||||
Assert.NotEqual(0, beta.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterPeriod()
|
||||
{
|
||||
const int period = 5;
|
||||
var beta = new Beta(period);
|
||||
|
||||
// We need period returns.
|
||||
// 1st update: initializes prev prices. No return.
|
||||
// 2nd update: 1st return.
|
||||
// ...
|
||||
// (period+1)th update: period-th return. Buffer full. IsHot true.
|
||||
|
||||
for (int i = 0; i <= period; i++)
|
||||
{
|
||||
Assert.False(beta.IsHot, $"IsHot should be false at index {i}");
|
||||
beta.Update(100 + i, 100 + i);
|
||||
}
|
||||
|
||||
// Now we have fed period+1 prices -> period returns.
|
||||
Assert.True(beta.IsHot, "IsHot should be true after period+1 updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_KnownBeta()
|
||||
{
|
||||
// Scenario: Asset returns are exactly 2x Market returns.
|
||||
// We need variable market returns to have non-zero variance.
|
||||
|
||||
int period = 10;
|
||||
var beta = new Beta(period);
|
||||
|
||||
double marketPrice = 100;
|
||||
double assetPrice = 100;
|
||||
|
||||
// Initialize
|
||||
beta.Update(assetPrice, marketPrice);
|
||||
|
||||
// Pattern of returns: +1%, -1%, +1%, -1%...
|
||||
// Asset returns: +2%, -2%, +2%, -2%...
|
||||
// This gives Beta = 2.
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double marketReturn = (i % 2 == 0) ? 0.01 : -0.01;
|
||||
double assetReturn = marketReturn * 2.0;
|
||||
|
||||
marketPrice *= (1 + marketReturn);
|
||||
assetPrice *= (1 + assetReturn);
|
||||
|
||||
TValue result = beta.Update(assetPrice, marketPrice);
|
||||
|
||||
if (beta.IsHot)
|
||||
{
|
||||
Assert.Equal(2.0, result.Value, precision: 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var beta = new Beta(5);
|
||||
|
||||
// Initialize
|
||||
beta.Update(100, 100);
|
||||
|
||||
// Add 5 more updates with different ratios to get non-1 beta
|
||||
beta.Update(102, 101); // Asset up 2%, market up 1%
|
||||
beta.Update(104, 102); // Asset up ~2%, market up ~1%
|
||||
beta.Update(108, 103); // Asset up ~4%, market up ~1%
|
||||
beta.Update(112, 104); // Asset up ~4%, market up ~1%
|
||||
beta.Update(116, 105); // Asset up ~4%, market up ~1%
|
||||
|
||||
double valueBefore = beta.Last.Value;
|
||||
|
||||
// Update last value with isNew=false with very different values
|
||||
beta.Update(90, 110, isNew: false); // Drastically different
|
||||
double valueAfter = beta.Last.Value;
|
||||
|
||||
// Value should change since we're updating the last bar
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var beta = new Beta(5);
|
||||
|
||||
// Initialize with 10 updates
|
||||
beta.Update(100, 100);
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
beta.Update(100 + i, 100 + i);
|
||||
}
|
||||
|
||||
double stateAfterTen = beta.Last.Value;
|
||||
|
||||
// Apply 5 corrections with isNew=false
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
beta.Update(200 + i, 200 + i, isNew: false);
|
||||
}
|
||||
|
||||
// Restore to original value
|
||||
beta.Update(109, 109, isNew: false);
|
||||
|
||||
Assert.Equal(stateAfterTen, beta.Last.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var beta = new Beta(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
beta.Update(100 + i * 2, 100 + i); // Different ratios
|
||||
}
|
||||
Assert.True(beta.IsHot);
|
||||
|
||||
beta.Reset();
|
||||
Assert.False(beta.IsHot);
|
||||
|
||||
// Re-initialize and verify it can accept new values
|
||||
// After reset, beta should be able to calculate fresh values
|
||||
beta.Update(100, 100);
|
||||
Assert.False(beta.IsHot); // Not hot yet, needs period+1 updates
|
||||
|
||||
// Feed more updates to reach hot state again
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
beta.Update(100 + i, 100 + i);
|
||||
}
|
||||
Assert.True(beta.IsHot);
|
||||
|
||||
// With equal proportional changes, beta should be 1
|
||||
Assert.Equal(1.0, beta.Last.Value, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_ReturnsFiniteValue()
|
||||
{
|
||||
var beta = new Beta(5);
|
||||
|
||||
// Initialize
|
||||
beta.Update(100, 100);
|
||||
|
||||
// Add some valid values
|
||||
beta.Update(101, 101);
|
||||
beta.Update(102, 102);
|
||||
|
||||
// Add NaN - Beta should handle gracefully
|
||||
var result = beta.Update(double.NaN, double.NaN);
|
||||
|
||||
// Result should be finite (may be 0 or previous value)
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_ReturnsFiniteValue()
|
||||
{
|
||||
var beta = new Beta(5);
|
||||
|
||||
// Initialize
|
||||
beta.Update(100, 100);
|
||||
|
||||
// Add some valid values
|
||||
beta.Update(101, 101);
|
||||
beta.Update(102, 102);
|
||||
|
||||
// Add Infinity - Beta should handle gracefully
|
||||
var result = beta.Update(double.PositiveInfinity, double.PositiveInfinity);
|
||||
|
||||
// Result should be finite (may be 0 or previous value)
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroMarketVariance_ReturnsZero()
|
||||
{
|
||||
// When market returns are constant (zero variance), beta is undefined
|
||||
// The implementation should return 0 in this case
|
||||
var beta = new Beta(5);
|
||||
|
||||
// Initialize
|
||||
beta.Update(100, 100);
|
||||
|
||||
// Same market price (zero returns/variance)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
beta.Update(100 + i, 100); // Asset changes, market constant
|
||||
}
|
||||
|
||||
// Beta should be 0 (or undefined) when market variance is 0
|
||||
Assert.Equal(0, beta.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resync_DoesNotDrift()
|
||||
{
|
||||
// Run for > 1000 updates to trigger Resync
|
||||
var beta = new Beta(10);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
beta.Update(100, 100); // Initialize
|
||||
|
||||
for (int i = 0; i < 1100; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
beta.Update(bar.Close * 1.5, bar.Close); // Asset follows market with beta ~1.5
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(beta.Last.Value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Skender.Stock.Indicators;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class BetaValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public BetaValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Skender()
|
||||
{
|
||||
// Generate Market Data (use existing Data)
|
||||
var marketQuotes = _data.Data;
|
||||
|
||||
// Generate Asset Data correlated to Market
|
||||
// Asset Returns = 1.5 * Market Returns + Noise
|
||||
var assetQuotes = new List<TBar>();
|
||||
double assetPrice = 100;
|
||||
const double targetBeta = 1.5;
|
||||
|
||||
// Use GBM for noise generation (sigma=0.2 gives ~0.0006 per step noise which matches original random noise level)
|
||||
var noiseGbm = new GBM(startPrice: 100, mu: 0, sigma: 0.2, seed: 777);
|
||||
|
||||
assetQuotes.Add(new TBar(marketQuotes[0].Time, assetPrice, assetPrice, assetPrice, assetPrice, 1000));
|
||||
|
||||
for (int i = 1; i < marketQuotes.Count; i++)
|
||||
{
|
||||
double marketReturn = (marketQuotes[i].Value - marketQuotes[i - 1].Value) / marketQuotes[i - 1].Value;
|
||||
|
||||
// Get noise from GBM return
|
||||
var noiseBar = noiseGbm.Next();
|
||||
double noise = (noiseBar.Close - noiseBar.Open) / noiseBar.Open;
|
||||
|
||||
double assetReturn = targetBeta * marketReturn + noise;
|
||||
|
||||
assetPrice *= (1 + assetReturn);
|
||||
assetQuotes.Add(new TBar(marketQuotes[i].Time, assetPrice, assetPrice, assetPrice, assetPrice, 1000));
|
||||
}
|
||||
|
||||
// Skender
|
||||
// Skender expects IEnumerable<Quote>
|
||||
var skenderMarket = marketQuotes.Select(x => new Quote { Date = x.AsDateTime, Close = (decimal)x.Value }).ToList();
|
||||
var skenderAsset = assetQuotes.Select(x => new Quote { Date = x.AsDateTime, Close = (decimal)x.Close }).ToList();
|
||||
|
||||
int period = 20;
|
||||
var skenderBeta = skenderAsset.GetBeta(skenderMarket, period).ToList();
|
||||
|
||||
// QuanTAlib
|
||||
var beta = new Beta(period);
|
||||
var qlBeta = new List<double>();
|
||||
|
||||
for (int i = 0; i < marketQuotes.Count; i++)
|
||||
{
|
||||
var result = beta.Update(assetQuotes[i].Close, marketQuotes[i].Value);
|
||||
qlBeta.Add(result.Value);
|
||||
}
|
||||
|
||||
// Compare
|
||||
// Skip warmup period. Skender Beta needs period returns, so period+1 prices?
|
||||
// Skender results align with input quotes.
|
||||
// First valid value should be at index 'period'.
|
||||
|
||||
// We verify the last 100 values
|
||||
int count = qlBeta.Count;
|
||||
int skip = period + 5; // Safety margin
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double sk = (skenderBeta[i].Beta ?? 0);
|
||||
double ql = qlBeta[i];
|
||||
|
||||
// Skender might return null/0 for warmup.
|
||||
if (Math.Abs(sk) > 1e-10)
|
||||
{
|
||||
Assert.Equal(sk, ql, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using static System.Math;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Beta Coefficient: Measures the volatility of an asset in relation to the overall market.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Beta is calculated as the covariance of the asset's returns and the market's returns,
|
||||
/// divided by the variance of the market's returns.
|
||||
///
|
||||
/// Formula:
|
||||
/// Beta = Cov(Ra, Rm) / Var(Rm)
|
||||
///
|
||||
/// Where:
|
||||
/// Ra = Return of Asset
|
||||
/// Rm = Return of Market
|
||||
///
|
||||
/// This implementation uses the O(1) slope formula for linear regression of Ra vs Rm:
|
||||
/// Beta = (N * Sum(Ra*Rm) - Sum(Ra) * Sum(Rm)) / (N * Sum(Rm^2) - Sum(Rm)^2)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Beta : AbstractBase
|
||||
{
|
||||
private readonly RingBuffer _returnsAsset;
|
||||
private readonly RingBuffer _returnsMarket;
|
||||
|
||||
private double _prevAsset;
|
||||
private double _prevMarket;
|
||||
private double _p_prevAsset;
|
||||
private double _p_prevMarket;
|
||||
private bool _isInitialized;
|
||||
|
||||
private double _sumRa;
|
||||
private double _sumRm;
|
||||
private double _sumRaRm;
|
||||
private double _sumRm2;
|
||||
|
||||
private const double Epsilon = 1e-10;
|
||||
private int _updateCount;
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public override bool IsHot => _returnsAsset.IsFull;
|
||||
|
||||
public Beta(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
||||
}
|
||||
_returnsAsset = new RingBuffer(period);
|
||||
_returnsMarket = new RingBuffer(period);
|
||||
Name = $"Beta({period})";
|
||||
WarmupPeriod = period + 1; // Need 1 extra for first return
|
||||
_isInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Beta indicator with new asset and market prices.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset price (TValue).</param>
|
||||
/// <param name="market">The market price (TValue).</param>
|
||||
/// <param name="isNew">Whether this is a new bar.</param>
|
||||
/// <returns>The calculated Beta value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue asset, TValue market, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
_isInitialized = true;
|
||||
return new TValue(asset.Time, 0);
|
||||
}
|
||||
|
||||
_p_prevAsset = _prevAsset;
|
||||
_p_prevMarket = _prevMarket;
|
||||
|
||||
// Calculate returns with division-by-zero and NaN/Infinity guards
|
||||
double ra, rm;
|
||||
if (Abs(_prevAsset) < Epsilon)
|
||||
{
|
||||
ra = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ra = (asset.Value - _prevAsset) / _prevAsset;
|
||||
if (!double.IsFinite(ra))
|
||||
{
|
||||
ra = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Abs(_prevMarket) < Epsilon)
|
||||
{
|
||||
rm = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
rm = (market.Value - _prevMarket) / _prevMarket;
|
||||
if (!double.IsFinite(rm))
|
||||
{
|
||||
rm = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
|
||||
// Update buffers and sums
|
||||
if (_returnsAsset.IsFull)
|
||||
{
|
||||
double oldRa = _returnsAsset.Oldest;
|
||||
double oldRm = _returnsMarket.Oldest;
|
||||
|
||||
_sumRa -= oldRa;
|
||||
_sumRm -= oldRm;
|
||||
_sumRaRm = FusedMultiplyAdd(-oldRa, oldRm, _sumRaRm);
|
||||
_sumRm2 = FusedMultiplyAdd(-oldRm, oldRm, _sumRm2);
|
||||
}
|
||||
|
||||
_returnsAsset.Add(ra);
|
||||
_returnsMarket.Add(rm);
|
||||
|
||||
_sumRa += ra;
|
||||
_sumRm += rm;
|
||||
_sumRaRm = FusedMultiplyAdd(ra, rm, _sumRaRm);
|
||||
_sumRm2 = FusedMultiplyAdd(rm, rm, _sumRm2);
|
||||
|
||||
_updateCount++;
|
||||
if (_updateCount % ResyncInterval == 0)
|
||||
{
|
||||
Resync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
_isInitialized = true;
|
||||
return new TValue(asset.Time, 0);
|
||||
}
|
||||
|
||||
if (_returnsAsset.Count == 0)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
return new TValue(asset.Time, 0);
|
||||
}
|
||||
|
||||
double oldRa = _returnsAsset.Newest;
|
||||
double oldRm = _returnsMarket.Newest;
|
||||
|
||||
// Calculate new returns with zero-guard for division
|
||||
double newRa, newRm;
|
||||
if (Abs(_p_prevAsset) < Epsilon)
|
||||
{
|
||||
newRa = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newRa = (asset.Value - _p_prevAsset) / _p_prevAsset;
|
||||
if (!double.IsFinite(newRa))
|
||||
{
|
||||
newRa = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Abs(_p_prevMarket) < Epsilon)
|
||||
{
|
||||
newRm = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newRm = (market.Value - _p_prevMarket) / _p_prevMarket;
|
||||
if (!double.IsFinite(newRm))
|
||||
{
|
||||
newRm = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
|
||||
_returnsAsset.UpdateNewest(newRa);
|
||||
_returnsMarket.UpdateNewest(newRm);
|
||||
|
||||
// Use FMA for better precision: _sumRa = _sumRa - oldRa + newRa
|
||||
_sumRa = FusedMultiplyAdd(1.0, newRa, FusedMultiplyAdd(-1.0, oldRa, _sumRa));
|
||||
_sumRm = FusedMultiplyAdd(1.0, newRm, FusedMultiplyAdd(-1.0, oldRm, _sumRm));
|
||||
_sumRaRm = FusedMultiplyAdd(newRa, newRm, FusedMultiplyAdd(-oldRa, oldRm, _sumRaRm));
|
||||
_sumRm2 = FusedMultiplyAdd(newRm, newRm, FusedMultiplyAdd(-oldRm, oldRm, _sumRm2));
|
||||
}
|
||||
|
||||
double beta = 0;
|
||||
int n = _returnsAsset.Count;
|
||||
if (n > 0)
|
||||
{
|
||||
// Use FMA for better numerical stability
|
||||
double denominator = FusedMultiplyAdd(n, _sumRm2, -_sumRm * _sumRm);
|
||||
if (Abs(denominator) > Epsilon)
|
||||
{
|
||||
double numerator = FusedMultiplyAdd(n, _sumRaRm, -_sumRa * _sumRm);
|
||||
beta = numerator / denominator;
|
||||
}
|
||||
}
|
||||
|
||||
Last = new TValue(asset.Time, beta);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(double asset, double market, bool isNew = true)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Update(new TValue(now, asset), new TValue(now, market), isNew);
|
||||
}
|
||||
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
throw new NotSupportedException("Beta requires two inputs (asset and market). Use Update(asset, market).");
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
throw new NotSupportedException("Beta requires two inputs (asset and market). Use Update(asset, market).");
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
throw new NotSupportedException("Beta requires two inputs (asset and market). Use Update(asset, market).");
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_returnsAsset.Clear();
|
||||
_returnsMarket.Clear();
|
||||
_sumRa = 0;
|
||||
_sumRm = 0;
|
||||
_sumRaRm = 0;
|
||||
_sumRm2 = 0;
|
||||
_isInitialized = false;
|
||||
_prevAsset = 0;
|
||||
_prevMarket = 0;
|
||||
_p_prevAsset = 0;
|
||||
_p_prevMarket = 0;
|
||||
_updateCount = 0;
|
||||
}
|
||||
|
||||
private void Resync()
|
||||
{
|
||||
_sumRa = 0;
|
||||
_sumRm = 0;
|
||||
_sumRaRm = 0;
|
||||
_sumRm2 = 0;
|
||||
|
||||
for (int i = 0; i < _returnsAsset.Count; i++)
|
||||
{
|
||||
double ra = _returnsAsset[i];
|
||||
double rm = _returnsMarket[i];
|
||||
|
||||
_sumRa += ra;
|
||||
_sumRm += rm;
|
||||
// Use FMA for better precision in cross-term and squared-term
|
||||
_sumRaRm = FusedMultiplyAdd(ra, rm, _sumRaRm);
|
||||
_sumRm2 = FusedMultiplyAdd(rm, rm, _sumRm2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Beta: Beta Coefficient
|
||||
|
||||
> "Volatility is not risk. It's the price of admission."
|
||||
|
||||
Beta measures the volatility of an asset in relation to the overall market. It's the slope of the regression line between the asset's returns and the market's returns. A beta of 1.0 means the asset moves in lockstep with the market. A beta of 2.0 means the asset is twice as volatile as the market.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The Beta coefficient was born from the Capital Asset Pricing Model (CAPM), developed by William Sharpe, John Lintner, and Jan Mossin in the 1960s. It formalized the distinction between systematic risk (market risk, which cannot be diversified away) and unsystematic risk (specific to the asset). In the pre-computer era, calculating beta was a tedious manual process involving graph paper and rulers. Today, it's a standard metric on every financial dashboard, though often misunderstood as a measure of "risk" rather than "relative volatility."
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
Beta is essentially the ratio of covariance to variance. It answers the question: "For every 1% move in the market, how much does this asset move?"
|
||||
|
||||
The calculation relies on the returns of both the asset and the market, not their prices. This implementation calculates returns on the fly from the input prices (`(Current - Previous) / Previous`).
|
||||
|
||||
To maintain O(1) performance, QuanTAlib uses Welford's online algorithm principles (or equivalent running sums) to update the covariance and variance components incrementally. This avoids iterating over the entire history for every new bar.
|
||||
|
||||
### The Dual-Input Challenge
|
||||
|
||||
Unlike most indicators that consume a single time series, Beta requires two synchronized inputs: the Asset and the Market. This breaks the standard `Update(value)` pattern. QuanTAlib solves this with a specialized `Update(asset, market)` overload. The standard single-input methods throw a `NotSupportedException` to prevent misuse.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
Beta is defined as:
|
||||
|
||||
$$ \beta = \frac{Cov(R_a, R_m)}{Var(R_m)} $$
|
||||
|
||||
Where:
|
||||
|
||||
* $R_a$ is the return of the asset.
|
||||
* $R_m$ is the return of the market.
|
||||
|
||||
In terms of linear regression, Beta is the slope ($b$) of the line $R_a = \alpha + \beta R_m + \epsilon$.
|
||||
|
||||
The O(1) implementation uses running sums of the returns:
|
||||
|
||||
$$ \beta = \frac{N \sum (R_a R_m) - \sum R_a \sum R_m}{N \sum R_m^2 - (\sum R_m)^2} $$
|
||||
|
||||
This formula is mathematically equivalent to the covariance/variance definition but allows for efficient incremental updates.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 15 ns/bar | Single-pass O(1) calculation. |
|
||||
| **Allocations** | 0 | Zero-allocation hot path. |
|
||||
| **Complexity** | O(1) | Constant time update regardless of period. |
|
||||
| **Accuracy** | 9 | Periodic resync prevents floating-point drift. |
|
||||
| **Timeliness** | Lagged | Depends on the lookback period. |
|
||||
| **Overshoot** | N/A | Not an oscillator. |
|
||||
| **Smoothness** | Low | Highly sensitive to outliers in returns. |
|
||||
|
||||
## Validation
|
||||
|
||||
Validated against Skender.Stock.Indicators.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Reference implementation. |
|
||||
| **TA-Lib** | ✅ | Matches `TA_BETA` (note: TA-Lib might use prices directly in some versions, check docs). |
|
||||
| **Skender** | ✅ | Matches `GetBeta` (uses returns). |
|
||||
| **Pandas-TA** | ✅ | Matches `beta` indicator. |
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Price vs. Returns**: Beta must be calculated on *returns*, not raw prices. This implementation handles the conversion internally. Feeding pre-calculated returns will yield incorrect results (it will calculate returns of returns).
|
||||
2. **Synchronization**: The Asset and Market data must be time-aligned. If the market data is missing for a bar where the asset has data, the correlation will be skewed.
|
||||
3. **Period Sensitivity**: A short period (e.g., 10) makes Beta noisy and unstable. A standard period is often 60 (approx. 3 months of daily data) or 252 (1 year).
|
||||
|
||||
## C# Usage
|
||||
|
||||
```csharp
|
||||
// Initialize with period 20
|
||||
var beta = new Beta(20);
|
||||
|
||||
// Update with Asset and Market prices
|
||||
// (e.g., AAPL price and SPY price)
|
||||
TValue result = beta.Update(assetPrice, marketPrice);
|
||||
|
||||
Console.WriteLine($"Beta: {result.Value:F4}");
|
||||
Reference in New Issue
Block a user