mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 11:08:05 +00:00
feat(statistics): add Variance indicator with O(1) calculation and usage example
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user