mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38: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,123 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SuperIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SuperIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SuperIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(3.0, indicator.Multiplier);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SuperTrend", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, SuperIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 20, Multiplier = 2.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("Super", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new SuperIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Super.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_Initialize_CreatesInternalSuper()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 14 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (SuperTrend, Upper, Lower)
|
||||
Assert.Equal(3, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value (either Up or Down)
|
||||
// One should be NaN, other should be value, or both NaN if cold
|
||||
double up = indicator.LinesSeries[0].GetValue(0);
|
||||
double down = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(up) || double.IsFinite(down));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuperIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new SuperIndicator { Period = 14 };
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
indicator.Multiplier = 4.0;
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(4.0, indicator.Multiplier);
|
||||
Assert.Equal(0, SuperIndicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SuperIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[InputParameter("Multiplier", sortIndex: 2, 0.1, 100.0, 0.1, 1)]
|
||||
public double Multiplier { get; set; } = 3.0;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Super _super = null!;
|
||||
private readonly LineSeries _series;
|
||||
private readonly LineSeries _upperBand;
|
||||
private readonly LineSeries _lowerBand;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Super {Period}:{Multiplier}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/master/lib/trends/super/Super.Quantower.cs";
|
||||
|
||||
public SuperIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "SuperTrend";
|
||||
Description = "SuperTrend Indicator";
|
||||
_series = new LineSeries(name: "SuperTrend", color: Color.Orange, width: 2, style: LineStyle.Solid);
|
||||
_upperBand = new LineSeries(name: "Upper Band", color: Color.Red, width: 1, style: LineStyle.Dot);
|
||||
_lowerBand = new LineSeries(name: "Lower Band", color: Color.Green, width: 1, style: LineStyle.Dot);
|
||||
AddLineSeries(_series);
|
||||
AddLineSeries(_upperBand);
|
||||
AddLineSeries(_lowerBand);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_super = new Super(Period, Multiplier);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var bar = this.GetInputBar(args);
|
||||
double value = _super.Update(bar, isNew).Value;
|
||||
|
||||
_series.SetValue(value, _super.IsHot, ShowColdValues);
|
||||
_upperBand.SetValue(_super.UpperBand.Value, _super.IsHot, ShowColdValues);
|
||||
_lowerBand.SetValue(_super.LowerBand.Value, _super.IsHot, ShowColdValues);
|
||||
|
||||
// Color logic
|
||||
if (_super.IsHot)
|
||||
{
|
||||
_series.SetMarker(0, _super.IsBullish ? Color.Green : Color.Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class SuperTests
|
||||
{
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
super.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(super.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
super.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
super.Update(bars[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = super.Update(modifiedBar, false);
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var super2 = new Super(10, 3.0);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
super2.Update(bars[i]);
|
||||
}
|
||||
var val3 = super2.Update(modifiedBar, true);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
Assert.Equal(super2.UpperBand.Value, super.UpperBand.Value, 1e-9);
|
||||
Assert.Equal(super2.LowerBand.Value, super.LowerBand.Value, 1e-9);
|
||||
Assert.Equal(super2.IsBullish, super.IsBullish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
super.Update(bars[i]);
|
||||
}
|
||||
|
||||
super.Reset();
|
||||
Assert.Equal(0, super.Last.Value);
|
||||
Assert.False(super.IsHot);
|
||||
|
||||
// Feed again
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
super.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(super.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Update_Matches_Streaming()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults.Add(super.Update(bars[i]).Value);
|
||||
}
|
||||
|
||||
var super2 = new Super(10, 3.0);
|
||||
var seriesResults = super2.Update(bars);
|
||||
|
||||
Assert.Equal(streamingResults.Count, seriesResults.Count);
|
||||
for (int i = 0; i < seriesResults.Count; i++)
|
||||
{
|
||||
// Handle NaN comparison
|
||||
if (double.IsNaN(streamingResults[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(seriesResults.Values[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Warmup_Handling()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// First 10 bars should be NaN
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var result = super.Update(bars[i]);
|
||||
Assert.True(double.IsNaN(result.Value), $"Bar {i} should be NaN");
|
||||
Assert.False(super.IsHot);
|
||||
}
|
||||
|
||||
// 11th bar (index 10) should be valid
|
||||
var result11 = super.Update(bars[10]);
|
||||
Assert.True(double.IsFinite(result11.Value), "Bar 10 should be finite");
|
||||
Assert.True(super.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidParameters_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(0, 3.0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(-1, 3.0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(10, 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Super(10, -1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Matches_Streaming()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var super = new Super(10, 3.0);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults.Add(super.Update(bars[i]).Value);
|
||||
}
|
||||
|
||||
var staticResults = Super.Batch(bars, 10, 3.0);
|
||||
|
||||
Assert.Equal(streamingResults.Count, staticResults.Count);
|
||||
for (int i = 0; i < staticResults.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(streamingResults[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(staticResults.Values[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Test TBarSeries chain
|
||||
var result = super.Update(bars);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<TSeries>(result);
|
||||
|
||||
// Test TBar chain (returns TValue)
|
||||
var result2 = super.Update(bars[0]);
|
||||
Assert.IsType<TValue>(result2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public sealed class SuperValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public SuperValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender()
|
||||
{
|
||||
var super = new Super(10, 3.0);
|
||||
var results = new List<double>();
|
||||
var upper = new List<double>();
|
||||
var lower = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = super.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
upper.Add(super.UpperBand.Value);
|
||||
lower.Add(super.LowerBand.Value);
|
||||
}
|
||||
|
||||
// Skender uses GetSuperTrend
|
||||
var skenderResults = _data.SkenderQuotes.GetSuperTrend(10, 3.0).ToList();
|
||||
|
||||
Assert.Equal(_data.Bars.Count, skenderResults.Count);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
// Skender returns null for warmup
|
||||
if (skenderResults[i].SuperTrend == null)
|
||||
{
|
||||
Assert.True(double.IsNaN(results[i]));
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.Equal((double)skenderResults[i].SuperTrend!, results[i], ValidationHelper.SkenderTolerance);
|
||||
|
||||
if (skenderResults[i].UpperBand != null)
|
||||
{
|
||||
Assert.Equal((double)skenderResults[i].UpperBand!, upper[i], ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
|
||||
if (skenderResults[i].LowerBand != null)
|
||||
{
|
||||
Assert.Equal((double)skenderResults[i].LowerBand!, lower[i], ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: OoplesFinance implementation of SuperTrend diverges significantly from Skender and QuanTAlib.
|
||||
// This is likely due to different initialization logic for ATR or the SuperTrend state itself.
|
||||
// Therefore, we do not validate against Ooples for SuperTrend.
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SuperTrend Indicator
|
||||
/// A trend-following indicator that uses ATR to define upper and lower bands.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Super : ITValuePublisher
|
||||
{
|
||||
private readonly double _multiplier;
|
||||
private readonly int _period;
|
||||
private TBar _prevBar;
|
||||
private TBar _lastInput;
|
||||
private TBar _p_prevBar;
|
||||
private TBar _p_lastInput;
|
||||
private int _sampleCount;
|
||||
private int _p_sampleCount;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public bool IsBullish;
|
||||
public double UpperBand;
|
||||
public double LowerBand;
|
||||
public bool IsInitialized;
|
||||
public double Atr;
|
||||
public double SumTr;
|
||||
}
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name => $"Super({_period},{_multiplier})";
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current SuperTrend value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current Upper Band value.
|
||||
/// </summary>
|
||||
public TValue UpperBand { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current Lower Band value.
|
||||
/// </summary>
|
||||
public TValue LowerBand { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the current trend is bullish.
|
||||
/// </summary>
|
||||
public bool IsBullish => _state.IsBullish;
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data to be valid.
|
||||
/// </summary>
|
||||
public bool IsHot => _sampleCount > _period;
|
||||
|
||||
public int WarmupPeriod => _period + 1;
|
||||
|
||||
public Super(int period = 10, double multiplier = 3.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0.");
|
||||
}
|
||||
if (multiplier <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be greater than 0.");
|
||||
}
|
||||
_period = period;
|
||||
_multiplier = multiplier;
|
||||
_state = new State { IsBullish = true, IsInitialized = false };
|
||||
_sampleCount = 0;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_state = new State { IsBullish = true, IsInitialized = false };
|
||||
_p_state = default;
|
||||
_prevBar = default;
|
||||
_lastInput = default;
|
||||
_p_prevBar = default;
|
||||
_p_lastInput = default;
|
||||
_sampleCount = 0;
|
||||
_p_sampleCount = 0;
|
||||
Last = default;
|
||||
UpperBand = default;
|
||||
LowerBand = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_p_prevBar = _prevBar;
|
||||
_p_lastInput = _lastInput;
|
||||
_p_sampleCount = _sampleCount;
|
||||
if (_sampleCount > 0)
|
||||
{
|
||||
_prevBar = _lastInput;
|
||||
}
|
||||
_sampleCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_prevBar = _p_prevBar;
|
||||
_lastInput = _p_lastInput;
|
||||
_sampleCount = _p_sampleCount;
|
||||
if (_sampleCount > 0)
|
||||
{
|
||||
_prevBar = _lastInput;
|
||||
}
|
||||
_sampleCount++;
|
||||
}
|
||||
_lastInput = input;
|
||||
|
||||
// Calculate True Range with NaN/Infinity guards
|
||||
double safeHigh = double.IsFinite(input.High) ? input.High : _prevBar.High;
|
||||
double safeLow = double.IsFinite(input.Low) ? input.Low : _prevBar.Low;
|
||||
double safePrevClose = double.IsFinite(_prevBar.Close) ? _prevBar.Close : safeHigh;
|
||||
|
||||
double tr;
|
||||
if (_sampleCount <= 1)
|
||||
{
|
||||
tr = safeHigh - safeLow;
|
||||
}
|
||||
else
|
||||
{
|
||||
double h_l = safeHigh - safeLow;
|
||||
double h_pc = Math.Abs(safeHigh - safePrevClose);
|
||||
double l_pc = Math.Abs(safeLow - safePrevClose);
|
||||
tr = Math.Max(h_l, Math.Max(h_pc, l_pc));
|
||||
}
|
||||
|
||||
// Update ATR using RMA (Wilder's smoothing)
|
||||
// Note: Skender's implementation skips the first bar's TR for the initial SMA calculation.
|
||||
double atr;
|
||||
if (_sampleCount == 1)
|
||||
{
|
||||
atr = 0;
|
||||
}
|
||||
else if (_sampleCount <= _period + 1)
|
||||
{
|
||||
_state.SumTr += tr;
|
||||
if (_sampleCount == _period + 1)
|
||||
{
|
||||
_state.Atr = _state.SumTr / _period;
|
||||
}
|
||||
atr = _state.Atr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// RMA: (prevAtr * (period - 1) + tr) / period
|
||||
// Rewritten as FMA: prevAtr * decay + tr * alpha where decay = (period-1)/period, alpha = 1/period
|
||||
double invPeriod = 1.0 / _period;
|
||||
_state.Atr = Math.FusedMultiplyAdd(_state.Atr, 1.0 - invPeriod, tr * invPeriod);
|
||||
atr = _state.Atr;
|
||||
}
|
||||
|
||||
double superTrend = double.NaN;
|
||||
double upperBand = double.NaN;
|
||||
double lowerBand = double.NaN;
|
||||
|
||||
if (_sampleCount > _period)
|
||||
{
|
||||
double mid = (input.High + input.Low) * 0.5;
|
||||
// Use FMA for band calculations: mid + multiplier * atr
|
||||
double upperEval = Math.FusedMultiplyAdd(_multiplier, atr, mid);
|
||||
double lowerEval = Math.FusedMultiplyAdd(-_multiplier, atr, mid);
|
||||
|
||||
if (!_state.IsInitialized)
|
||||
{
|
||||
_state.IsBullish = true; // Skender seems to default to Bullish (or determines it dynamically)
|
||||
_state.UpperBand = upperEval;
|
||||
_state.LowerBand = lowerEval;
|
||||
_state.IsInitialized = true;
|
||||
}
|
||||
|
||||
double prevUpperBand = _state.UpperBand;
|
||||
double prevLowerBand = _state.LowerBand;
|
||||
double prevClose = _prevBar.Close;
|
||||
|
||||
// New upper band
|
||||
if (upperEval < prevUpperBand || prevClose > prevUpperBand)
|
||||
{
|
||||
_state.UpperBand = upperEval;
|
||||
}
|
||||
|
||||
// New lower band
|
||||
if (lowerEval > prevLowerBand || prevClose < prevLowerBand)
|
||||
{
|
||||
_state.LowerBand = lowerEval;
|
||||
}
|
||||
|
||||
// SuperTrend
|
||||
if (_state.IsBullish)
|
||||
{
|
||||
if (input.Close < _state.LowerBand)
|
||||
{
|
||||
_state.IsBullish = false;
|
||||
superTrend = _state.UpperBand;
|
||||
}
|
||||
else
|
||||
{
|
||||
superTrend = _state.LowerBand;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input.Close > _state.UpperBand)
|
||||
{
|
||||
_state.IsBullish = true;
|
||||
superTrend = _state.LowerBand;
|
||||
}
|
||||
else
|
||||
{
|
||||
superTrend = _state.UpperBand;
|
||||
}
|
||||
}
|
||||
|
||||
upperBand = _state.UpperBand;
|
||||
lowerBand = _state.LowerBand;
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, superTrend);
|
||||
UpperBand = new TValue(input.Time, upperBand);
|
||||
LowerBand = new TValue(input.Time, lowerBand);
|
||||
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = Update(source[i], isNew: true);
|
||||
t.Add(val.Time);
|
||||
v.Add(val.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 10, double multiplier = 3.0)
|
||||
{
|
||||
var indicator = new Super(period, multiplier);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# SUPER: SuperTrend
|
||||
|
||||
> "It's not an indicator; it's a trailing stop with a marketing budget. Perfect for traders who want to catch the trend but lack the emotional discipline to hold on."
|
||||
|
||||
SuperTrend is a trend-following indicator that overlays the price chart. It uses the Average True Range (ATR) to calculate upper and lower volatility bands, switching between them based on the direction of the closing price. It effectively functions as a trailing stop-loss that adapts to market volatility.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Created by Olivier Seban. It gained massive popularity in the retail trading community for its visual simplicity: Green line = Buy, Red line = Sell. It combines the volatility measurement of Wilder's ATR with a simple breakout logic.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
SuperTrend is a state machine. It maintains two theoretical bands (Upper and Lower) and a boolean state (`IsBullish`).
|
||||
|
||||
### The Ratchet Mechanism
|
||||
|
||||
The bands act as a ratchet:
|
||||
|
||||
* **Bullish Mode**: The Lower Band (Stop Loss) can only move up. If the calculated Lower Band drops, the indicator ignores it and keeps the previous value.
|
||||
* **Bearish Mode**: The Upper Band (Stop Loss) can only move down.
|
||||
|
||||
The trend flips when the Close price crosses the active band.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Basic Bands
|
||||
|
||||
$$ Upper_{basic} = \frac{High + Low}{2} + (Multiplier \times ATR) $$
|
||||
$$ Lower_{basic} = \frac{High + Low}{2} - (Multiplier \times ATR) $$
|
||||
|
||||
### 2. Ratchet Logic (Bullish Example)
|
||||
|
||||
$$ Lower_{final} = \begin{cases} Lower_{basic} & \text{if } Lower_{basic} > Lower_{prev} \text{ or } Close_{prev} < Lower_{prev} \\ Lower_{prev} & \text{otherwise} \end{cases} $$
|
||||
|
||||
### 3. Trend Logic
|
||||
|
||||
$$ SuperTrend = \begin{cases} Lower_{final} & \text{if Bullish} \\ Upper_{final} & \text{if Bearish} \end{cases} $$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 9 | High; O(1) calculation with minimal overhead. |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths. |
|
||||
| **Complexity** | O(1) | Constant time regardless of period. |
|
||||
| **Accuracy** | 10 | Matches standard implementations exactly. |
|
||||
| **Timeliness** | 5 | Lag depends on ATR period and multiplier. |
|
||||
| **Overshoot** | 0 | Bands are constrained by price action. |
|
||||
| **Smoothness** | 2 | Step-like behavior; not a smooth curve. |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated. |
|
||||
| **TA-Lib** | N/A | Not implemented. |
|
||||
| **Skender** | ✅ | Matches `GetSuperTrend` exactly. |
|
||||
| **Tulip** | N/A | Not implemented. |
|
||||
| **Ooples** | ❌ | Diverges significantly due to initialization logic. |
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Repainting**: SuperTrend does not repaint historical values, but the current bar's value can flip back and forth until the Close is finalized.
|
||||
2. **Whipsaws**: In ranging markets, SuperTrend will generate frequent false signals, buying the top and selling the bottom. It requires a trend filter (like ADX).
|
||||
3. **ATR Warmup**: The indicator requires $N$ bars to stabilize the ATR before the bands become accurate.
|
||||
@@ -0,0 +1,67 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("SuperTrend", "SUPER", overlay=true)
|
||||
|
||||
//@function Calculates SuperTrend using ATR-based dynamic support/resistance
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/super.md
|
||||
//@param source Price series for calculation (typically hlc3 or close)
|
||||
//@param atr_period Lookback period for ATR calculation
|
||||
//@param multiplier Multiplier applied to ATR for band calculation
|
||||
//@returns Tuple [supertrend, direction] where direction is 1 (bullish) or -1 (bearish)
|
||||
//@optimized O(1) with proper warmup handling
|
||||
super(series float source, simple int atr_period, simple float multiplier) =>
|
||||
if atr_period <= 0
|
||||
runtime.error("ATR period must be greater than 0")
|
||||
if multiplier <= 0.0
|
||||
runtime.error("Multiplier must be greater than 0")
|
||||
float hl2_value = (high + low) / 2.0
|
||||
float tr = math.max(high - low, math.max(math.abs(high - nz(close[1])), math.abs(low - nz(close[1]))))
|
||||
float alpha = 1.0 / atr_period
|
||||
float beta = 1.0 - alpha
|
||||
var bool warmup = true
|
||||
var float e = 1.0
|
||||
var float atr = 0.0
|
||||
var float compensated_atr = tr
|
||||
atr := alpha * (tr - atr) + atr
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = 1.0 / (1.0 - e)
|
||||
compensated_atr := c * atr
|
||||
warmup := e > 1e-10
|
||||
else
|
||||
compensated_atr := atr
|
||||
float basic_ub = hl2_value + (multiplier * compensated_atr)
|
||||
float basic_lb = hl2_value - (multiplier * compensated_atr)
|
||||
var float final_ub = basic_ub
|
||||
var float final_lb = basic_lb
|
||||
var int trend = 1
|
||||
final_ub := basic_ub < final_ub or nz(close[1]) > final_ub ? basic_ub : final_ub
|
||||
final_lb := basic_lb > final_lb or nz(close[1]) < final_lb ? basic_lb : final_lb
|
||||
int prev_trend = nz(trend[1], 1)
|
||||
trend := close > final_ub ? 1 : close < final_lb ? -1 : prev_trend
|
||||
float supertrend = trend == 1 ? final_lb : final_ub
|
||||
[supertrend, trend]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_atr_period = input.int(10, "ATR Period", minval=1, maxval=100)
|
||||
i_multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1)
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
[st_line, st_direction] = super(i_source, i_atr_period, i_multiplier)
|
||||
|
||||
// Colors
|
||||
color bullish_color = color.new(color.green, 0)
|
||||
color bearish_color = color.new(color.red, 0)
|
||||
color line_color = st_direction == 1 ? bullish_color : bearish_color
|
||||
|
||||
// Plot
|
||||
plot(st_line, "SuperTrend", color=line_color, linewidth=2, style=plot.style_line)
|
||||
|
||||
// Optional: Plot buy/sell signals when direction changes
|
||||
bool direction_changed = st_direction != nz(st_direction[1])
|
||||
plotshape(direction_changed and st_direction == 1, "Buy Signal", shape.labelup, location.belowbar, color=bullish_color, text="BUY", textcolor=color.white, size=size.small)
|
||||
plotshape(direction_changed and st_direction == -1, "Sell Signal", shape.labeldown, location.abovebar, color=bearish_color, text="SELL", textcolor=color.white, size=size.small)
|
||||
Reference in New Issue
Block a user