feat(statistics): add Variance indicator with O(1) calculation and usage example

This commit is contained in:
Miha Kralj
2025-12-25 17:18:41 -08:00
parent 9ba89812cd
commit 4ff6dc0ad9
61 changed files with 6069 additions and 99 deletions
@@ -0,0 +1,69 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class LinRegIndicatorTests
{
[Fact]
public void LinRegIndicator_Constructor_SetsDefaults()
{
var indicator = new LinRegIndicator();
Assert.Equal(14, indicator.Period);
Assert.Equal(0, indicator.Offset);
Assert.True(indicator.ShowColdValues);
Assert.Equal("LinReg - Linear Regression Curve", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(SourceType.Close, indicator.Source);
}
[Fact]
public void LinRegIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new LinRegIndicator { Period = 20 };
Assert.Equal(0, LinRegIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void LinRegIndicator_Initialize_CreatesInternalLinReg()
{
var indicator = new LinRegIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
Assert.Equal("LinReg", indicator.LinesSeries[0].Name);
}
[Fact]
public void LinRegIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new LinRegIndicator { Period = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// Need enough bars for Period
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 linreg = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(linreg));
}
}
+228
View File
@@ -0,0 +1,228 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class LinRegIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 14;
[InputParameter("Offset", sortIndex: 2, -2000, 2000, 1, 0)]
public int Offset { get; set; } = 0;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private LinReg? _linreg;
private readonly LineSeries? _series;
private Func<IHistoryItem, double>? _priceSelector;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"LinReg({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
public LinRegIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "LinReg - Linear Regression Curve";
Description = "Plots the end point of the linear regression line for each bar.";
_series = new(name: "LinReg", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_linreg = new LinReg(Period, Offset);
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector!(item);
var time = this.HistoricalData.Time();
var input = new TValue(time, value);
TValue result = _linreg!.Update(input, args.IsNewBar());
_series!.SetValue(result.Value, _linreg.IsHot, ShowColdValues);
}
}
[SkipLocalsInit]
public sealed class LinRegSlopeIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 14;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private LinReg? _linreg;
private readonly LineSeries? _series;
private Func<IHistoryItem, double>? _priceSelector;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"LinRegSlope({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
public LinRegSlopeIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "LinReg Slope";
Description = "Plots the slope of the linear regression line.";
_series = new(name: "Slope", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_linreg = new LinReg(Period);
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector!(item);
var time = this.HistoricalData.Time();
var input = new TValue(time, value);
_linreg!.Update(input, args.IsNewBar());
_series!.SetValue(_linreg.Slope, _linreg.IsHot, ShowColdValues);
}
}
[SkipLocalsInit]
public sealed class LinRegInterceptIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 14;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private LinReg? _linreg;
private readonly LineSeries? _series;
private Func<IHistoryItem, double>? _priceSelector;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"LinRegIntercept({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
public LinRegInterceptIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "LinReg Intercept";
Description = "Plots the intercept of the linear regression line.";
_series = new(name: "Intercept", color: IndicatorExtensions.Experiments, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_linreg = new LinReg(Period);
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector!(item);
var time = this.HistoricalData.Time();
var input = new TValue(time, value);
_linreg!.Update(input, args.IsNewBar());
_series!.SetValue(_linreg.Intercept, _linreg.IsHot, ShowColdValues);
}
}
[SkipLocalsInit]
public sealed class LinRegRSquaredIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 14;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private LinReg? _linreg;
private readonly LineSeries? _series;
private Func<IHistoryItem, double>? _priceSelector;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"LinRegR2({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
public LinRegRSquaredIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "LinReg R-Squared";
Description = "Plots the R-Squared (coefficient of determination) of the linear regression line.";
_series = new(name: "RSquared", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_linreg = new LinReg(Period);
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector!(item);
var time = this.HistoricalData.Time();
var input = new TValue(time, value);
_linreg!.Update(input, args.IsNewBar());
_series!.SetValue(_linreg.RSquared, _linreg.IsHot, ShowColdValues);
}
}
+124
View File
@@ -0,0 +1,124 @@
using Xunit;
namespace QuanTAlib.Tests;
public class LinRegTests
{
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new LinReg(0));
Assert.Throws<ArgumentException>(() => new LinReg(-1));
}
[Fact]
public void Calc_ReturnsValue()
{
var linreg = new LinReg(10);
var result = linreg.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
}
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var linreg = new LinReg(5);
for (int i = 0; i < 5; i++)
{
linreg.Update(new TValue(DateTime.UtcNow, i));
}
Assert.Equal(4, linreg.Last.Value); // Linear 0,1,2,3,4 -> LinReg at 4 is 4
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var linreg = new LinReg(5);
for (int i = 0; i < 5; i++)
{
linreg.Update(new TValue(DateTime.UtcNow, i));
}
// Last value is 4.
// Update with isNew=false to 5.
// Series becomes 0,1,2,3,5.
// Regression line will change.
linreg.Update(new TValue(DateTime.UtcNow, 5), isNew: false);
Assert.NotEqual(4, linreg.Last.Value);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var linreg = new LinReg(5);
linreg.Update(new TValue(DateTime.UtcNow, 10));
linreg.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(10, linreg.Last.Value);
}
[Fact]
public void AllModes_ProduceSameResult()
{
int period = 10;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = LinReg.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
LinReg.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new LinReg(period);
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new LinReg(pubSource, period);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
Assert.Equal(expected, spanResult, precision: 8);
Assert.Equal(expected, streamingResult, precision: 8);
Assert.Equal(expected, eventingResult, precision: 8);
}
[Fact]
public void Slope_Intercept_RSquared_Calculated()
{
// Perfect linear series: 0, 1, 2, 3, 4
// y = 1*x + 0 (if x starts at 0 and increases)
// In LinReg, x=0 is current (4), x=4 is oldest (0).
// So points are (0,4), (1,3), (2,2), (3,1), (4,0).
// y = -1*x + 4.
// Slope should be -(-1) = 1 (since we inverted slope in implementation to match time direction?)
// Wait, implementation says: Slope = -m.
// m for (0,4)...(4,0) is -1.
// So Slope = 1.
// Intercept (at x=0) is 4.
// RSquared should be 1.
var linreg = new LinReg(5);
for (int i = 0; i < 5; i++)
{
linreg.Update(new TValue(DateTime.UtcNow, i));
}
Assert.Equal(1.0, linreg.Slope, precision: 6);
Assert.Equal(4.0, linreg.Intercept, precision: 6);
Assert.Equal(1.0, linreg.RSquared, precision: 6);
}
}
@@ -0,0 +1,71 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Xunit;
using Skender.Stock.Indicators;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using System.Collections.Generic;
namespace QuanTAlib.Tests;
public class LinRegValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public LinRegValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_data.Dispose();
}
}
[SkipLocalsInit]
[Fact]
public void Validate_Against_Skender_Slope()
{
var period = 14;
var skender = _data.SkenderQuotes.GetSlope(period).ToList();
var linreg = new LinReg(period);
var slopeSeries = new TSeries();
foreach (var item in _data.Data)
{
linreg.Update(item);
slopeSeries.Add(new TValue(item.Time, linreg.Slope));
}
ValidationHelper.VerifyData(slopeSeries, skender, x => x.Slope, tolerance: ValidationHelper.DefaultTolerance);
}
[SkipLocalsInit]
[Fact]
public void Validate_Against_Skender_RSquared()
{
var period = 14;
var skender = _data.SkenderQuotes.GetSlope(period).ToList();
var linreg = new LinReg(period);
var r2Series = new TSeries();
foreach (var item in _data.Data)
{
linreg.Update(item);
r2Series.Add(new TValue(item.Time, linreg.RSquared));
}
ValidationHelper.VerifyData(r2Series, skender, x => x.RSquared, tolerance: ValidationHelper.DefaultTolerance);
}
}
+435
View File
@@ -0,0 +1,435 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// LinReg: Linear Regression Curve
/// </summary>
/// <remarks>
/// The Linear Regression Curve plots the end point of the linear regression line for each bar.
/// It fits a straight line y = mx + b to the data points using the least squares method.
///
/// Calculation:
/// Uses linear regression y = mx + b where x=0 is the current bar and x increases into the past.
/// m = (n * sum_xy - sum_x * sum_y) / denominator
/// b = (sum_y - m * sum_x) / n
/// LinReg = b - m * offset
///
/// O(1) update:
/// sum_y_new = sum_y_old - oldest + newest
/// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
///
/// Properties:
/// - Slope (m): The rate of change of the regression line.
/// - Intercept (b): The value of the regression line at x=0 (current bar).
/// - RSquared (r^2): The coefficient of determination (goodness of fit).
/// </remarks>
[SkipLocalsInit]
public sealed class LinReg : AbstractBase
{
private readonly int _period;
private readonly int _offset;
private readonly RingBuffer _buffer;
private readonly double _sum_x;
private readonly double _denominator;
private record struct State(double SumY, double SumXY, double SumY2, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
private int _tickCount;
private const int ResyncInterval = 1000;
private const double MinDenominator = 1e-10;
/// <summary>
/// The slope (m) of the linear regression line.
/// </summary>
public double Slope { get; private set; }
/// <summary>
/// The intercept (b) of the linear regression line at x=0.
/// </summary>
public double Intercept { get; private set; }
/// <summary>
/// The coefficient of determination (R-squared).
/// </summary>
public double RSquared { get; private set; }
public override bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates LinReg with specified period and offset.
/// </summary>
/// <param name="period">Lookback period (must be > 0)</param>
/// <param name="offset">
/// Offset from current bar (default 0).
/// Positive: project into future (offset=1 gives next bar's expected value)
/// Negative: project into past (offset=-1 gives previous bar's fitted value)
/// Zero: current bar (end point of regression line)
/// </param>
public LinReg(int period, int offset = 0)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_offset = offset;
_buffer = new RingBuffer(period);
Name = $"LinReg({period})";
WarmupPeriod = period;
// Precalculate constants
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
_sum_x = 0.5 * period * (period - 1);
// sum_x2 = 0^2 + ... + (n-1)^2 = (n-1)n(2n-1)/6
double sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
// denominator = n * sum_x2 - sum_x^2
_denominator = period * sum_x2 - _sum_x * _sum_x;
}
public LinReg(ITValuePublisher source, int period, int offset = 0) : this(period, offset)
{
source.Pub += (item) => Update(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_state.LastValidValue = input;
return input;
}
return _state.LastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
if (_buffer.IsFull)
{
double oldest = _buffer.Oldest;
double prev_sum_y = _state.SumY;
// O(1) update for sum_xy
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
_state.SumXY = _state.SumXY + prev_sum_y - _period * oldest;
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
// O(1) update for sum_y2
_state.SumY2 = Math.FusedMultiplyAdd(-oldest, oldest, _state.SumY2);
_state.SumY2 = Math.FusedMultiplyAdd(val, val, _state.SumY2);
_buffer.Add(val);
}
else
{
_buffer.Add(val);
_state.SumY += val;
_state.SumY2 = Math.FusedMultiplyAdd(val, val, _state.SumY2);
// Recalculate sum_xy from scratch during warmup
_state.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
// x=0 is newest (index count-1), x=count-1 is oldest (index 0)
int x = span.Length - 1 - i;
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
}
}
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
Resync();
}
}
private void Resync()
{
_state.SumY = _buffer.Sum;
_state.SumXY = 0;
var span = _buffer.GetSpan();
// Vectorized SumY2
_state.SumY2 = span.DotProduct(span);
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
double val = GetValidValue(input.Value);
UpdateState(val);
_p_state = _state;
_state.LastVal = val;
}
else
{
_state.LastValidValue = _p_state.LastValidValue;
double val = GetValidValue(input.Value);
_state.SumY = _p_state.SumY - _p_state.LastVal + val;
_state.SumY2 = Math.FusedMultiplyAdd(-_p_state.LastVal, _p_state.LastVal, _p_state.SumY2);
_state.SumY2 = Math.FusedMultiplyAdd(val, val, _state.SumY2);
_state.SumXY = _p_state.SumXY; // Unchanged: newest value at x=0 contributes 0 to sum_xy
_buffer.UpdateNewest(val);
_state.LastVal = val;
}
double result;
if (_buffer.Count <= 1)
{
result = _buffer.Newest;
Slope = 0;
Intercept = result;
RSquared = 0;
}
else
{
double n = _buffer.Count;
double sx = _sum_x;
double denom = _denominator;
if (!_buffer.IsFull)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
if (Math.Abs(denom) < MinDenominator)
{
result = _buffer.Newest;
Slope = 0;
Intercept = result;
RSquared = 0;
}
else
{
double m = Math.FusedMultiplyAdd(n, _state.SumXY, -sx * _state.SumY) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, _state.SumY) / n;
// Convert slope to time-forward direction:
// Our x-axis: x=0 (now), x=n-1 (past) — increases backward in time
// For rising prices: newest > oldest, so y decreases as x increases → m < 0
// Time-forward slope = -m → positive for rising prices
Slope = -m;
Intercept = b;
result = Math.FusedMultiplyAdd(-m, _offset, b);
// Calculate R-Squared
// R2 = (n * sum_xy - sum_x * sum_y)^2 / ( (n * sum_x2 - sum_x^2) * (n * sum_y2 - sum_y^2) )
double numerator = Math.FusedMultiplyAdd(n, _state.SumXY, -sx * _state.SumY);
double term2 = Math.FusedMultiplyAdd(n, _state.SumY2, -_state.SumY * _state.SumY);
RSquared = Math.Abs(term2) < MinDenominator
? 1.0 // All y are same
: numerator * numerator / (denom * term2);
}
}
Last = new TValue(input.Time, result);
PubEvent(Last);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
for (int i = 0; i < len; i++)
{
t.Add(0);
v.Add(0);
}
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
double initialLastValid = _state.LastValidValue;
Calculate(source.Values, vSpan, _period, _offset, initialLastValid);
source.Times.CopyTo(tSpan);
// Restore state
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
Reset();
if (startIndex > 0)
{
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
{
_state.LastValidValue = source.Values[i];
break;
}
}
}
else
{
_state.LastValidValue = initialLastValid;
}
double lastProcessedValue = _state.LastValidValue;
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
UpdateState(val);
lastProcessedValue = val;
}
_state.LastVal = lastProcessedValue;
_p_state = _state;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period, int offset = 0)
{
var linreg = new LinReg(period, offset);
return linreg.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int offset = 0, double initialLastValid = 0)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
int len = source.Length;
if (len == 0) return;
// Stack allocate for typical periods (most < 100)
// Heap allocate for large periods to avoid stack overflow
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
: new double[period];
double sum_y = 0;
double sum_xy = 0;
double lastValid = initialLastValid;
int bufferIndex = 0;
int count = 0;
double full_sum_x = 0.5 * period * (period - 1);
double full_sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double full_denom = period * full_sum_x2 - full_sum_x * full_sum_x;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
if (count < period)
{
buffer[count] = val;
sum_y += val;
count++;
sum_xy = 0;
for (int j = 0; j < count; j++)
{
sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy);
}
if (count <= 1)
{
output[i] = val;
}
else
{
double n = count;
double sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
double denom = n * sx2 - sx * sx;
if (Math.Abs(denom) < MinDenominator)
{
output[i] = val;
}
else
{
double m = Math.FusedMultiplyAdd(n, sum_xy, -sx * sum_y) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, sum_y) / n;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
}
}
if (count == period)
{
bufferIndex = 0;
}
}
else
{
double oldest = buffer[bufferIndex];
double prev_sum_y = sum_y;
sum_xy = sum_xy + prev_sum_y - period * oldest;
sum_y = sum_y - oldest + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
double m = Math.FusedMultiplyAdd(period, sum_xy, -full_sum_x * sum_y) / full_denom;
double b = Math.FusedMultiplyAdd(-m, full_sum_x, sum_y) / period;
output[i] = Math.FusedMultiplyAdd(-m, offset, b);
}
}
}
public override void Reset()
{
_buffer.Clear();
_state = default;
_p_state = default;
Last = default;
_tickCount = 0;
Slope = 0;
Intercept = 0;
RSquared = 0;
}
}
+90
View File
@@ -0,0 +1,90 @@
# LinReg: Linear Regression Curve
> "The trend is your friend, until it bends."
The Linear Regression Curve plots the end point of the linear regression line for each bar. It fits a straight line $y = mx + b$ to the data points using the least squares method, providing a smoothed representation of the price trend that is more responsive than a Simple Moving Average (SMA).
## Historical Context
Linear Regression is a fundamental statistical tool used to model the relationship between a dependent variable (price) and an independent variable (time). In technical analysis, it is used to identify the prevailing trend and potential reversal points. The Linear Regression Curve (often called LSMA or Least Squares Moving Average) connects the endpoints of regression lines calculated over a rolling window.
## Architecture & Physics
The `LinReg` indicator calculates the best-fit line for the last `Period` data points. It minimizes the sum of squared vertical distances between the observed data and the fitted line.
The calculation is optimized for streaming data using O(1) updates. Instead of recalculating the sums of $x$, $y$, $xy$, and $x^2$ from scratch for each new bar, the algorithm updates these sums incrementally as the window slides.
### Implementation Details
- **O(1) Update Formula**: The incremental update for $\sum xy$ is mathematically elegant. When removing the oldest value and shifting all x-coordinates by +1, the sum increases by the previous sum of y minus the contribution of the oldest value: `sum_xy_new = sum_xy_old + prev_sum_y - n * oldest`.
- **Floating-Point Drift Protection**: To combat the accumulation of rounding errors inherent in incremental algorithms, the indicator performs a full recalculation from scratch every 1000 updates (`ResyncInterval`).
- **R-Squared Stability**: Handles edge cases where variance is zero (all values identical) by setting $R^2$ to 1.0 (perfect fit to a horizontal line), avoiding division by zero.
- **Slope Sign Convention**: The internal coordinate system uses $x=0$ for the present and increases into the past. This results in a negative slope for rising prices in x-space. The public `Slope` property negates this value (`Slope = -m`) to provide a standard time-forward slope interpretation.
### Complexity
| Metric | Value | Notes |
| :--- | :--- | :--- |
| **Time Complexity** | O(1) | Constant time update per bar. |
| **Space Complexity** | O(N) | Requires a buffer of size `Period`. |
| **Stability** | High | Uses double precision floating point. |
## Mathematical Foundation
The linear regression line is defined by the equation:
$$ y = mx + b $$
Where:
- $m$ is the slope.
- $b$ is the y-intercept.
- $x$ is the time index (0 for the current bar, increasing into the past).
The coefficients are calculated as:
$$ m = \frac{n \sum xy - \sum x \sum y}{n \sum x^2 - (\sum x)^2} $$
$$ b = \frac{\sum y - m \sum x}{n} $$
The `LinReg` value at the current bar (offset 0) is simply the intercept $b$ (since $x=0$).
### Properties
- **Slope**: The rate of change of the regression line. Positive slope indicates an uptrend, negative slope indicates a downtrend.
- **Intercept**: The value of the regression line at the current bar.
- **RSquared**: The coefficient of determination ($r^2$), indicating how well the line fits the data (0 to 1).
## Performance Profile
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | High | O(1) updates ensure minimal latency. |
| **Allocations** | 0 | Zero allocations in the hot path. |
| **Accuracy** | High | Matches standard statistical definitions. |
| **Responsiveness** | High | More responsive than SMA for the same period. |
## Validation
Validated against Skender.Stock.Indicators.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **Skender** | ✅ | Slope and RSquared match. |
| **Ooples** | ⚠️ | Slope magnitude differs significantly (likely unit mismatch). |
## Usage
```csharp
using QuanTAlib;
// Create indicator with period 14
var linreg = new LinReg(14);
// Update with new value
linreg.Update(new TValue(DateTime.UtcNow, 100.0));
// Access result
double value = linreg.Last.Value;
double slope = linreg.Slope;
double r2 = linreg.RSquared;