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:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
@@ -0,0 +1,79 @@
---
description: Configuration for AI behavior when interacting with Codacy's MCP Server
applyTo: '**'
---
---
# Codacy Rules
Configuration for AI behavior when interacting with Codacy's MCP Server
## using any tool that accepts the arguments: `provider`, `organization`, or `repository`
* ALWAYS use:
* provider: gh
* organization: mihakralj
* repository: QuanTAlib
* Avoid calling `git remote -v` unless really necessary
## CRITICAL: After ANY successful `edit_file` or `reapply` operation
* YOU MUST IMMEDIATELY run the `codacy_cli_analyze` tool from Codacy's MCP Server for each file that was edited, with:
* `rootPath`: set to the workspace path
* `file`: set to the path of the edited file
* `tool`: leave empty or unset
* If any issues are found in the new edits, propose and apply fixes for them.
* > NOTE: Failure to follow this rule is considered a critical error.
## When you tried to run the `codacy_cli_analyze` tool and the Codacy CLI is not installed
* Ask the user 'Codacy CLI is not installed. Would you like me to install it now?'
* If the user responds with "yes", run the `codacy_cli_install` tool and then continue with the original task
* If the user responds with "no", instruct the user that they can disable automatic analysis in the extension settings
* Wait for the user to respond before proceeding with any other actions
## After every response
* If you made any file edits in this conversation, verify you ran `codacy_cli_analyze` tool from Codacy's MCP Server
## When there are no Codacy MCP Server tools available, or the MCP Server is not reachable
* Suggest the user the following troubleshooting steps:
* Try to reset the MCP on the extension
* If the user is using VSCode, suggest them to review their Copilot > MCP settings in Github, under their organization or personal account. Refer them to Settings > Copilot > Enable MCP servers in Copilot. Suggested URL (https://github.com/settings/copilot/features) or https://github.com/organizations/{organization-name}/settings/copilot/features (This can only be done by their organization admins / owners)
* If none of the above steps work, suggest the user to contact Codacy support
## Trying to call a tool that needs a rootPath as a parameter
* Always use the standard, non-URL-encoded file system path
## CRITICAL: Dependencies and Security Checks
* IMMEDIATELY after ANY of these actions:
* Running npm/yarn/pnpm install
* Adding dependencies to package.json
* Adding requirements to requirements.txt
* Adding dependencies to pom.xml
* Adding dependencies to build.gradle
* Any other package manager operations
* You MUST run the `codacy_cli_analyze` tool with:
* `rootPath`: set to the workspace path
* `tool`: set to "trivy"
* `file`: leave empty or unset
* If any vulnerabilities are found because of the newly added packages:
* Stop all other operations
* Propose and apply fixes for the security issues
* Only continue with the original task after security issues are resolved
* EXAMPLE:
* After: npm install react-markdown
* Do: Run codacy_cli_analyze with trivy
* Before: Continuing with any other tasks
## General
* Repeat the relevant steps for each modified file.
* "Propose fixes" means to both suggest and, if possible, automatically apply the fixes.
* You MUST NOT wait for the user to ask for analysis or remind you to run the tool.
* Do not run `codacy_cli_analyze` looking for changes in duplicated code or code complexity metrics.
* Complexity metrics are different from complexity issues. When trying to fix complexity in a repository or file, focus on solving the complexity issues and ignore the complexity metric.
* Do not run `codacy_cli_analyze` looking for changes in code coverage.
* Do not try to manually install Codacy CLI using either brew, npm, npx, or any other package manager.
* If the Codacy CLI is not installed, just run the `codacy_cli_analyze` tool from Codacy's MCP Server.
* When calling `codacy_cli_analyze`, only send provider, organization and repository if the project is a git repository.
## Whenever a call to a Codacy tool that uses `repository` or `organization` as a parameter returns a 404 error
* Offer to run the `codacy_setup_repository` tool to add the repository to Codacy
* If the user accepts, run the `codacy_setup_repository` tool
* Do not ever try to run the `codacy_setup_repository` tool on your own
* After setup, immediately retry the action that failed (only retry once)
---
+4
View File
@@ -0,0 +1,4 @@
#Ignore vscode AI rules
.github\instructions\codacy.instructions.md
+1
View File
@@ -0,0 +1 @@
{}
+169
View File
@@ -0,0 +1,169 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class LsmaIndicatorTests
{
[Fact]
public void LsmaIndicator_Constructor_SetsDefaults()
{
var indicator = new LsmaIndicator();
Assert.Equal(25, indicator.Period);
Assert.Equal(0, indicator.Offset);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("LSMA - Least Squares Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void LsmaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new LsmaIndicator { Period = 20 };
Assert.Equal(0, LsmaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void LsmaIndicator_ShortName_IncludesPeriodOffsetAndSource()
{
var indicator = new LsmaIndicator { Period = 15, Offset = 2 };
Assert.Contains("LSMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void LsmaIndicator_SourceCodeLink_IsValid()
{
var indicator = new LsmaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Lsma.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void LsmaIndicator_Initialize_CreatesInternalLsma()
{
var indicator = new LsmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void LsmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new LsmaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have a value
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void LsmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new LsmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void LsmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new LsmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void LsmaIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new LsmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
}
[Fact]
public void LsmaIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new LsmaIndicator { Period = 3, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void LsmaIndicator_PeriodAndOffset_CanBeChanged()
{
var indicator = new LsmaIndicator { Period = 5, Offset = 0 };
Assert.Equal(5, indicator.Period);
Assert.Equal(0, indicator.Offset);
indicator.Period = 20;
indicator.Offset = 2;
Assert.Equal(20, indicator.Period);
Assert.Equal(2, indicator.Offset);
Assert.Equal(0, LsmaIndicator.MinHistoryDepths);
}
}
+59
View File
@@ -0,0 +1,59 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class LsmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 25;
[InputParameter("Offset", sortIndex: 2, -1000, 1000, 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 Lsma _lsma = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"LSMA {Period}:{_sourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/lsma/Lsma.Quantower.cs";
public LsmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "LSMA - Least Squares Moving Average";
Description = "Least Squares Moving Average";
_series = new LineSeries(name: $"LSMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_priceSelector = Source.GetPriceSelector();
_sourceName = Source.ToString();
_lsma = new Lsma(Period, Offset);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
bool isNew = args.IsNewBar();
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
double value = _lsma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
_series.SetValue(value, _lsma.IsHot, ShowColdValues);
}
}
+303
View File
@@ -0,0 +1,303 @@
namespace QuanTAlib.Tests;
public class LsmaTests
{
[Fact]
public void Constructor_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Lsma(0));
Assert.Throws<ArgumentException>(() => new Lsma(-1));
}
[Fact]
public void Constructor_ValidParameters_SetsProperties()
{
var lsma = new Lsma(14, 0);
Assert.Equal("Lsma(14)", lsma.Name);
Assert.False(lsma.IsHot);
}
[Fact]
public void Update_SingleValue_ReturnsSameValue()
{
var lsma = new Lsma(14);
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
}
[Fact]
public void Update_LinearTrend_ReturnsExactValue()
{
// For a perfect linear trend y = x, LSMA should return x
const int period = 10;
var lsma = new Lsma(period);
for (int i = 0; i < period * 2; i++)
{
var result = lsma.Update(new TValue(DateTime.UtcNow, i));
if (i >= period) // After warmup
{
Assert.Equal(i, result.Value, 1e-9);
}
}
}
[Fact]
public void Update_ConstantValue_ReturnsSameValue()
{
const int period = 10;
var lsma = new Lsma(period);
const double value = 123.45;
for (int i = 0; i < period * 2; i++)
{
var result = lsma.Update(new TValue(DateTime.UtcNow, value));
Assert.Equal(value, result.Value, 1e-9);
}
}
[Fact]
public void Update_WithOffset_ProjectsCorrectly()
{
// y = 2x + 1
// At x=10, y=21. Slope=2, Intercept=1
// LSMA(offset=1) should project to x=11 -> y=23
const int period = 5;
const int offset = 1;
var lsma = new Lsma(period, offset);
for (int i = 0; i < 20; i++)
{
double y = 2 * i + 1;
var result = lsma.Update(new TValue(DateTime.UtcNow, y));
if (i >= period)
{
double expected = 2 * (i + offset) + 1;
Assert.Equal(expected, result.Value, 1e-9);
}
}
}
[Fact]
public void Update_BarCorrection_UpdatesCorrectly()
{
var lsma = new Lsma(5);
// Fill buffer
for (int i = 0; i < 5; i++)
{
lsma.Update(new TValue(DateTime.UtcNow, i));
}
// New bar
var result1 = lsma.Update(new TValue(DateTime.UtcNow, 10));
// Update same bar with different value
var result2 = lsma.Update(new TValue(DateTime.UtcNow, 20), isNew: false);
Assert.NotEqual(result1.Value, result2.Value);
// Verify internal state by adding next bar
// If state was corrupted, this would fail
var result3 = lsma.Update(new TValue(DateTime.UtcNow, 30));
Assert.True(double.IsFinite(result3.Value));
}
[Fact]
public void Update_NaN_HandlesGracefully()
{
var lsma = new Lsma(5);
lsma.Update(new TValue(DateTime.UtcNow, 1));
lsma.Update(new TValue(DateTime.UtcNow, 2));
var result = lsma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Input sequence becomes: 1, 2, 2 (NaN replaced by last valid 2)
// Regression on (2,1), (1,2), (0,2)
// Result should be 2.166666667
Assert.Equal(2.1666666666666665, result.Value, 1e-9);
}
[Fact]
public void Calculate_StaticMethod_MatchesObjectInstance()
{
const int period = 10;
const int count = 100;
var source = new TSeries();
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
source.Add(bar.C);
}
var lsma = new Lsma(period);
var series1 = lsma.Update(source);
var series2 = Lsma.Batch(source, period);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < count; i++)
{
Assert.Equal(series1[i].Value, series2[i].Value, 1e-9);
}
}
[Fact]
public void Calculate_Span_MatchesSeries()
{
const int period = 10;
const int count = 100;
var values = new double[count];
var output = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
values[i] = bar.Close;
}
Lsma.Calculate(values, output, period);
var lsma = new Lsma(period);
for (int i = 0; i < count; i++)
{
var result = lsma.Update(new TValue(DateTime.UtcNow, values[i]));
Assert.Equal(result.Value, output[i], 1e-9);
}
}
[Fact]
public void Reset_ClearsState()
{
var lsma = new Lsma(5);
for (int i = 0; i < 10; i++)
{
lsma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(lsma.IsHot);
lsma.Reset();
Assert.False(lsma.IsHot);
Assert.Equal(0, lsma.Last.Value);
// Should behave like new instance
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
const int period = 5;
var lsma = new Lsma(period);
for (int i = 0; i < period; i++)
{
Assert.False(lsma.IsHot);
lsma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(lsma.IsHot);
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var lsma = new Lsma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
// Verify subscription works
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
// Dispose and verify unsubscription
lsma.Dispose();
// Add more data - lsma should NOT update
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value); // Should remain at previous value
}
[Fact]
public void Dispose_IsIdempotent()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Multiple Dispose calls should not throw
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
lsma.Dispose();
lsma.Dispose();
lsma.Dispose();
#pragma warning restore S3966
// Verify still unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public async System.Threading.Tasks.Task Dispose_IsThreadSafe()
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Dispose from multiple threads simultaneously
var tasks = new System.Threading.Tasks.Task[10];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = System.Threading.Tasks.Task.Run(() => lsma.Dispose());
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Verify unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
}
[Fact]
public void Dispose_WithoutSource_DoesNotThrow()
{
// Lsma created without source parameter
var lsma = new Lsma(5);
// Should not throw even though there's no source to unsubscribe from
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
lsma.Dispose();
lsma.Dispose(); // Idempotent
#pragma warning restore S3966
// Verify state remains valid
Assert.False(lsma.IsHot);
}
[Fact]
public void Constructor_NullSource_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new Lsma(null!, 5));
}
}
@@ -0,0 +1,80 @@
using Skender.Stock.Indicators;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class LsmaValidationTests
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
public LsmaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib LSMA (batch TSeries)
var lsma = new global::QuanTAlib.Lsma(period);
var qResult = lsma.Update(_testData.Data);
// Calculate Skender EPMA (Endpoint Moving Average = LSMA)
var sResult = _testData.SkenderQuotes.GetEpma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, x => x.Epma, tolerance: ValidationHelper.OoplesTolerance);
}
_output.WriteLine("LSMA Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib LSMA (streaming)
var lsma = new global::QuanTAlib.Lsma(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(lsma.Update(item).Value);
}
// Calculate Skender EPMA
var sResult = _testData.SkenderQuotes.GetEpma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResults, sResult, x => x.Epma, tolerance: ValidationHelper.OoplesTolerance);
}
_output.WriteLine("LSMA Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib LSMA (Span API)
double[] qOutput = new double[_testData.RawData.Length];
global::QuanTAlib.Lsma.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period);
// Calculate Skender EPMA
var sResult = _testData.SkenderQuotes.GetEpma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, sResult, x => x.Epma, tolerance: ValidationHelper.OoplesTolerance);
}
_output.WriteLine("LSMA Span validated successfully against Skender");
}
}
+421
View File
@@ -0,0 +1,421 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// LSMA: Least Squares Moving Average
/// </summary>
/// <remarks>
/// LSMA calculates the linear regression line for the last n values and returns the value at the current position (or offset).
/// Uses a RingBuffer for storage and O(1) updates for regression sums.
///
/// 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
/// LSMA = 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
///
/// IsHot:
/// Becomes true when the buffer is full (period samples processed).
///
/// Disposal:
/// When constructed with an ITValuePublisher source, Lsma subscribes to the source's Pub event.
/// Call Dispose() to unsubscribe and prevent memory leaks, especially in long-running applications
/// or when creating many short-lived indicator instances.
/// </remarks>
[SkipLocalsInit]
public sealed class Lsma : AbstractBase
{
private readonly int _period;
private readonly int _offset;
private readonly RingBuffer _buffer;
private readonly double _sum_x;
private readonly double _denominator;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _source;
private int _disposed;
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
private int _tickCount;
private bool _isNew;
private const int ResyncInterval = 1000;
public override bool IsHot => _buffer.IsFull;
public bool IsNew => _isNew;
/// <summary>
/// Creates LSMA 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 values project into future.</param>
public Lsma(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 = $"Lsma({period})";
WarmupPeriod = period;
_handler = Handle;
// 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;
_state.LastValidValue = double.NaN;
}
public Lsma(ITValuePublisher source, int period, int offset = 0) : this(period, offset)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_source.Pub += _handler;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[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 = Math.FusedMultiplyAdd(-_period, oldest, _state.SumXY + prev_sum_y);
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
_buffer.Add(val);
}
else
{
if (_buffer.Count > 0)
{
_state.SumXY += _state.SumY;
}
_state.SumY += val;
_buffer.Add(val);
}
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
Resync();
}
}
private void Resync()
{
_state.SumY = _buffer.Sum;
_state.SumXY = 0;
var span = _buffer.GetSpan();
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)
{
_isNew = isNew;
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);
// For isNew=false, we update the current bar.
// sum_xy remains constant because it depends on the previous window state which hasn't changed.
// sum_y updates to reflect the change in the newest value.
_state.SumY = _p_state.SumY - _p_state.LastVal + val;
_state.SumXY = _p_state.SumXY; // Restore sum_xy to the state after the shift
_buffer.UpdateNewest(val);
_state.LastVal = val;
}
double result;
if (_buffer.Count <= 1)
{
result = _buffer.Newest;
}
else
{
// Calculate regression parameters
// During warmup, we use the current count as n
double n = _buffer.Count;
double sx = _sum_x;
double denom = _denominator;
if (!_buffer.IsFull)
{
// Recalculate constants for smaller n
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) < 1e-10)
{
result = _buffer.Newest;
}
else
{
double m = Math.FusedMultiplyAdd(n, _state.SumXY, -sx * _state.SumY) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, _state.SumY) / n;
// LSMA = b - m * offset
result = Math.FusedMultiplyAdd(-m, _offset, b);
}
}
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
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);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
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
// We need to replay the last 'period' bars to set up the buffer and sums correctly
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
Reset();
// Initialize lastValidValue
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, TimeSpan? step = null)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period, int offset = 0)
{
var lsma = new Lsma(period, offset);
return lsma.Update(source);
}
/// <summary>
/// Calculates LSMA in-place, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int offset = 0, double initialLastValid = double.NaN)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
int len = source.Length;
if (len == 0) return;
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; // Points to where the NEXT value will be written (circular)
int count = 0;
// Precalculate constants for full period
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)
{
// Warmup phase
buffer[count] = val;
count++;
// O(1) update: adding new value at x=0, existing values shift x+1
// New value at x=0 contributes 0, existing sum shifts by sum_y
if (count > 1)
{
sum_xy += sum_y; // Shift existing values before adding new
}
sum_y += val;
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) < 1e-10)
{
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; // Reset for circular buffer usage
}
}
else
{
// Full buffer phase - O(1) update
double oldest = buffer[bufferIndex];
double prev_sum_y = sum_y;
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
sum_xy = Math.FusedMultiplyAdd(-period, oldest, sum_xy + prev_sum_y);
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);
}
}
}
/// <summary>
/// Resets the LSMA state.
/// </summary>
public override void Reset()
{
_buffer.Clear();
_state = default;
_state.LastValidValue = double.NaN;
_p_state = default;
Last = default;
_tickCount = 0;
}
/// <summary>
/// Disposes the Lsma instance, unsubscribing from the source publisher if subscribed.
/// This method is idempotent and thread-safe.
/// </summary>
protected override void Dispose(bool disposing)
{
// Use Interlocked.CompareExchange for thread-safe, idempotent disposal
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0 && _source != null)
{
_source.Pub -= _handler;
_source = null;
}
base.Dispose(disposing);
}
}
+230
View File
@@ -0,0 +1,230 @@
# LSMA: Least Squares Moving Average
> "If you want to know where the price is going, draw a line through where it's been. LSMA does this for every single bar, tirelessly fitting linear regressions while you sleep."
LSMA (Least Squares Moving Average), also known as the Moving Linear Regression or Endpoint Moving Average, calculates the least squares regression line for the preceding time periods. In plain English: it finds the "best fit" line for the data window and tells you where that line ends.
## Historical Context
Linear regression is as old as Gauss (c. 1809). Applying it as a moving window to financial time series is a more recent development, popularized by traders who realized that a moving average is just a poor man's regression line (specifically, an SMA is a regression line with a slope of 0). LSMA captures both the level and the trend (slope) of the data.
## Architecture & Physics
LSMA is computationally heavier than an SMA because it minimizes the sum of squared errors for a line equation $y = mx + b$.
* **Slope ($m$)**: Represents the trend strength/direction.
* **Intercept ($b$)**: Represents the value at the start of the window.
* **Endpoint**: The value at the current bar ($y = m \times 0 + b$ in our coordinate system where current bar is 0).
## Mathematical Foundation
The regression line is $y = mx + b$.
$$ m = \frac{N \sum xy - \sum x \sum y}{N \sum x^2 - (\sum x)^2} $$
$$ b = \frac{\sum y - m \sum x}{N} $$
$$ \text{LSMA} = b - m \times \text{Offset} $$
(Note: In the QuanTAlib implementation, $x$ ranges from $N-1$ (oldest) to $0$ (newest) to simplify the math).
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
The O(1) algorithm maintains running sums instead of recomputing the regression on each bar:
**State variables maintained:**
- `sum_x`: Sum of x indices (precomputed constant for fixed period)
- `sum_y`: Running sum of y values
- `sum_xy`: Running sum of x×y products
- `sum_xx`: Sum of x² (precomputed constant)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| ADD/SUB | 6 | 1 | 6 |
| MUL | 4 | 3 | 12 |
| DIV | 2 | 15 | 30 |
| **Total** | **12** | — | **~48 cycles** |
**Hot path breakdown:**
- Update running sums: `sum_y += new - old`, `sum_xy += (N-1)×new - sum_y_old` → 4 ADD/SUB
- Slope calculation: `m = (N×sum_xy - sum_x×sum_y) / denom` → 2 MUL + 1 DIV
- Intercept: `b = (sum_y - m×sum_x) / N` → 1 MUL + 1 SUB + 1 DIV
- Endpoint: `LSMA = b - m×offset` → 1 MUL + 1 SUB
**Comparison with naive O(N) regression:**
| Mode | Complexity | Cycles (Period=100) |
| :--- | :---: | :---: |
| Naive (recompute) | O(N) | ~600 cycles |
| QuanTAlib O(1) | O(1) | ~48 cycles |
| **Improvement** | **—** | **~12× faster** |
### Batch Mode (SIMD)
LSMA batch can vectorize the running sum updates:
| Operation | Scalar Ops (512 bars) | SIMD Ops (AVX2) | Speedup |
| :--- | :---: | :---: | :---: |
| Running sum updates | 512 | 64 | 8× |
| Slope calculations | 1024 | 128 | 8× |
| Endpoint projections | 512 | 64 | 8× |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Mathematically precise regression endpoint |
| **Timeliness** | 8/10 | Projects trend forward, reducing perceived lag |
| **Overshoot** | 2/10 | Significant overshoot on trend reversals (projects continuation) |
| **Smoothness** | 3/10 | Sensitive to outliers; least-squares fit follows noise |
## Validation
Validated against Skender.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **Skender** | ✅ | Matches `GetEpma` |
| **TA-Lib** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented. |
| **Ooples** | N/A | Not implemented. |
### C# Implementation Considerations
The QuanTAlib LSMA implementation achieves O(1) streaming updates through running sum maintenance with several optimizations:
#### O(1) Running Sum Algorithm
The implementation maintains two running sums (`SumY`, `SumXY`) that enable constant-time updates instead of O(N) recalculation:
```csharp
// O(1) update for sum_xy: sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
_state.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _state.SumXY + prev_sum_y);
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
```
#### Precomputed Constants
Mathematical constants are computed once in the constructor to avoid redundant calculations:
```csharp
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
_sum_x = 0.5 * period * (period - 1);
// sum_x2 = 0² + ... + (n-1)² = (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²
_denominator = period * sum_x2 - _sum_x * _sum_x;
```
#### State Record Struct
State uses `LayoutKind.Auto` for compiler-optimized field ordering:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private State _state;
private State _p_state; // Previous state for bar correction
```
#### FusedMultiplyAdd Usage
FMA is used extensively for slope, intercept, and endpoint calculations:
```csharp
double m = Math.FusedMultiplyAdd(n, _state.SumXY, -sx * _state.SumY) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, _state.SumY) / n;
result = Math.FusedMultiplyAdd(-m, _offset, b);
```
#### Periodic Resync
Running sums accumulate floating-point drift; periodic resync every 1000 ticks corrects this:
```csharp
private const int ResyncInterval = 1000;
private void Resync()
{
_state.SumY = _buffer.Sum;
_state.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
}
}
```
#### Stackalloc/ArrayPool Strategy
The static `Calculate` method uses stackalloc for small periods (≤256) to avoid heap allocation:
```csharp
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
: new double[period];
```
#### Thread-Safe Disposal
Disposal uses atomic operations for idempotent, thread-safe cleanup:
```csharp
protected override void Dispose(bool disposing)
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0 && _source != null)
{
_source.Pub -= _handler;
_source = null;
}
base.Dispose(disposing);
}
```
#### NaN Handling
Invalid values are replaced with the last valid value to maintain calculation integrity:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_state.LastValidValue = input;
return input;
}
return _state.LastValidValue;
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Lookback window |
| `_offset` | `int` | 4 | Forecast offset |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular storage |
| `_sum_x` | `double` | 8 | Precomputed Σx |
| `_denominator` | `double` | 8 | Precomputed denominator |
| `_state` | `State` | 32 | Current state (SumY, SumXY, LastVal, LastValidValue) |
| `_p_state` | `State` | 32 | Previous state for rollback |
| `_tickCount` | `int` | 4 | Resync counter |
| `_disposed` | `int` | 4 | Atomic disposal flag |
| **Total** | | **~104 bytes** | Per instance (excluding RingBuffer internal storage) |
### Common Pitfalls
1. **Overshoot**: Because it projects a trend, LSMA will overshoot significantly when the trend reverses. It assumes the trend continues.
2. **Offset**: You can use a positive offset to extrapolate into the future (forecasting), or a negative offset to center the average.
3. **Noise**: It is very sensitive to outliers because it tries to fit a line to them.
+61
View File
@@ -0,0 +1,61 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Least Squares Moving Average (LSMA)", "LSMA", overlay=true)
//@function Calculates LSMA by fitting a linear regression line to price data
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/lsma.md
//@param source Series to calculate LSMA from
//@param period Lookback period for the linear regression
//@returns LSMA value, calculates from first bar using available data
//@optimized Uses circular buffer with linear regression for O(n) complexity per bar
lsma(series float source, simple int period) =>
if period <= 1
runtime.error("Period must be greater than 1")
source
else
int p = math.min(bar_index + 1, period)
if p <= 1
source
else
var array<float> buffer = array.new_float(period, na)
var int head = 0
array.set(buffer, head, source)
head := (head + 1) % period
float sum_y = 0.0
float sum_xy = 0.0
float sum_x = 0.0
float sum_x2 = 0.0
float count = 0.0
int idx = (head - 1 + period) % period
for i = 0 to p - 1
float val = array.get(buffer, idx)
if not na(val)
sum_x += i
sum_y += val
sum_xy += i * val
sum_x2 += i * i
count += 1.0
idx := (idx - 1 + period) % period
if count <= 1.0
source
else
float denom = count * sum_x2 - sum_x * sum_x
if denom == 0.0
source
else
float slope = (count * sum_xy - sum_x * sum_y) / denom
float intercept = (sum_y - slope * sum_x) / count
intercept
// ---------- Main loop ----------
// Inputs
i_period = input.int(10, "Period", minval=1)
i_source = input.source(close, "Source")
// Calculation
lsma_value = lsma(i_source, i_period)
// Plot
plot(lsma_value, "LSMA", color=color.yellow, linewidth=2)