mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +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 AccelIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AccelIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("ACCEL - Second Derivative (Acceleration)", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.False(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_MinHistoryDepths_IsThree()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
Assert.Equal(3, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_ShortName_IsAccel()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
Assert.Equal("ACCEL", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Accel", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Zero", indicator.LinesSeries[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
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 AccelIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
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 AccelIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
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 AccelIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
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 AccelIndicator_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 AccelIndicator { 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 AccelIndicator_ShowColdValues_False_SetsNaN()
|
||||
{
|
||||
var indicator = new AccelIndicator { 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 AccelIndicator_LinearTrend_ProducesZeroAcceleration()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Linear trend: constant slope = zero acceleration
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + i * 5; // constant +5 per bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastAccel = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0, lastAccel, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_AcceleratingTrend_ProducesPositiveAcceleration()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Quadratic trend: increasing slope = positive acceleration
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + i * i; // quadratic growth
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastAccel = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastAccel > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccelIndicator_DeceleratingTrend_ProducesNegativeAcceleration()
|
||||
{
|
||||
var indicator = new AccelIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Decelerating trend: decreasing slope = negative acceleration
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 200 - i * i; // quadratic decay
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastAccel = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastAccel < 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ACCEL (Second Derivative / Acceleration) Quantower indicator.
|
||||
/// Measures the rate of change of the rate of change - derivative of slope.
|
||||
/// </summary>
|
||||
public class AccelIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Accel? _accel;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => 3;
|
||||
public override string ShortName => "ACCEL";
|
||||
|
||||
public AccelIndicator()
|
||||
{
|
||||
Name = "ACCEL - Second Derivative (Acceleration)";
|
||||
Description = "Measures rate of change of rate of change - derivative of slope";
|
||||
SeparateWindow = true;
|
||||
OnBackGround = false;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_accel = new Accel();
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("Accel", Momentum, 2, LineStyle.Histogramm));
|
||||
AddLineSeries(new LineSeries("Zero", Color.Gray, 1, LineStyle.Dot));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_accel == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_accel.Update(input, isNew);
|
||||
|
||||
bool isHot = _accel.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_accel.Last.Value, isHot, ShowColdValues);
|
||||
LinesSeries[1].SetValue(0);
|
||||
|
||||
if (isHot || ShowColdValues)
|
||||
{
|
||||
double accel = _accel.Last.Value;
|
||||
Color color;
|
||||
if (accel > 0)
|
||||
color = Color.Green;
|
||||
else if (accel < 0)
|
||||
color = Color.Red;
|
||||
else
|
||||
color = Color.Gray;
|
||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AccelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var accel = new Accel();
|
||||
Assert.Equal(0, accel.Last.Value);
|
||||
Assert.False(accel.IsHot);
|
||||
Assert.Contains("Accel", accel.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(3, accel.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var accel = new Accel();
|
||||
accel.Update(new TValue(DateTime.UtcNow, 10));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 20));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
double valueBefore = accel.Last.Value;
|
||||
|
||||
// Update with isNew=false should change the result
|
||||
accel.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
|
||||
double valueAfter = accel.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var accel = new Accel();
|
||||
accel.Update(new TValue(DateTime.UtcNow, 10));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 20));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var accel = new Accel();
|
||||
accel.Update(new TValue(DateTime.UtcNow, 10));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 20));
|
||||
accel.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
var resultPosInf = accel.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = accel.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var accel = new Accel();
|
||||
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);
|
||||
accel.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = accel.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
accel.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = accel.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>(() =>
|
||||
Accel.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];
|
||||
Accel.Calculate(tValues, batchOutput);
|
||||
double expected = batchOutput[^1];
|
||||
|
||||
// 2. Streaming Mode
|
||||
var streamingInd = new Accel();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 3. TSeries Batch Mode
|
||||
var batchSeriesResult = Accel.Calculate(series);
|
||||
double tseriesResult = batchSeriesResult.Last.Value;
|
||||
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, tseriesResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_KnownValues()
|
||||
{
|
||||
// accel[i] = source[i] - 2*source[i-1] + source[i-2]
|
||||
// Data: 10, 20, 35, 40, 42
|
||||
// slope[1] = 20-10 = 10
|
||||
// slope[2] = 35-20 = 15
|
||||
// slope[3] = 40-35 = 5
|
||||
// slope[4] = 42-40 = 2
|
||||
// accel[0] = 0 (insufficient history)
|
||||
// accel[1] = 0 (insufficient history)
|
||||
// accel[2] = 35 - 2*20 + 10 = 35 - 40 + 10 = 5
|
||||
// accel[3] = 40 - 2*35 + 20 = 40 - 70 + 20 = -10
|
||||
// accel[4] = 42 - 2*40 + 35 = 42 - 80 + 35 = -3
|
||||
|
||||
double[] data = [10, 20, 35, 40, 42];
|
||||
double[] expected = [0, 0, 5, -10, -3];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var accel = new Accel();
|
||||
|
||||
Assert.False(accel.IsHot);
|
||||
accel.Update(new TValue(DateTime.UtcNow, 10));
|
||||
Assert.False(accel.IsHot);
|
||||
accel.Update(new TValue(DateTime.UtcNow, 20));
|
||||
Assert.False(accel.IsHot);
|
||||
accel.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.True(accel.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
accel.Update(new TValue(DateTime.UtcNow, i));
|
||||
}
|
||||
Assert.True(accel.IsHot);
|
||||
|
||||
accel.Reset();
|
||||
Assert.False(accel.IsHot);
|
||||
Assert.Equal(0, accel.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 accel = new Accel();
|
||||
var iterativeResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
iterativeResults[i] = accel.Last.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = new double[count];
|
||||
Accel.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 accel = new Accel();
|
||||
var iterativeResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
accel.Update(data[i]);
|
||||
iterativeResults[i] = accel.Last.Value;
|
||||
}
|
||||
|
||||
// TSeries Batch
|
||||
var accelBatch = new Accel();
|
||||
var batchSeries = accelBatch.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 accel = new Accel(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 10));
|
||||
source.Add(new TValue(DateTime.UtcNow, 20));
|
||||
source.Add(new TValue(DateTime.UtcNow, 35));
|
||||
|
||||
Assert.True(accel.IsHot);
|
||||
Assert.Equal(5, accel.Last.Value); // 35 - 2*20 + 10 = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Accel using synthetic data with known mathematical results.
|
||||
/// </summary>
|
||||
public class AccelValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void QuadraticSequence_ProducesConstantAccel()
|
||||
{
|
||||
// Quadratic sequence: 0, 1, 4, 9, 16, 25 (x^2)
|
||||
// Accel = second difference = 2 (constant for quadratic)
|
||||
// f(n) = n², slope(n) = 2n-1, accel = 2
|
||||
double[] data = [0, 1, 4, 9, 16, 25];
|
||||
double[] expected = [0, 0, 2, 2, 2, 2]; // First two are warmup (0), rest are 2
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinearSequence_ProducesZeroAccel()
|
||||
{
|
||||
// Linear sequence: 0, 2, 4, 6, 8, 10 (slope = 2, accel = 0)
|
||||
double[] data = [0, 2, 4, 6, 8, 10];
|
||||
double[] expected = [0, 0, 0, 0, 0, 0];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantSequence_ProducesZeroAccel()
|
||||
{
|
||||
// Constant sequence: 5, 5, 5, 5, 5 (slope = 0, accel = 0)
|
||||
double[] data = [5, 5, 5, 5, 5];
|
||||
double[] expected = [0, 0, 0, 0, 0];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CubicSequence_ProducesLinearAccel()
|
||||
{
|
||||
// Cubic sequence: 0, 1, 8, 27, 64, 125 (x^3)
|
||||
// First diff: 1, 7, 19, 37, 61
|
||||
// Second diff (accel): 6, 12, 18, 24 (linear, step of 6)
|
||||
double[] data = [0, 1, 8, 27, 64, 125];
|
||||
double[] expected = [0, 0, 6, 12, 18, 24];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeQuadratic_ProducesNegativeAccel()
|
||||
{
|
||||
// Negative quadratic: -x² → 0, -1, -4, -9, -16
|
||||
// Accel = -2 (constant)
|
||||
double[] data = [0, -1, -4, -9, -16];
|
||||
double[] expected = [0, 0, -2, -2, -2];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlternatingSequence_ProducesAlternatingAccel()
|
||||
{
|
||||
// Alternating: 0, 10, 0, 10, 0
|
||||
// Slope: 10, -10, 10, -10
|
||||
// Accel: -20, 20, -20
|
||||
double[] data = [0, 10, 0, 10, 0];
|
||||
double[] expected = [0, 0, -20, 20, -20];
|
||||
|
||||
var accel = new Accel();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalculation_MatchesSyntheticData()
|
||||
{
|
||||
double[] data = [0, 1, 4, 9, 16, 25];
|
||||
double[] expected = [0, 0, 2, 2, 2, 2];
|
||||
double[] output = new double[data.Length];
|
||||
|
||||
Accel.Calculate(data, output);
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
Assert.Equal(expected[i], output[i], precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeQuadraticSequence_ProducesConstantAccel()
|
||||
{
|
||||
// Generate 1000 points: f(n) = n² with coefficient 0.5 → accel = 1
|
||||
int count = 1000;
|
||||
double[] data = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
data[i] = 0.5 * i * i;
|
||||
}
|
||||
|
||||
var accel = new Accel();
|
||||
// Skip warmup period (first 2 bars)
|
||||
_ = accel.Update(new TValue(DateTime.UtcNow, data[0]));
|
||||
_ = accel.Update(new TValue(DateTime.UtcNow, data[1]));
|
||||
|
||||
for (int i = 2; i < count; i++)
|
||||
{
|
||||
accel.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(1.0, accel.Last.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
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>
|
||||
/// ACCEL: Second Derivative (Acceleration)
|
||||
/// Measures the rate of change of velocity - the acceleration of price movement.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The second derivative approximates acceleration: how fast the velocity is changing.
|
||||
///
|
||||
/// Formula:
|
||||
/// Accel_t = Slope_t - Slope_{t-1}
|
||||
/// = (Value_t - Value_{t-1}) - (Value_{t-1} - Value_{t-2})
|
||||
/// = Value_t - 2*Value_{t-1} + Value_{t-2}
|
||||
///
|
||||
/// Key properties:
|
||||
/// - O(1) streaming complexity
|
||||
/// - Zero allocations in hot path
|
||||
/// - SIMD-optimized batch calculation
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Accel : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double Prev1, double Prev2, double LastValidValue, int Count);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
public override bool IsHot => _state.Count >= 3;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Accel (second derivative) indicator.
|
||||
/// </summary>
|
||||
public Accel()
|
||||
{
|
||||
Name = "Accel";
|
||||
WarmupPeriod = 3;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Accel indicator with event subscription.
|
||||
/// </summary>
|
||||
public Accel(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 >= 2)
|
||||
{
|
||||
// accel = val - 2*prev1 + prev2
|
||||
result = Math.FusedMultiplyAdd(-2.0, _state.Prev1, val + _state.Prev2);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
// Shift history
|
||||
_state.Prev2 = _state.Prev1;
|
||||
_state.Prev1 = val;
|
||||
_state.Count = Math.Min(_state.Count + 1, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rollback for bar correction
|
||||
_state.LastValidValue = _p_state.LastValidValue;
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
if (_p_state.Count >= 2)
|
||||
{
|
||||
result = Math.FusedMultiplyAdd(-2.0, _p_state.Prev1, val + _p_state.Prev2);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
// Update current state from previous (don't shift)
|
||||
_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 two values using cached span
|
||||
if (len >= 2)
|
||||
{
|
||||
double v1 = double.IsFinite(sourceValues[len - 1]) ? sourceValues[len - 1] : _state.LastValidValue;
|
||||
double v2 = double.IsFinite(sourceValues[len - 2]) ? sourceValues[len - 2] : v1;
|
||||
_state.Prev1 = v1;
|
||||
_state.Prev2 = v2;
|
||||
_state.LastValidValue = v1;
|
||||
_state.Count = Math.Min(len, 3);
|
||||
_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 accel = new Accel();
|
||||
return accel.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates second derivative (acceleration) for a span.
|
||||
/// accel[i] = source[i] - 2*source[i-1] + source[i-2]
|
||||
/// </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 two elements have insufficient history
|
||||
output[0] = 0.0;
|
||||
if (len == 1) return;
|
||||
output[1] = 0.0;
|
||||
if (len == 2) return;
|
||||
|
||||
int i = 2;
|
||||
|
||||
// Check for non-finite values - if any exist, use scalar path only
|
||||
bool hasNonFinite = false;
|
||||
for (int k = 0; k < len && !hasNonFinite; k++)
|
||||
{
|
||||
hasNonFinite = !double.IsFinite(source[k]);
|
||||
}
|
||||
|
||||
// AVX512: 8 doubles at once (only if all values are finite)
|
||||
if (!hasNonFinite && Avx512F.IsSupported && len >= 10)
|
||||
{
|
||||
var two = Vector512.Create(2.0);
|
||||
const int VectorWidth = 8;
|
||||
int simdEnd = len - ((len - 2) % 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));
|
||||
// accel = current - 2*prev1 + prev2
|
||||
var twoTimesP1 = Avx512F.Multiply(two, prev1);
|
||||
var diff = Avx512F.Subtract(current, twoTimesP1);
|
||||
var result = Avx512F.Add(diff, prev2);
|
||||
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
// AVX: 4 doubles at once (only if all values are finite)
|
||||
else if (!hasNonFinite && Avx.IsSupported && len >= 6)
|
||||
{
|
||||
var two = Vector256.Create(2.0);
|
||||
const int VectorWidth = 4;
|
||||
int simdEnd = len - ((len - 2) % 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 twoTimesP1 = Avx.Multiply(two, prev1);
|
||||
var diff = Avx.Subtract(current, twoTimesP1);
|
||||
var result = Avx.Add(diff, prev2);
|
||||
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
// ARM64 Neon: 2 doubles at once (only if all values are finite)
|
||||
else if (!hasNonFinite && AdvSimd.Arm64.IsSupported && len >= 4)
|
||||
{
|
||||
var two = Vector128.Create(2.0);
|
||||
const int VectorWidth = 2;
|
||||
int simdEnd = len - ((len - 2) % 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 twoTimesP1 = AdvSimd.Arm64.Multiply(two, prev1);
|
||||
var diff = AdvSimd.Arm64.Subtract(current, twoTimesP1);
|
||||
var result = AdvSimd.Arm64.Add(diff, prev2);
|
||||
result.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
// Initialize prev values from actual data at position i-1 and i-2
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double curr = source[i];
|
||||
double p1 = source[i - 1];
|
||||
double p2 = source[i - 2];
|
||||
|
||||
// Handle NaN/Infinity by substitution (find first finite value)
|
||||
double fallback = FindFinite(curr, p1, p2);
|
||||
if (!double.IsFinite(curr)) curr = fallback;
|
||||
if (!double.IsFinite(p1)) p1 = fallback;
|
||||
if (!double.IsFinite(p2)) p2 = fallback;
|
||||
|
||||
// accel = curr - 2*prev1 + prev2
|
||||
output[i] = Math.FusedMultiplyAdd(-2.0, p1, curr + p2);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double FindFinite(double a, double b, double c)
|
||||
{
|
||||
if (double.IsFinite(a)) return a;
|
||||
if (double.IsFinite(b)) return b;
|
||||
if (double.IsFinite(c)) return c;
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
# ACCEL: Second Derivative (Acceleration)
|
||||
|
||||
> "Velocity tells you where you're going. Acceleration tells you if you're getting there faster or slower."
|
||||
|
||||
ACCEL measures the rate of change of velocity—the acceleration of a time series. As the second derivative, it reveals momentum shifts before they manifest in price direction. Positive acceleration means velocity is increasing (trend strengthening); negative means velocity is decreasing (trend weakening). This O(1) streaming implementation uses FMA optimization and SIMD batch processing.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The second derivative appears throughout physics (Newton's F=ma) and signal processing. In financial markets, acceleration precedes velocity, which precedes price. A stock can be rising (positive slope) but decelerating (negative accel)—an early warning of trend exhaustion.
|
||||
|
||||
Traders have long recognized this pattern: "the trend is slowing down." ACCEL quantifies that intuition precisely. When price makes higher highs but acceleration turns negative, the rally is losing steam. When price makes lower lows but acceleration turns positive, the selloff is exhausting.
|
||||
|
||||
QuanTAlib implements ACCEL as the discrete second difference with FMA optimization, SIMD batch processing, and full bar correction support.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ACCEL computes the second finite difference with three-point history:
|
||||
|
||||
### 1. Second Difference Operation
|
||||
|
||||
The fundamental operation:
|
||||
|
||||
$$
|
||||
A_t = V_t - 2V_{t-1} + V_{t-2}
|
||||
$$
|
||||
|
||||
This is algebraically equivalent to:
|
||||
|
||||
$$
|
||||
A_t = (V_t - V_{t-1}) - (V_{t-1} - V_{t-2}) = S_t - S_{t-1}
|
||||
$$
|
||||
|
||||
where $S$ is the first derivative (slope).
|
||||
|
||||
### 2. FMA Optimization
|
||||
|
||||
The formula $V_t - 2V_{t-1} + V_{t-2}$ is computed using Fused Multiply-Add:
|
||||
|
||||
$$
|
||||
A_t = \text{FMA}(-2, V_{t-1}, V_t + V_{t-2})
|
||||
$$
|
||||
|
||||
This reduces rounding error and may execute in a single CPU cycle on modern hardware.
|
||||
|
||||
### 3. State Management
|
||||
|
||||
State consists of:
|
||||
- `Prev1`: The previous input value $V_{t-1}$
|
||||
- `Prev2`: The value before that $V_{t-2}$
|
||||
- `LastValidValue`: Last known finite value for NaN/Infinity substitution
|
||||
- `Count`: Number of values processed (0, 1, 2, or 3+)
|
||||
|
||||
The indicator becomes "hot" (fully warmed up) after 3 values.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Discrete Second Derivative
|
||||
|
||||
For a time series $V$:
|
||||
|
||||
$$
|
||||
A_t = \frac{d^2V}{dt^2} \approx V_t - 2V_{t-1} + V_{t-2}
|
||||
$$
|
||||
|
||||
This is the central difference approximation of the second derivative.
|
||||
|
||||
### Interpretation
|
||||
|
||||
| Acceleration Value | Slope Value | Meaning |
|
||||
| :--- | :--- | :--- |
|
||||
| $A > 0$ | $S > 0$ | Rising and accelerating (strong uptrend) |
|
||||
| $A < 0$ | $S > 0$ | Rising but decelerating (weakening uptrend) |
|
||||
| $A > 0$ | $S < 0$ | Falling but decelerating (weakening downtrend) |
|
||||
| $A < 0$ | $S < 0$ | Falling and accelerating (strong downtrend) |
|
||||
| $A = 0$ | any | Constant velocity (linear trend) |
|
||||
|
||||
### Inflection Points
|
||||
|
||||
Acceleration zero-crossings indicate inflection points—where the trend changes character:
|
||||
|
||||
$$
|
||||
A_t > 0 \text{ and } A_{t-1} < 0 \implies \text{Concave-up inflection (potential bottom)}
|
||||
$$
|
||||
|
||||
$$
|
||||
A_t < 0 \text{ and } A_{t-1} > 0 \implies \text{Concave-down inflection (potential top)}
|
||||
$$
|
||||
|
||||
### Derivative Chain
|
||||
|
||||
ACCEL is the middle link:
|
||||
|
||||
$$
|
||||
\text{Slope}_t = V_t - V_{t-1}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{Accel}_t = \text{Slope}_t - \text{Slope}_{t-1} = V_t - 2V_{t-1} + V_{t-2}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{Jolt}_t = \text{Accel}_t - \text{Accel}_{t-1}
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| FMA | 1 | 4 | 4 |
|
||||
| ADD | 1 | 1 | 1 |
|
||||
| MOV (state update) | 3 | 1 | 3 |
|
||||
| CMP (IsFinite check) | 1 | 1 | 1 |
|
||||
| **Total** | **6** | — | **~9 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 | 9 | 4,608 | 1× |
|
||||
| AVX-512 SIMD | 1.1 | 563 | 8× |
|
||||
| AVX SIMD | 2.3 | 1,178 | 4× |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact finite difference |
|
||||
| **Timeliness** | 10/10 | Zero lag (instantaneous) |
|
||||
| **Smoothness** | 2/10 | Amplifies noise significantly |
|
||||
| **Computational Cost** | 10/10 | Single FMA + bookkeeping |
|
||||
| **Memory** | 10/10 | ~64 bytes state |
|
||||
|
||||
## Validation
|
||||
|
||||
ACCEL is a fundamental operation. Validation confirms exact match with manual calculation.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented directly |
|
||||
| **Skender** | N/A | Not implemented directly |
|
||||
| **Manual Calculation** | ✅ | Exact match |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Extreme Noise Sensitivity**: Second derivatives amplify noise quadratically. A 1% random wiggle in price becomes a massive acceleration spike. Pre-smooth the input (EMA, SMA) before computing ACCEL for noisy data.
|
||||
|
||||
2. **Scale Dependency**: ACCEL output scales with input magnitude squared. A $100 stock has 10,000× larger accelerations than a $1 stock. Normalize if comparing across instruments.
|
||||
|
||||
3. **Warmup Period**: ACCEL requires 3 values to produce meaningful output. The first two outputs are always 0.
|
||||
|
||||
4. **Sign Interpretation**: Positive acceleration doesn't mean "going up"—it means "velocity increasing." A falling stock with positive acceleration is falling more slowly.
|
||||
|
||||
5. **Lagging Confirmation**: By the time acceleration confirms a trend change, much of the move may be over. Use acceleration for early warning, not entry 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**: ~64 bytes per instance. Negligible for most use cases.
|
||||
|
||||
## References
|
||||
|
||||
- Newton, Isaac. (1687). "Philosophiæ Naturalis Principia Mathematica."
|
||||
- Numerical Methods: Finite Difference Approximations.
|
||||
- Murphy, John J. (1999). "Technical Analysis of the Financial Markets."
|
||||
@@ -0,0 +1,84 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Acceleration (Slope of Slope) (ACCEL)", "ACCEL", overlay=false, precision=8)
|
||||
|
||||
//@function Calculates acceleration (slope of slope)
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/accel.md
|
||||
//@param src Source series to calculate slope from
|
||||
//@param len Lookback period for calculation
|
||||
//@returns acceleration
|
||||
accel(series float src, simple int len1) =>
|
||||
if len1 <= 1
|
||||
runtime.error("Length 1 for 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_slope = na
|
||||
if validCount1 >= 2
|
||||
float n1 = validCount1
|
||||
float divisor1 = n1 * sumX21 - sumX1 * sumX1
|
||||
if divisor1 != 0.0
|
||||
current_slope := (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_slope
|
||||
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 calculatedAccel = na
|
||||
if validCount2 >= 2
|
||||
float n2 = validCount2
|
||||
float divisor2 = n2 * sumX22 - sumX2 * sumX2
|
||||
if divisor2 != 0.0
|
||||
calculatedAccel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2
|
||||
calculatedAccel
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=2)
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
a = accel(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(a, "Accel", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user