mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 05:28:05 +00:00
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:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user