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
+218
View File
@@ -0,0 +1,218 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class JerkIndicatorTests
{
[Fact]
public void JerkIndicator_Constructor_SetsDefaults()
{
var indicator = new JerkIndicator();
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("JERK - Third Derivative", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.False(indicator.OnBackGround);
}
[Fact]
public void JerkIndicator_MinHistoryDepths_IsFour()
{
var indicator = new JerkIndicator();
Assert.Equal(4, indicator.MinHistoryDepths);
}
[Fact]
public void JerkIndicator_ShortName_IsJerk()
{
var indicator = new JerkIndicator();
Assert.Equal("JERK", indicator.ShortName);
}
[Fact]
public void JerkIndicator_Initialize_CreatesLineSeries()
{
var indicator = new JerkIndicator();
indicator.Initialize();
Assert.Equal(2, indicator.LinesSeries.Count);
Assert.Equal("Jerk", indicator.LinesSeries[0].Name);
Assert.Equal("Zero", indicator.LinesSeries[1].Name);
}
[Fact]
public void JerkIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new JerkIndicator();
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 JerkIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new JerkIndicator();
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 JerkIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new JerkIndicator();
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 JerkIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new JerkIndicator();
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 JerkIndicator_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 JerkIndicator { 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 JerkIndicator_ShowColdValues_False_SetsNaN()
{
var indicator = new JerkIndicator { 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 JerkIndicator_QuadraticTrend_ProducesZeroJerk()
{
var indicator = new JerkIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Quadratic trend: constant acceleration = zero jerk
for (int i = 0; i < 10; i++)
{
double price = 100 + i * i; // constant accel = 2
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastJerk = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, lastJerk, 6);
}
[Fact]
public void JerkIndicator_CubicTrend_ProducesConstantJerk()
{
var indicator = new JerkIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Cubic trend: changing acceleration = non-zero jerk
for (int i = 0; i < 10; i++)
{
double price = 100 + i * i * i; // cubic growth
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastJerk = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastJerk != 0);
}
[Fact]
public void JerkIndicator_LinearTrend_ProducesZeroJerk()
{
var indicator = new JerkIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Linear trend: zero accel = zero jerk
for (int i = 0; i < 10; i++)
{
double price = 100 + i * 5; // constant slope
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastJerk = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, lastJerk, 6);
}
}
+71
View File
@@ -0,0 +1,71 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// JERK (Third Derivative) Quantower indicator.
/// Measures the rate of change of acceleration - derivative of accel.
/// </summary>
public class JerkIndicator : Indicator, IWatchlistIndicator
{
[DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Jerk? _jerk;
private Func<IHistoryItem, double>? _selector;
public int MinHistoryDepths => 4;
public override string ShortName => "JERK";
public JerkIndicator()
{
Name = "JERK - Third Derivative";
Description = "Measures rate of change of acceleration - derivative of accel";
SeparateWindow = true;
OnBackGround = false;
}
protected override void OnInit()
{
_jerk = new Jerk();
_selector = Source.GetPriceSelector();
AddLineSeries(new LineSeries("Jerk", Momentum, 2, LineStyle.Histogramm));
AddLineSeries(new LineSeries("Zero", Color.Gray, 1, LineStyle.Dot));
}
protected override void OnUpdate(UpdateArgs args)
{
if (_jerk == null || _selector == null) return;
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
bool isNew = args.IsNewBar();
TValue input = new(item.TimeLeft, value);
_jerk.Update(input, isNew);
bool isHot = _jerk.IsHot;
LinesSeries[0].SetValue(_jerk.Last.Value, isHot, ShowColdValues);
LinesSeries[1].SetValue(0);
if (isHot || ShowColdValues)
{
double jerk = _jerk.Last.Value;
Color color;
if (jerk > 0)
color = Color.Green;
else if (jerk < 0)
color = Color.Red;
else
color = Color.Gray;
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
}
}
}
+305
View File
@@ -0,0 +1,305 @@
namespace QuanTAlib.Tests;
public class JerkTests
{
[Fact]
public void Properties_Accessible()
{
var jerk = new Jerk();
Assert.Equal(0, jerk.Last.Value);
Assert.False(jerk.IsHot);
Assert.Contains("Jerk", jerk.Name, StringComparison.Ordinal);
Assert.Equal(4, jerk.WarmupPeriod);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var jerk = new Jerk();
jerk.Update(new TValue(DateTime.UtcNow, 10));
jerk.Update(new TValue(DateTime.UtcNow, 20));
jerk.Update(new TValue(DateTime.UtcNow, 30));
jerk.Update(new TValue(DateTime.UtcNow, 40));
double valueBefore = jerk.Last.Value;
// Update with isNew=false should change the result
jerk.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
double valueAfter = jerk.Last.Value;
Assert.NotEqual(valueBefore, valueAfter);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var jerk = new Jerk();
jerk.Update(new TValue(DateTime.UtcNow, 10));
jerk.Update(new TValue(DateTime.UtcNow, 20));
jerk.Update(new TValue(DateTime.UtcNow, 30));
jerk.Update(new TValue(DateTime.UtcNow, 40));
var result = jerk.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var jerk = new Jerk();
jerk.Update(new TValue(DateTime.UtcNow, 10));
jerk.Update(new TValue(DateTime.UtcNow, 20));
jerk.Update(new TValue(DateTime.UtcNow, 30));
jerk.Update(new TValue(DateTime.UtcNow, 40));
var resultPosInf = jerk.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultPosInf.Value));
var resultNegInf = jerk.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultNegInf.Value));
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var jerk = new Jerk();
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);
jerk.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double stateAfterTen = jerk.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
jerk.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalResult = jerk.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>(() =>
Jerk.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];
Jerk.Calculate(tValues, batchOutput);
double expected = batchOutput[^1];
// 2. Streaming Mode
var streamingInd = new Jerk();
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 3. TSeries Batch Mode
var batchSeriesResult = Jerk.Calculate(series);
double tseriesResult = batchSeriesResult.Last.Value;
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, tseriesResult, precision: 9);
}
[Fact]
public void Calculation_KnownValues()
{
// jerk[i] = source[i] - 3*source[i-1] + 3*source[i-2] - source[i-3]
// Data: 10, 20, 35, 40, 42, 50
// jerk[0] = 0 (insufficient history)
// jerk[1] = 0 (insufficient history)
// jerk[2] = 0 (insufficient history)
// jerk[3] = 40 - 3*35 + 3*20 - 10 = 40 - 105 + 60 - 10 = -15
// jerk[4] = 42 - 3*40 + 3*35 - 20 = 42 - 120 + 105 - 20 = 7
// jerk[5] = 50 - 3*42 + 3*40 - 35 = 50 - 126 + 120 - 35 = 9
double[] data = [10, 20, 35, 40, 42, 50];
double[] expected = [0, 0, 0, -15, 7, 9];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var jerk = new Jerk();
Assert.False(jerk.IsHot);
jerk.Update(new TValue(DateTime.UtcNow, 10));
Assert.False(jerk.IsHot);
jerk.Update(new TValue(DateTime.UtcNow, 20));
Assert.False(jerk.IsHot);
jerk.Update(new TValue(DateTime.UtcNow, 30));
Assert.False(jerk.IsHot);
jerk.Update(new TValue(DateTime.UtcNow, 40));
Assert.True(jerk.IsHot);
}
[Fact]
public void Reset_ClearsState()
{
var jerk = new Jerk();
for (int i = 0; i < 10; i++)
{
jerk.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(jerk.IsHot);
jerk.Reset();
Assert.False(jerk.IsHot);
Assert.Equal(0, jerk.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 jerk = new Jerk();
var iterativeResults = new double[count];
for (int i = 0; i < count; i++)
{
jerk.Update(new TValue(DateTime.UtcNow, data[i]));
iterativeResults[i] = jerk.Last.Value;
}
// Batch
var batchResults = new double[count];
Jerk.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 jerk = new Jerk();
var iterativeResults = new double[count];
for (int i = 0; i < count; i++)
{
jerk.Update(data[i]);
iterativeResults[i] = jerk.Last.Value;
}
// TSeries Batch
var jerkBatch = new Jerk();
var batchSeries = jerkBatch.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 jerk = new Jerk(source);
source.Add(new TValue(DateTime.UtcNow, 10));
source.Add(new TValue(DateTime.UtcNow, 20));
source.Add(new TValue(DateTime.UtcNow, 35));
source.Add(new TValue(DateTime.UtcNow, 40));
Assert.True(jerk.IsHot);
// jerk = 40 - 3*35 + 3*20 - 10 = 40 - 105 + 60 - 10 = -15
Assert.Equal(-15, jerk.Last.Value);
}
[Fact]
public void DerivativeChain_MatchesDirectCalculation()
{
// Jerk should equal Accel of Slope
// Also: Jerk[i] = Accel[i] - Accel[i-1]
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 456);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Direct Jerk calculation
var jerk = new Jerk();
var jerkResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
jerk.Update(series[i]);
jerkResults[i] = jerk.Last.Value;
}
// Chain: Slope -> Accel (should match Jerk after accounting for warmup)
var slope = new Slope();
var accel = new Accel();
var chainResults = new double[series.Count];
for (int i = 0; i < series.Count; i++)
{
var slopeVal = slope.Update(series[i]);
var accelOfSlope = accel.Update(slopeVal);
chainResults[i] = accelOfSlope.Value;
}
// Compare from index 3 onwards (when both have sufficient warmup)
for (int i = 3; i < series.Count; i++)
{
Assert.Equal(jerkResults[i], chainResults[i], precision: 9);
}
}
}
+165
View File
@@ -0,0 +1,165 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Jerk using synthetic data with known mathematical results.
/// </summary>
public class JerkValidationTests
{
[Fact]
public void CubicSequence_ProducesConstantJerk()
{
// Cubic sequence: 0, 1, 8, 27, 64, 125 (x^3)
// First diff (slope): 1, 7, 19, 37, 61
// Second diff (accel): 6, 12, 18, 24
// Third diff (jerk): 6, 6, 6 (constant for cubic)
double[] data = [0, 1, 8, 27, 64, 125];
double[] expected = [0, 0, 0, 6, 6, 6]; // First three are warmup (0), rest are 6
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void QuadraticSequence_ProducesZeroJerk()
{
// Quadratic sequence: 0, 1, 4, 9, 16, 25 (x^2)
// Accel = 2 (constant), so Jerk = 0
double[] data = [0, 1, 4, 9, 16, 25];
double[] expected = [0, 0, 0, 0, 0, 0];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void LinearSequence_ProducesZeroJerk()
{
// Linear sequence: 0, 2, 4, 6, 8, 10 (slope = 2, accel = 0, jerk = 0)
double[] data = [0, 2, 4, 6, 8, 10];
double[] expected = [0, 0, 0, 0, 0, 0];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void ConstantSequence_ProducesZeroJerk()
{
// Constant sequence: 5, 5, 5, 5, 5 (all derivatives = 0)
double[] data = [5, 5, 5, 5, 5];
double[] expected = [0, 0, 0, 0, 0];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void QuarticSequence_ProducesLinearJerk()
{
// Quartic sequence: 0, 1, 16, 81, 256, 625 (x^4)
// First diff: 1, 15, 65, 175, 369
// Second diff: 14, 50, 110, 194
// Third diff (jerk): 36, 60, 84 (linear, step of 24)
double[] data = [0, 1, 16, 81, 256, 625];
double[] expected = [0, 0, 0, 36, 60, 84];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void NegativeCubic_ProducesNegativeJerk()
{
// Negative cubic: -x³ → 0, -1, -8, -27, -64
// Jerk = -6 (constant)
double[] data = [0, -1, -8, -27, -64];
double[] expected = [0, 0, 0, -6, -6];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void AlternatingSequence_ProducesAlternatingJerk()
{
// Alternating: 0, 10, 0, 10, 0, 10
// Slope: 10, -10, 10, -10, 10
// Accel: -20, 20, -20, 20
// Jerk: 40, -40, 40
double[] data = [0, 10, 0, 10, 0, 10];
double[] expected = [0, 0, 0, 40, -40, 40];
var jerk = new Jerk();
for (int i = 0; i < data.Length; i++)
{
var result = jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(expected[i], result.Value, precision: 9);
}
}
[Fact]
public void BatchCalculation_MatchesSyntheticData()
{
double[] data = [0, 1, 8, 27, 64, 125];
double[] expected = [0, 0, 0, 6, 6, 6];
double[] output = new double[data.Length];
Jerk.Calculate(data, output);
for (int i = 0; i < data.Length; i++)
{
Assert.Equal(expected[i], output[i], precision: 9);
}
}
[Fact]
public void LargeCubicSequence_ProducesConstantJerk()
{
// Generate 1000 points: f(n) = n³ with coefficient 1/6 → jerk = 1
// f(n) = n³/6, f'(n) = n²/2, f''(n) = n, f'''(n) = 1
// Discrete: jerk = 1 (after warmup)
// Note: Large cubic values accumulate floating-point error, use precision: 8
int count = 1000;
double[] data = new double[count];
for (int i = 0; i < count; i++)
{
data[i] = (double)(i * i * i) / 6.0;
}
var jerk = new Jerk();
// Skip warmup period (first 3 bars)
_ = jerk.Update(new TValue(DateTime.UtcNow, data[0]));
_ = jerk.Update(new TValue(DateTime.UtcNow, data[1]));
_ = jerk.Update(new TValue(DateTime.UtcNow, data[2]));
for (int i = 3; i < count; i++)
{
jerk.Update(new TValue(DateTime.UtcNow, data[i]));
Assert.Equal(1.0, jerk.Last.Value, precision: 6);
}
}
}
+363
View File
@@ -0,0 +1,363 @@
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>
/// JERK: Third Derivative (Rate of Acceleration Change)
/// Measures how fast the acceleration is changing - the "jerk" in physics terms.
/// </summary>
/// <remarks>
/// The third derivative approximates jerk: the rate of change of acceleration.
///
/// Formula:
/// Jerk_t = Accel_t - Accel_{t-1}
/// = (Value_t - 2*Value_{t-1} + Value_{t-2}) - (Value_{t-1} - 2*Value_{t-2} + Value_{t-3})
/// = Value_t - 3*Value_{t-1} + 3*Value_{t-2} - Value_{t-3}
///
/// Key properties:
/// - O(1) streaming complexity
/// - Zero allocations in hot path
/// - SIMD-optimized batch calculation
/// </remarks>
[SkipLocalsInit]
public sealed class Jerk : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(double Prev1, double Prev2, double Prev3, double LastValidValue, int Count);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _handler;
public override bool IsHot => _state.Count >= 4;
/// <summary>
/// Creates a new Jerk (third derivative) indicator.
/// </summary>
public Jerk()
{
Name = "Jerk";
WarmupPeriod = 4;
_handler = Handle;
}
/// <summary>
/// Creates a new Jerk indicator with event subscription.
/// </summary>
public Jerk(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 >= 3)
{
// jerk = val - 3*prev1 + 3*prev2 - prev3
// Using FMA: val - 3*prev1 + 3*prev2 - prev3
// = FMA(-3, prev1, val) + FMA(3, prev2, -prev3)
double term1 = Math.FusedMultiplyAdd(-3.0, _state.Prev1, val);
double term2 = Math.FusedMultiplyAdd(3.0, _state.Prev2, -_state.Prev3);
result = term1 + term2;
}
else
{
result = 0.0;
}
// Shift history
_state.Prev3 = _state.Prev2;
_state.Prev2 = _state.Prev1;
_state.Prev1 = val;
_state.Count = Math.Min(_state.Count + 1, 4);
}
else
{
// Rollback for bar correction
_state.LastValidValue = _p_state.LastValidValue;
double val = GetValidValue(input.Value);
if (_p_state.Count >= 3)
{
double term1 = Math.FusedMultiplyAdd(-3.0, _p_state.Prev1, val);
double term2 = Math.FusedMultiplyAdd(3.0, _p_state.Prev2, -_p_state.Prev3);
result = term1 + term2;
}
else
{
result = 0.0;
}
// Update current state from previous (don't shift)
_state.Prev3 = _p_state.Prev3;
_state.Prev2 = _p_state.Prev2;
_state.Prev1 = 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;
// Cache source spans ONCE before any operations to avoid repeated property access
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 three values using cached span
if (len >= 3)
{
double v1 = double.IsFinite(sourceValues[len - 1]) ? sourceValues[len - 1] : _state.LastValidValue;
double v2 = double.IsFinite(sourceValues[len - 2]) ? sourceValues[len - 2] : v1;
double v3 = double.IsFinite(sourceValues[len - 3]) ? sourceValues[len - 3] : v2;
_state.Prev1 = v1;
_state.Prev2 = v2;
_state.Prev3 = v3;
_state.LastValidValue = v1;
_state.Count = Math.Min(len, 4);
_p_state = _state;
}
else if (len == 2)
{
double v1 = double.IsFinite(sourceValues[1]) ? sourceValues[1] : _state.LastValidValue;
double v2 = double.IsFinite(sourceValues[0]) ? sourceValues[0] : v1;
_state.Prev1 = v1;
_state.Prev2 = v2;
_state.LastValidValue = v1;
_state.Count = 2;
_p_state = _state;
}
else if (len == 1)
{
double v1 = double.IsFinite(sourceValues[0]) ? sourceValues[0] : _state.LastValidValue;
_state.Prev1 = v1;
_state.LastValidValue = v1;
_state.Count = 1;
_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 jerk = new Jerk();
return jerk.Update(source);
}
/// <summary>
/// Calculates third derivative (jerk) for a span.
/// jerk[i] = source[i] - 3*source[i-1] + 3*source[i-2] - source[i-3]
/// </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 three elements have insufficient history
output[0] = 0.0;
if (len == 1) return;
output[1] = 0.0;
if (len == 2) return;
output[2] = 0.0;
if (len == 3) return;
int i = 3;
// Check for non-finite values before using SIMD (SIMD doesn't handle NaN properly)
bool allFinite = !source.ContainsNonFinite();
// AVX512: 8 doubles at once (only if all values are finite)
if (allFinite && Avx512F.IsSupported && len >= 11)
{
var three = Vector512.Create(3.0);
var negThree = Vector512.Create(-3.0);
const int VectorWidth = 8;
int simdEnd = len - ((len - 3) % VectorWidth);
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 prev1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var prev2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 2));
var prev3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 3));
// jerk = current - 3*prev1 + 3*prev2 - prev3
// Using FMA: FMA(-3, prev1, current) + FMA(3, prev2, -prev3)
var term1 = Avx512F.FusedMultiplyAdd(negThree, prev1, current);
var negPrev3 = Avx512F.Subtract(Vector512<double>.Zero, prev3);
var term2 = Avx512F.FusedMultiplyAdd(three, prev2, negPrev3);
var result = Avx512F.Add(term1, term2);
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// AVX2 with FMA: 4 doubles at once (only if all values are finite)
else if (allFinite && Fma.IsSupported && len >= 7)
{
var three = Vector256.Create(3.0);
var negThree = Vector256.Create(-3.0);
const int VectorWidth = 4;
int simdEnd = len - ((len - 3) % VectorWidth);
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 prev1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var prev2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 2));
var prev3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 3));
// jerk = current - 3*prev1 + 3*prev2 - prev3
// Using FMA: FMA(-3, prev1, current) + FMA(3, prev2, -prev3)
var term1 = Fma.MultiplyAdd(negThree, prev1, current);
var negPrev3 = Avx.Subtract(Vector256<double>.Zero, prev3);
var term2 = Fma.MultiplyAdd(three, prev2, negPrev3);
var result = Avx.Add(term1, term2);
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// AVX fallback (no FMA): 4 doubles at once (only if all values are finite)
else if (allFinite && Avx.IsSupported && len >= 7)
{
var three = Vector256.Create(3.0);
const int VectorWidth = 4;
int simdEnd = len - ((len - 3) % VectorWidth);
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 prev1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var prev2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 2));
var prev3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 3));
var threeTimesP1 = Avx.Multiply(three, prev1);
var threeTimesP2 = Avx.Multiply(three, prev2);
var result = Avx.Subtract(current, threeTimesP1);
result = Avx.Add(result, threeTimesP2);
result = Avx.Subtract(result, prev3);
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// ARM64 Neon with FMA: 2 doubles at once (only if all values are finite)
else if (allFinite && AdvSimd.Arm64.IsSupported && len >= 5)
{
var three = Vector128.Create(3.0);
var negThree = Vector128.Create(-3.0);
const int VectorWidth = 2;
int simdEnd = len - ((len - 3) % VectorWidth);
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 prev1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
var prev2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 2));
var prev3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 3));
// jerk = current - 3*prev1 + 3*prev2 - prev3
// Using FMA: FMA(-3, prev1, current) + FMA(3, prev2, -prev3)
var term1 = AdvSimd.Arm64.FusedMultiplyAdd(current, negThree, prev1);
var negPrev3 = AdvSimd.Arm64.Subtract(Vector128<double>.Zero, prev3);
var term2 = AdvSimd.Arm64.FusedMultiplyAdd(negPrev3, three, prev2);
var result = AdvSimd.Arm64.Add(term1, term2);
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
}
}
// Scalar fallback for remaining elements
// Initialize prev values from actual data at positions i-1, i-2, i-3
for (; i < len; i++)
{
double curr = source[i];
double p1 = source[i - 1];
double p2 = source[i - 2];
double p3 = source[i - 3];
// Handle NaN/Infinity by substitution (find first finite value)
double fallback = FindFinite(curr, p1, p2, p3);
if (!double.IsFinite(curr)) curr = fallback;
if (!double.IsFinite(p1)) p1 = fallback;
if (!double.IsFinite(p2)) p2 = fallback;
if (!double.IsFinite(p3)) p3 = fallback;
// jerk = curr - 3*prev1 + 3*prev2 - prev3
double term1 = Math.FusedMultiplyAdd(-3.0, p1, curr);
double term2 = Math.FusedMultiplyAdd(3.0, p2, -p3);
output[i] = term1 + term2;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double FindFinite(double a, double b, double c, double d)
{
if (double.IsFinite(a)) return a;
if (double.IsFinite(b)) return b;
if (double.IsFinite(c)) return c;
if (double.IsFinite(d)) return d;
return 0.0;
}
}
+198
View File
@@ -0,0 +1,198 @@
# JERK: Third Derivative
> "Acceleration tells you the trend is changing. Jerk tells you that change is itself changing—the earliest possible warning."
JERK measures the rate of change of acceleration—called "jerk" in physics. As the third derivative, it detects changes in momentum dynamics before they appear in acceleration, velocity, or price. A positive jerk means acceleration is increasing; negative means acceleration is decreasing. This O(1) streaming implementation uses dual FMA optimization and SIMD batch processing for four-point calculations.
## Historical Context
The third derivative (jerk) appears in mechanical engineering, robotics, and ride comfort analysis. Roller coasters are designed to minimize jerk; elevators smooth their motion to reduce it. In financial markets, jerk reveals sudden shifts in how fast the trend is accelerating or decelerating.
While first and second derivatives see wide use in technical analysis (momentum, ROC, acceleration indicators), the third derivative remains underutilized. This is partly computational—four consecutive points are needed—and partly interpretive: jerk is abstract. Yet it provides the earliest mathematical signal of trend character change.
Consider: price is rising, acceleration is positive (strong uptrend). If jerk turns negative, acceleration will soon decrease, then velocity will peak, then price will top. Jerk leads the entire sequence.
QuanTAlib implements JERK as the discrete third difference with dual FMA optimization, SIMD batch processing, and full bar correction support.
## Architecture & Physics
JERK computes the third finite difference with four-point history:
### 1. Third Difference Operation
The fundamental operation:
$$
J_t = V_t - 3V_{t-1} + 3V_{t-2} - V_{t-3}
$$
This is algebraically equivalent to:
$$
J_t = A_t - A_{t-1}
$$
where $A$ is the second derivative (acceleration).
### 2. Dual FMA Optimization
The formula uses two Fused Multiply-Add operations:
$$
\text{term}_1 = \text{FMA}(-3, V_{t-1}, V_t)
$$
$$
\text{term}_2 = \text{FMA}(3, V_{t-2}, -V_{t-3})
$$
$$
J_t = \text{term}_1 + \text{term}_2
$$
This structure reduces rounding error and leverages pipelined FMA units on modern CPUs.
### 3. State Management
State consists of:
- `Prev1`: The previous input value $V_{t-1}$
- `Prev2`: The value before that $V_{t-2}$
- `Prev3`: The value before that $V_{t-3}$
- `LastValidValue`: Last known finite value for NaN/Infinity substitution
- `Count`: Number of values processed (0, 1, 2, 3, or 4+)
The indicator becomes "hot" (fully warmed up) after 4 values.
## Mathematical Foundation
### Discrete Third Derivative
For a time series $V$:
$$
J_t = \frac{d^3V}{dt^3} \approx V_t - 3V_{t-1} + 3V_{t-2} - V_{t-3}
$$
This is the forward difference approximation of the third derivative.
### Binomial Coefficients
The coefficients $(1, -3, 3, -1)$ are the alternating binomial coefficients for $n=3$:
$$
\binom{3}{0} = 1, \quad -\binom{3}{1} = -3, \quad \binom{3}{2} = 3, \quad -\binom{3}{3} = -1
$$
### Derivative Chain
JERK completes the derivative hierarchy:
$$
\text{Slope}_t = V_t - V_{t-1}
$$
$$
\text{Accel}_t = V_t - 2V_{t-1} + V_{t-2}
$$
$$
\text{Jerk}_t = V_t - 3V_{t-1} + 3V_{t-2} - V_{t-3}
$$
### Interpretation Matrix
| Jerk | Accel | Slope | Meaning |
| :--- | :--- | :--- | :--- |
| $J > 0$ | $A > 0$ | $S > 0$ | Uptrend strengthening at increasing rate |
| $J < 0$ | $A > 0$ | $S > 0$ | Uptrend strengthening but rate slowing |
| $J > 0$ | $A < 0$ | $S > 0$ | Uptrend weakening but rate of weakening slowing |
| $J < 0$ | $A < 0$ | $S > 0$ | Uptrend weakening at increasing rate |
| $J > 0$ | $A < 0$ | $S < 0$ | Downtrend strengthening but rate slowing |
| $J < 0$ | $A < 0$ | $S < 0$ | Downtrend strengthening at increasing rate |
| $J > 0$ | $A > 0$ | $S < 0$ | Downtrend weakening at increasing rate |
| $J < 0$ | $A > 0$ | $S < 0$ | Downtrend weakening but rate slowing |
### Inflection Detection
Jerk zero-crossings can indicate second-order inflection points:
$$
J_t \times J_{t-1} < 0 \implies \text{Acceleration inflection point}
$$
This precedes the acceleration zero-crossing, which precedes the velocity peak/trough.
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| FMA | 2 | 4 | 8 |
| ADD | 1 | 1 | 1 |
| NEG | 1 | 1 | 1 |
| MOV (state update) | 4 | 1 | 4 |
| CMP (IsFinite check) | 1 | 1 | 1 |
| **Total** | **9** | — | **~15 cycles** |
### 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 | 15 | 7,680 | 1× |
| AVX-512 SIMD | 1.9 | 973 | 8× |
| AVX SIMD | 3.8 | 1,946 | 4× |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact finite difference |
| **Timeliness** | 10/10 | Zero lag (instantaneous) |
| **Smoothness** | 1/10 | Extreme noise amplification |
| **Computational Cost** | 10/10 | Dual FMA + bookkeeping |
| **Memory** | 10/10 | ~80 bytes state |
## Validation
JERK is a fundamental operation. Validation confirms exact match with manual calculation and derivative chain composition.
| Library | Status | Notes |
| :--- | :---: | :--- |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Manual Calculation** | ✅ | Exact match |
| **Derivative Chain** | ✅ | Jerk = Accel - Accel_{t-1} matches |
## Common Pitfalls
1. **Catastrophic Noise Sensitivity**: Third derivatives amplify noise cubically. A 1% random wiggle becomes a wild jerk spike. Pre-smooth the input significantly (14+ period EMA minimum) before computing JERK.
2. **Scale Dependency**: JERK output scales with input magnitude cubed. A $100 stock has 1,000,000× larger jerks than a $1 stock. Normalization is essential for cross-instrument comparison.
3. **Warmup Period**: JERK requires 4 values to produce meaningful output. The first three outputs are always 0.
4. **Abstract Interpretation**: Jerk doesn't have an intuitive physical meaning for most traders. Use it as an early warning signal, not a direct trading trigger.
5. **Lead Time vs. Reliability**: Jerk provides the earliest signal but is also the most prone to false signals. Combine with lower derivatives for confirmation.
6. **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).
7. **Memory Footprint**: ~80 bytes per instance. Negligible for most use cases.
8. **Derivative Chain Verification**: JERK should equal the difference of consecutive ACCEL values. Use this identity to verify implementation correctness.
## References
- Newton, Isaac. (1687). "Philosophiæ Naturalis Principia Mathematica."
- Numerical Methods: Finite Difference Approximations.
- Eager, David et al. (2016). "Beyond velocity and acceleration: jerk, snap and higher derivatives." European Journal of Physics.
+113
View File
@@ -0,0 +1,113 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Acceleration, Slope of Slope (JERK)", "JERK", overlay=false, precision=8)
//@function Calculates jerk (slope of slope of slope)
//@param src Source series to calculate slope from
//@param len Lookback period for calculation
//@returns jerk
jerk(series float src, simple int len1) =>
if len1 <= 1
runtime.error("Length 1 for first slope calculation must be greater than 1")
var float sumX1 = 0.0, var float sumY1 = 0.0, var float sumXY1 = 0.0, var float sumX21 = 0.0
var int validCount1 = 0
var array<float> x_values1 = array.new_float(len1)
var array<float> y_values1 = array.new_float(len1)
var int head1 = 0
var int internal_time_counter1 = 0
if internal_time_counter1 >= len1
float oldX1 = array.get(x_values1, head1)
float oldY1 = array.get(y_values1, head1)
if not na(oldY1)
sumX1 := sumX1 - oldX1, sumY1 := sumY1 - oldY1
sumXY1 := sumXY1 - oldX1 * oldY1, sumX21 := sumX21 - oldX1 * oldX1
validCount1 := validCount1 - 1
float currentX1 = internal_time_counter1
float currentY1 = src
array.set(x_values1, head1, currentX1)
array.set(y_values1, head1, currentY1)
if not na(currentY1)
sumX1 := sumX1 + currentX1, sumY1 := sumY1 + currentY1
sumXY1 := sumXY1 + currentX1 * currentY1, sumX21 := sumX21 + currentX1 * currentX1
validCount1 := validCount1 + 1
head1 := (head1 + 1) % len1
internal_time_counter1 := internal_time_counter1 + 1
float current_slope1 = na
if validCount1 >= 2
float n1 = validCount1
float divisor1 = n1 * sumX21 - sumX1 * sumX1
if divisor1 != 0.0
current_slope1 := (n1 * sumXY1 - sumX1 * sumY1) / divisor1
var float sumX2 = 0.0, var float sumY2 = 0.0, var float sumXY2 = 0.0, var float sumX22 = 0.0
var int validCount2 = 0
var array<float> x_values2 = array.new_float(len1)
var array<float> y_values2 = array.new_float(len1)
var int head2 = 0
var int internal_time_counter2 = 0
if internal_time_counter2 >= len1
float oldX2 = array.get(x_values2, head2)
float oldY2 = array.get(y_values2, head2)
if not na(oldY2)
sumX2 := sumX2 - oldX2, sumY2 := sumY2 - oldY2
sumXY2 := sumXY2 - oldX2 * oldY2, sumX22 := sumX22 - oldX2 * oldX2
validCount2 := validCount2 - 1
float currentX2 = internal_time_counter2
float currentY2 = current_slope1
array.set(x_values2, head2, currentX2)
array.set(y_values2, head2, currentY2)
if not na(currentY2)
sumX2 := sumX2 + currentX2, sumY2 := sumY2 + currentY2
sumXY2 := sumXY2 + currentX2 * currentY2, sumX22 := sumX22 + currentX2 * currentX2
validCount2 := validCount2 + 1
head2 := (head2 + 1) % len1
internal_time_counter2 := internal_time_counter2 + 1
float current_accel = na
if validCount2 >= 2
float n2 = validCount2
float divisor2 = n2 * sumX22 - sumX2 * sumX2
if divisor2 != 0.0
current_accel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2
var float sumX3 = 0.0, var float sumY3 = 0.0, var float sumXY3 = 0.0, var float sumX23 = 0.0
var int validCount3 = 0
var array<float> x_values3 = array.new_float(len1)
var array<float> y_values3 = array.new_float(len1)
var int head3 = 0
var int internal_time_counter3 = 0
if internal_time_counter3 >= len1
float oldX3 = array.get(x_values3, head3)
float oldY3 = array.get(y_values3, head3)
if not na(oldY3)
sumX3 := sumX3 - oldX3, sumY3 := sumY3 - oldY3
sumXY3 := sumXY3 - oldX3 * oldY3, sumX23 := sumX23 - oldX3 * oldX3
validCount3 := validCount3 - 1
float currentX3 = internal_time_counter3
float currentY3 = current_accel
array.set(x_values3, head3, currentX3)
array.set(y_values3, head3, currentY3)
if not na(currentY3)
sumX3 := sumX3 + currentX3, sumY3 := sumY3 + currentY3
sumXY3 := sumXY3 + currentX3 * currentY3, sumX23 := sumX23 + currentX3 * currentX3
validCount3 := validCount3 + 1
head3 := (head3 + 1) % len1
internal_time_counter3 := internal_time_counter3 + 1
float calculatedjerk = na
if validCount3 >= 2
float n3 = validCount3
float divisor3 = n3 * sumX23 - sumX3 * sumX3
if divisor3 != 0.0
calculatedjerk := (n3 * sumXY3 - sumX3 * sumY3) / divisor3
calculatedjerk
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=2)
i_source = input.source(close, "Source")
// Calculation
a = jerk(i_source, i_period)
// Plot
plot(a, "jerk", color=color.yellow, linewidth=2)