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
+214
View File
@@ -0,0 +1,214 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class SlopeIndicatorTests
{
[Fact]
public void SlopeIndicator_Constructor_SetsDefaults()
{
var indicator = new SlopeIndicator();
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("SLOPE - First Derivative (Velocity)", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.False(indicator.OnBackGround);
}
[Fact]
public void SlopeIndicator_MinHistoryDepths_IsTwo()
{
var indicator = new SlopeIndicator();
Assert.Equal(2, indicator.MinHistoryDepths);
}
[Fact]
public void SlopeIndicator_ShortName_IsSlope()
{
var indicator = new SlopeIndicator();
Assert.Equal("SLOPE", indicator.ShortName);
}
[Fact]
public void SlopeIndicator_Initialize_CreatesLineSeries()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
Assert.Equal(2, indicator.LinesSeries.Count);
Assert.Equal("Slope", indicator.LinesSeries[0].Name);
Assert.Equal("Zero", indicator.LinesSeries[1].Name);
}
[Fact]
public void SlopeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.Equal(1, indicator.LinesSeries[1].Count);
}
[Fact]
public void SlopeIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new SlopeIndicator();
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 SlopeIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void SlopeIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(
now.AddMinutes(i),
100 + i * 2,
105 + i * 2,
95 + i * 2,
102 + i * 2);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(20, indicator.LinesSeries[0].Count);
for (int i = 0; i < 20; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
Assert.Equal(0, indicator.LinesSeries[1].GetValue(i));
}
}
[Fact]
public void SlopeIndicator_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 SlopeIndicator { Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
}
[Fact]
public void SlopeIndicator_ShowColdValues_False_SetsNaN()
{
var indicator = new SlopeIndicator { ShowColdValues = false };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void SlopeIndicator_Uptrend_ProducesPositiveSlope()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double price = 100 + i * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastSlope = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastSlope > 0);
}
[Fact]
public void SlopeIndicator_Downtrend_ProducesNegativeSlope()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double price = 200 - i * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastSlope = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastSlope < 0);
}
[Fact]
public void SlopeIndicator_FlatPrices_ProducesZeroSlope()
{
var indicator = new SlopeIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastSlope = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, lastSlope);
}
}
+71
View File
@@ -0,0 +1,71 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// SLOPE (First Derivative / Velocity) Quantower indicator.
/// Measures the instantaneous rate of change between consecutive values.
/// </summary>
public class SlopeIndicator : Indicator, IWatchlistIndicator
{
[DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Slope? _slope;
private Func<IHistoryItem, double>? _selector;
public int MinHistoryDepths => 2;
public override string ShortName => "SLOPE";
public SlopeIndicator()
{
Name = "SLOPE - First Derivative (Velocity)";
Description = "Measures instantaneous rate of change between consecutive values";
SeparateWindow = true;
OnBackGround = false;
}
protected override void OnInit()
{
_slope = new Slope();
_selector = Source.GetPriceSelector();
AddLineSeries(new LineSeries("Slope", Momentum, 2, LineStyle.Histogramm));
AddLineSeries(new LineSeries("Zero", Color.Gray, 1, LineStyle.Dot));
}
protected override void OnUpdate(UpdateArgs args)
{
if (_slope == null || _selector == null) return;
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
bool isNew = args.IsNewBar();
TValue input = new(item.TimeLeft, value);
_slope.Update(input, isNew);
bool isHot = _slope.IsHot;
LinesSeries[0].SetValue(_slope.Last.Value, isHot, ShowColdValues);
LinesSeries[1].SetValue(0);
if (isHot || ShowColdValues)
{
double slope = _slope.Last.Value;
Color color;
if (slope > 0)
color = Color.Green;
else if (slope < 0)
color = Color.Red;
else
color = Color.Gray;
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
}
}
}
+251
View File
@@ -0,0 +1,251 @@
namespace QuanTAlib.Tests;
public class SlopeTests
{
[Fact]
public void Properties_Accessible()
{
var slope = new Slope();
Assert.Equal(0, slope.Last.Value);
Assert.False(slope.IsHot);
Assert.Contains("Slope", slope.Name, StringComparison.Ordinal);
Assert.Equal(2, slope.WarmupPeriod);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var slope = new Slope();
slope.Update(new TValue(DateTime.UtcNow, 10));
slope.Update(new TValue(DateTime.UtcNow, 20));
double valueBefore = slope.Last.Value;
// Update with isNew=false should change the result
slope.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
double valueAfter = slope.Last.Value;
Assert.NotEqual(valueBefore, valueAfter);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var slope = new Slope();
slope.Update(new TValue(DateTime.UtcNow, 10));
slope.Update(new TValue(DateTime.UtcNow, 20));
var result = slope.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var slope = new Slope();
slope.Update(new TValue(DateTime.UtcNow, 10));
slope.Update(new TValue(DateTime.UtcNow, 20));
var resultPosInf = slope.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultPosInf.Value));
var resultNegInf = slope.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultNegInf.Value));
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var slope = new Slope();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
slope.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double stateAfterTen = slope.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
slope.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalResult = slope.Update(tenthInput, isNew: false);
// State should match the original state after 10 values
Assert.Equal(stateAfterTen, finalResult.Value, 1e-9);
}
[Fact]
public void SpanBatch_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] wrongSizeOutput = new double[3];
// Output must be same length as source
Assert.Throws<ArgumentException>(() =>
Slope.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan()));
}
[Fact]
public void AllModes_ProduceSameResult()
{
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 (static span)
var tValues = series.Values.ToArray();
var batchOutput = new double[tValues.Length];
Slope.Calculate(tValues, batchOutput);
double expected = batchOutput[^1];
// 2. Streaming Mode
var streamingInd = new Slope();
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 3. TSeries Batch Mode
var batchSeriesResult = Slope.Calculate(series);
double tseriesResult = batchSeriesResult.Last.Value;
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, tseriesResult, precision: 9);
}
[Fact]
public void Calculation_KnownValues()
{
// slope[i] = source[i] - source[i-1]
// Data: 10, 20, 25, 30, 28
// Slopes: 0, 10, 5, 5, -2
double[] data = [10, 20, 25, 30, 28];
double[] expected = [0, 10, 5, 5, -2];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var slope = new Slope();
Assert.False(slope.IsHot);
slope.Update(new TValue(DateTime.UtcNow, 10));
Assert.False(slope.IsHot);
slope.Update(new TValue(DateTime.UtcNow, 20));
Assert.True(slope.IsHot);
}
[Fact]
public void Reset_ClearsState()
{
var slope = new Slope();
for (int i = 0; i < 10; i++)
{
slope.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(slope.IsHot);
slope.Reset();
Assert.False(slope.IsHot);
Assert.Equal(0, slope.Last.Value);
}
[Fact]
public void Batch_Matches_Iterative()
{
int count = 1000;
var data = new double[count];
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
for (int i = 0; i < count; i++)
{
data[i] = gbm.Next().Close;
}
// Iterative
var slope = new Slope();
var iterativeResults = new double[count];
for (int i = 0; i < count; i++)
{
slope.Update(new TValue(DateTime.UtcNow, data[i]));
iterativeResults[i] = slope.Last.Value;
}
// Batch
var batchResults = new double[count];
Slope.Calculate(data, batchResults);
// Compare
for (int i = 0; i < count; i++)
{
Assert.Equal(iterativeResults[i], batchResults[i], precision: 9);
}
}
[Fact]
public void Update_TSeries_Matches_Iterative()
{
int count = 1000;
var data = new TSeries();
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
data.Add(new TValue(bar.Time, bar.Close));
}
// Iterative
var slope = new Slope();
var iterativeResults = new double[count];
for (int i = 0; i < count; i++)
{
slope.Update(data[i]);
iterativeResults[i] = slope.Last.Value;
}
// TSeries Batch
var slopeBatch = new Slope();
var batchSeries = slopeBatch.Update(data);
// Compare
for (int i = 0; i < count; i++)
{
Assert.Equal(iterativeResults[i], batchSeries[i].Value, precision: 9);
}
}
[Fact]
public void EventSubscription_Works()
{
var source = new TSeries();
var slope = new Slope(source);
source.Add(new TValue(DateTime.UtcNow, 10));
source.Add(new TValue(DateTime.UtcNow, 20));
Assert.True(slope.IsHot);
Assert.Equal(10, slope.Last.Value);
}
}
@@ -0,0 +1,138 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Slope using synthetic data with known mathematical results.
/// </summary>
public class SlopeValidationTests
{
[Fact]
public void LinearSequence_ProducesConstantSlope()
{
// Linear sequence: 0, 2, 4, 6, 8, 10 (slope = 2)
double[] data = [0, 2, 4, 6, 8, 10];
double[] expected = [0, 2, 2, 2, 2, 2]; // First is 0 (no history), rest are 2
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void ConstantSequence_ProducesZeroSlope()
{
// Constant sequence: 5, 5, 5, 5, 5 (slope = 0)
double[] data = [5, 5, 5, 5, 5];
double[] expected = [0, 0, 0, 0, 0];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void DecreasingSequence_ProducesNegativeSlope()
{
// Decreasing sequence: 10, 7, 4, 1, -2 (slope = -3)
double[] data = [10, 7, 4, 1, -2];
double[] expected = [0, -3, -3, -3, -3];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void QuadraticSequence_ProducesLinearSlope()
{
// Quadratic sequence: 0, 1, 4, 9, 16, 25 (x^2)
// Slope: n^2 - (n-1)^2 = 2n - 1 → 1, 3, 5, 7, 9
double[] data = [0, 1, 4, 9, 16, 25];
double[] expected = [0, 1, 3, 5, 7, 9];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void AlternatingSequence_ProducesAlternatingSlope()
{
// Alternating: 0, 10, 0, 10, 0
double[] data = [0, 10, 0, 10, 0];
double[] expected = [0, 10, -10, 10, -10];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void FibonacciSequence_ProducesCorrectSlope()
{
// Fibonacci: 1, 1, 2, 3, 5, 8, 13
// Slope: 0, 1, 1, 2, 3, 5
double[] data = [1, 1, 2, 3, 5, 8, 13];
double[] expected = [0, 0, 1, 1, 2, 3, 5];
var slope = new Slope();
for (int i = 0; i < data.Length; i++)
{
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void BatchCalculation_MatchesSyntheticData()
{
double[] data = [0, 2, 4, 6, 8, 10];
double[] expected = [0, 2, 2, 2, 2, 2];
double[] output = new double[data.Length];
Slope.Calculate(data, output);
for (int i = 0; i < data.Length; i++)
{
Assert.Equal(expected[i], output[i], precision: 9);
}
}
[Fact]
public void LargeLinearSequence_ProducesConstantSlope()
{
// Generate 1000 points with slope = 0.5
int count = 1000;
double[] data = new double[count];
for (int i = 0; i < count; i++)
{
data[i] = 100.0 + i * 0.5;
}
var slope = new Slope();
// First element - no previous value, slope = 0
slope.Update(new TValue(DateTime.UtcNow, data[0]));
Assert.Equal(0.0, slope.Last.Value, precision: 9);
// Rest should have constant slope of 0.5
for (int i = 1; i < count; i++)
{
slope.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(0.5, slope.Last.Value, precision: 9);
}
}
}
+296
View File
@@ -0,0 +1,296 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
namespace QuanTAlib;
/// <summary>
/// SLOPE: First Derivative (Rate of Change)
/// Measures the velocity of price movement - the instantaneous rate of change.
/// </summary>
/// <remarks>
/// The first derivative approximates velocity: how fast the value is changing.
///
/// Formula:
/// Slope_t = Value_t - Value_{t-1}
///
/// Key properties:
/// - O(1) streaming complexity
/// - Zero allocations in hot path
/// - SIMD-optimized batch calculation
/// </remarks>
[SkipLocalsInit]
public sealed class Slope : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double PrevValue, double LastValidValue, int Count);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
public override bool IsHot => _state.Count >= 2;
/// <summary>
/// Creates a new Slope (first derivative) indicator.
/// </summary>
public Slope()
{
Name = "Slope";
WarmupPeriod = 2;
_handler = Handle;
}
/// <summary>
/// Creates a new Slope indicator with event subscription.
/// </summary>
public Slope(ITValuePublisher source) : this()
{
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_state.LastValidValue = input;
return input;
}
return _state.LastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double result;
if (isNew)
{
_p_state = _state;
double val = GetValidValue(input.Value);
if (_state.Count >= 1)
{
result = val - _state.PrevValue;
}
else
{
result = 0.0;
}
_state.PrevValue = val;
_state.Count = Math.Min(_state.Count + 1, 2);
}
else
{
// Rollback for bar correction
_state.LastValidValue = _p_state.LastValidValue;
double val = GetValidValue(input.Value);
if (_p_state.Count >= 1)
{
result = val - _p_state.PrevValue;
}
else
{
result = 0.0;
}
_state.PrevValue = val;
_state.Count = Math.Max(_p_state.Count, 1);
}
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
int len = source.Count;
ReadOnlySpan<double> sourceValues = source.Values;
ReadOnlySpan<long> sourceTimes = source.Times;
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);
Calculate(sourceValues, vSpan);
sourceTimes.CopyTo(tSpan);
// Prime state with last value
if (len >= 1)
{
_state.PrevValue = double.IsFinite(sourceValues[len - 1]) ? sourceValues[len - 1] : _state.LastValidValue;
_state.Count = Math.Min(len, 2);
_state.LastValidValue = _state.PrevValue;
_p_state = _state;
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public override void Reset()
{
_state = default;
_p_state = default;
Last = default;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double val in source)
{
Update(new TValue(DateTime.MinValue, val));
}
}
public static TSeries Calculate(TSeries source)
{
var slope = new Slope();
return slope.Update(source);
}
/// <summary>
/// Calculates first derivative (slope) for a span.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
int len = source.Length;
if (len == 0) return;
// First element has no previous - set to 0
output[0] = 0.0;
if (len == 1) return;
int i = 1;
// Check if all values are finite before using SIMD
// SIMD paths don't handle NaN/Infinity properly
bool allFinite = !source.ContainsNonFinite();
// Only use SIMD if all values are finite
if (allFinite)
{
// AVX512: 8 doubles at once
if (Avx512F.IsSupported && len >= 9)
{
const int VectorWidth = 8;
int simdEnd = len - VectorWidth + 1;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
for (; i < simdEnd; i += VectorWidth)
{
var current = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var diff = Avx512F.Subtract(current, prev);
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// AVX: 4 doubles at once
else if (Avx.IsSupported && len >= 5)
{
const int VectorWidth = 4;
int simdEnd = len - VectorWidth + 1;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
for (; i < simdEnd; i += VectorWidth)
{
var current = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var prev = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var diff = Avx.Subtract(current, prev);
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// ARM64 Neon: 2 doubles at once
else if (AdvSimd.Arm64.IsSupported && len >= 3)
{
const int VectorWidth = 2;
int simdEnd = len - VectorWidth + 1;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
for (; i < simdEnd; i += VectorWidth)
{
var current = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var prev = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var diff = AdvSimd.Arm64.Subtract(current, prev);
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
}
// Scalar fallback for remaining elements
// Track last valid value forward to avoid O(n²) backward scanning
double lastValid = 0.0;
// Find first valid value if we're starting from the beginning
if (i == 1)
{
for (int k = 0; k < len; k++)
{
if (double.IsFinite(source[k]))
{
lastValid = source[k];
break;
}
}
}
else if (i > 1)
{
// We already processed some elements via SIMD, find last valid from processed
for (int k = i - 1; k >= 0; k--)
{
if (double.IsFinite(source[k]))
{
lastValid = source[k];
break;
}
}
}
double prevValid = lastValid;
for (; i < len; i++)
{
double curr = source[i];
double prev = source[i - 1];
// Handle NaN/Infinity using tracked last valid values
if (double.IsFinite(curr))
{
lastValid = curr;
}
else
{
curr = lastValid;
}
if (double.IsFinite(prev))
{
prevValid = prev;
}
else
{
prev = prevValid;
}
output[i] = curr - prev;
}
}
}
+147
View File
@@ -0,0 +1,147 @@
# SLOPE: First Derivative (Velocity)
> "The simplest measure of change reveals the most: is it going up, or going down?"
SLOPE measures the instantaneous rate of change—the velocity of a time series. As the first derivative, it answers the fundamental question: how fast is the value changing right now? A positive slope means ascending; negative means descending; zero means flat. This O(1) streaming implementation uses SIMD optimization for batch calculations and handles bar corrections via state rollback.
## Historical Context
The first derivative appears in Newton's calculus (1687) and forms the foundation of technical analysis. Every momentum indicator, every rate-of-change calculation, every velocity measure reduces to some form of first difference.
In discrete time series, the continuous derivative $\frac{dx}{dt}$ becomes the finite difference $\Delta x = x_t - x_{t-1}$. This simple subtraction underpins RSI's momentum, MACD's signal line, and every trend-following system that asks "which way is it moving?"
QuanTAlib implements SLOPE as a first-class indicator with full streaming support, SIMD batch optimization, and proper state management for bar corrections.
## Architecture & Physics
SLOPE is a memoryless differentiator with minimal state requirements:
### 1. First Difference Operation
The fundamental operation:
$$
S_t = V_t - V_{t-1}
$$
where $V_t$ is the current value and $V_{t-1}$ is the previous value.
### 2. State Management
State consists of:
- `PrevValue`: The previous input value
- `LastValidValue`: Last known finite value for NaN/Infinity substitution
- `Count`: Number of values processed (0, 1, or 2+)
The indicator becomes "hot" (fully warmed up) after 2 values.
### 3. Bar Correction via Rollback
When `isNew=false`, the indicator rolls back to the previous state before recalculating:
$$
\text{State}_{current} \leftarrow \text{State}_{previous}
$$
This enables real-time bar updates without corrupting the running calculation.
## Mathematical Foundation
### Discrete First Derivative
For a time series $V$:
$$
S_t = V_t - V_{t-1}
$$
This is the forward difference approximation of the derivative.
### Interpretation
| Slope Value | Meaning |
| :--- | :--- |
| $S > 0$ | Price ascending (bullish) |
| $S < 0$ | Price descending (bearish) |
| $S = 0$ | Price unchanged (consolidation) |
| $|S|$ large | Fast movement |
| $|S|$ small | Slow movement |
### Relationship to Higher Derivatives
SLOPE forms the basis of the derivative chain:
$$
\text{Accel}_t = \text{Slope}_t - \text{Slope}_{t-1}
$$
$$
\text{Jolt}_t = \text{Accel}_t - \text{Accel}_{t-1}
$$
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| SUB | 1 | 1 | 1 |
| MOV (state update) | 2 | 1 | 2 |
| CMP (IsFinite check) | 1 | 1 | 1 |
| **Total** | **4** | — | **~4 cycles** |
SLOPE is one of the fastest possible indicators—a single subtraction plus state bookkeeping.
### Batch Mode (512 values, SIMD)
| Architecture | Vector Width | Elements/Op | Total Ops (512 values) |
| :--- | :---: | :---: | :---: |
| AVX-512 | 512 bits | 8 doubles | 64 |
| AVX | 256 bits | 4 doubles | 128 |
| ARM64 Neon | 128 bits | 2 doubles | 256 |
| Scalar | 64 bits | 1 double | 512 |
**Batch efficiency (512 bars):**
| Mode | Cycles/bar | Total (512 bars) | Speedup |
| :--- | :---: | :---: | :---: |
| Scalar streaming | 4 | 2,048 | 1× |
| AVX-512 SIMD | 0.5 | 256 | 8× |
| AVX SIMD | 1 | 512 | 4× |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact finite difference |
| **Timeliness** | 10/10 | Zero lag (instantaneous) |
| **Smoothness** | 3/10 | Amplifies noise |
| **Computational Cost** | 10/10 | Single subtraction |
| **Memory** | 10/10 | ~48 bytes state |
## Validation
SLOPE is a fundamental operation. Validation confirms exact match with manual calculation.
| Library | Status | Notes |
| :--- | :---: | :--- |
| **TA-Lib** | N/A | Uses ROC (percent change) |
| **Skender** | N/A | Uses Slope regression |
| **Manual Calculation** | ✅ | Exact match |
## Common Pitfalls
1. **Noise Amplification**: First derivatives amplify high-frequency noise. A 1% price wiggle becomes a full slope reversal. Consider smoothing the input or output for noisy data.
2. **Scale Dependency**: SLOPE output depends on input scale. A $100 stock has 100× larger slopes than a $1 stock. Normalize if comparing across instruments.
3. **Warmup Period**: SLOPE requires 2 values to produce meaningful output. The first output is always 0.
4. **Using isNew Incorrectly**: When processing live ticks within the same bar, use `Update(value, isNew: false)`. When a new bar opens, use `isNew: true` (default).
5. **Memory Footprint**: ~48 bytes per instance. Negligible for most use cases.
## References
- Newton, Isaac. (1687). "Philosophiæ Naturalis Principia Mathematica."
- Numerical Methods: Finite Difference Approximations.
+62
View File
@@ -0,0 +1,62 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Slope, Linear Regression (SLOPE)", "SLOPE", overlay=false, precision=8)
//@function Calculates slope (linear regression)
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/slope.md
//@param src Source series to calculate slope from
//@param len Lookback period for calculation
//@returns Slope value properly calculated
slope(series float src, simple int len) =>
if len <= 1
runtime.error("Length must be greater than 1")
var float sumX = 0.0
var float sumY = 0.0
var float sumXY = 0.0
var float sumX2 = 0.0
var int validCount = 0
var array<float> x_values = array.new_float(len)
var array<float> y_values = array.new_float(len)
var int head = 0
var int internal_time_counter = 0
if internal_time_counter >= len
float oldX = array.get(x_values, head)
float oldY = array.get(y_values, head)
if not na(oldY)
sumX := sumX - oldX
sumY := sumY - oldY
sumXY := sumXY - oldX * oldY
sumX2 := sumX2 - oldX * oldX
validCount := validCount - 1
float currentX = internal_time_counter
float currentY = src
array.set(x_values, head, currentX)
array.set(y_values, head, currentY)
if not na(currentY)
sumX := sumX + currentX
sumY := sumY + currentY
sumXY := sumXY + currentX * currentY
sumX2 := sumX2 + currentX * currentX
validCount := validCount + 1
head := (head + 1) % len
internal_time_counter := internal_time_counter + 1
float calculatedSlope = na
if validCount >= 2
float n = validCount
float divisor = n * sumX2 - sumX * sumX
if divisor != 0.0
calculatedSlope := (n * sumXY - sumX * sumY) / divisor
calculatedSlope
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=2)
i_source = input.source(close, "Source")
// Calculation
s = slope(i_source, i_period)
// Plot
plot(s, "Slope", color=color.yellow, linewidth=2)