mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
adding missing validations
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GatorIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GatorIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
|
||||
Assert.Equal(13, indicator.JawPeriod);
|
||||
Assert.Equal(8, indicator.JawShift);
|
||||
Assert.Equal(8, indicator.TeethPeriod);
|
||||
Assert.Equal(5, indicator.TeethShift);
|
||||
Assert.Equal(5, indicator.LipsPeriod);
|
||||
Assert.Equal(3, indicator.LipsShift);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("GATOR - Williams Gator Oscillator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new GatorIndicator { JawPeriod = 21, TeethPeriod = 13, LipsPeriod = 8 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("GATOR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("21", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("13", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("8", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
|
||||
Assert.Equal(0, GatorIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_Initialize_CreatesInternalGator()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// Should have two line series (Upper + Lower)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new GatorIndicator
|
||||
{
|
||||
JawPeriod = 5, JawShift = 3,
|
||||
TeethPeriod = 3, TeethShift = 2,
|
||||
LipsPeriod = 2, LipsShift = 1
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double upperVal = indicator.LinesSeries[0].GetValue(0);
|
||||
double lowerVal = indicator.LinesSeries[1].GetValue(0);
|
||||
Assert.True(double.IsFinite(upperVal));
|
||||
Assert.True(double.IsFinite(lowerVal));
|
||||
Assert.True(upperVal >= 0);
|
||||
Assert.True(lowerVal <= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new GatorIndicator
|
||||
{
|
||||
JawPeriod = 5, JawShift = 3,
|
||||
TeethPeriod = 3, TeethShift = 2,
|
||||
LipsPeriod = 2, LipsShift = 1
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[1].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[][] paramSets =
|
||||
{
|
||||
new[] { 5, 3, 3, 2, 2, 1 },
|
||||
new[] { 13, 8, 8, 5, 5, 3 },
|
||||
new[] { 21, 13, 13, 8, 8, 5 }
|
||||
};
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
var indicator = new GatorIndicator
|
||||
{
|
||||
JawPeriod = ps[0], JawShift = ps[1],
|
||||
TeethPeriod = ps[2], TeethShift = ps[3],
|
||||
LipsPeriod = ps[4], LipsShift = ps[5]
|
||||
};
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double upperVal = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(upperVal), $"Periods ({ps[0]},{ps[2]},{ps[4]}) should produce finite upper");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
Assert.Equal(13, indicator.JawPeriod);
|
||||
|
||||
indicator.JawPeriod = 21;
|
||||
indicator.TeethPeriod = 13;
|
||||
indicator.LipsPeriod = 8;
|
||||
Assert.Equal(21, indicator.JawPeriod);
|
||||
Assert.Equal(13, indicator.TeethPeriod);
|
||||
Assert.Equal(8, indicator.LipsPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Gator.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatorIndicator_HasTwoLineSeries_WithCorrectNames()
|
||||
{
|
||||
var indicator = new GatorIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Upper", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Lower", indicator.LinesSeries[1].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class GatorIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Jaw Period", sortIndex: 1, 1, 100, 1, 0)]
|
||||
public int JawPeriod { get; set; } = 13;
|
||||
|
||||
[InputParameter("Jaw Shift", sortIndex: 2, 0, 50, 1, 0)]
|
||||
public int JawShift { get; set; } = 8;
|
||||
|
||||
[InputParameter("Teeth Period", sortIndex: 3, 1, 100, 1, 0)]
|
||||
public int TeethPeriod { get; set; } = 8;
|
||||
|
||||
[InputParameter("Teeth Shift", sortIndex: 4, 0, 50, 1, 0)]
|
||||
public int TeethShift { get; set; } = 5;
|
||||
|
||||
[InputParameter("Lips Period", sortIndex: 5, 1, 100, 1, 0)]
|
||||
public int LipsPeriod { get; set; } = 5;
|
||||
|
||||
[InputParameter("Lips Shift", sortIndex: 6, 0, 50, 1, 0)]
|
||||
public int LipsShift { get; set; } = 3;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Gator _gator = null!;
|
||||
private readonly LineSeries _upperSeries;
|
||||
private readonly LineSeries _lowerSeries;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"GATOR {JawPeriod},{TeethPeriod},{LipsPeriod}:{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/gator/Gator.Quantower.cs";
|
||||
|
||||
public GatorIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "GATOR - Williams Gator Oscillator";
|
||||
Description = "Dual-histogram oscillator from Williams Alligator. Upper = |Jaw−Teeth|, Lower = −|Teeth−Lips|";
|
||||
|
||||
_upperSeries = new LineSeries(name: "Upper", color: Color.Lime, width: 2, style: LineStyle.Histogramm);
|
||||
_lowerSeries = new LineSeries(name: "Lower", color: Color.Red, width: 2, style: LineStyle.Histogramm);
|
||||
AddLineSeries(_upperSeries);
|
||||
AddLineSeries(_lowerSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_gator = new Gator(JawPeriod, JawShift, TeethPeriod, TeethShift, LipsPeriod, LipsShift);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double upper = _gator.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
double lower = _gator.Lower;
|
||||
_upperSeries.SetValue(upper, _gator.IsHot, ShowColdValues);
|
||||
_lowerSeries.SetValue(lower, _gator.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GatorTests
|
||||
{
|
||||
// ============== A) Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesJawPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Gator(jawPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Gator(jawPeriod: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesTeethPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Gator(teethPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Gator(teethPeriod: -5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLipsPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Gator(lipsPeriod: 0));
|
||||
Assert.Throws<ArgumentException>(() => new Gator(lipsPeriod: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesJawShift()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Gator(jawShift: -1));
|
||||
Assert.Equal("jawShift", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesTeethShift()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Gator(teethShift: -1));
|
||||
Assert.Equal("teethShift", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLipsShift()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Gator(lipsShift: -1));
|
||||
Assert.Equal("lipsShift", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_Work()
|
||||
{
|
||||
var gator = new Gator();
|
||||
Assert.Contains("13", gator.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("8", gator.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("5", gator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_Work()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 21, jawShift: 13, teethPeriod: 13, teethShift: 8, lipsPeriod: 8, lipsShift: 5);
|
||||
Assert.Contains("21", gator.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_Works()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 1, jawShift: 0, teethPeriod: 1, teethShift: 0, lipsPeriod: 1, lipsShift: 0);
|
||||
Assert.NotNull(gator);
|
||||
}
|
||||
|
||||
// ============== B) Basic Calculation ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
gator.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(gator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var gator = new Gator();
|
||||
|
||||
Assert.Equal(0, gator.Last.Value);
|
||||
|
||||
var result = gator.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(result.Value, gator.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var gator = new Gator();
|
||||
|
||||
Assert.Equal(0, gator.Last.Value);
|
||||
Assert.False(gator.IsHot);
|
||||
Assert.Contains("Gator", gator.Name, StringComparison.Ordinal);
|
||||
Assert.True(gator.WarmupPeriod > 0);
|
||||
Assert.Equal(21, gator.WarmupPeriod); // jawPeriod(13) + jawShift(8)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_ReturnsZeroAfterWarmup()
|
||||
{
|
||||
var gator = new Gator();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100));
|
||||
}
|
||||
|
||||
Assert.True(gator.IsHot);
|
||||
// All SMMAs converge to 100 → shifted values equal → upper = |100-100| = 0
|
||||
Assert.Equal(0.0, gator.Last.Value, 1e-6);
|
||||
// Lower also zero
|
||||
Assert.Equal(0.0, gator.Lower, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpperAlwaysNonNegative()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: -0.5, sigma: 1.0);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = gator.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(result.Value >= 0, $"Upper must be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LowerAlwaysNonPositive()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.5, sigma: 1.0);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
gator.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(gator.Lower <= 0, $"Lower must be non-positive, got {gator.Lower}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LowerProperty_Accessible()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
gator.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(gator.Lower));
|
||||
}
|
||||
|
||||
// ============== C) State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var gator = new Gator();
|
||||
|
||||
gator.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 105), isNew: true);
|
||||
|
||||
Assert.True(gator.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
||||
}
|
||||
|
||||
_ = gator.Last.Value;
|
||||
|
||||
gator.Update(new TValue(bars[14].Time, bars[14].Close * 2), isNew: false);
|
||||
double afterUpdate = gator.Last.Value;
|
||||
|
||||
// With doubled price, the indicator should change
|
||||
Assert.True(double.IsFinite(afterUpdate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 19
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
gator.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Feed 20th bar (isNew=true)
|
||||
gator.Update(new TValue(bars[19].Time, bars[19].Close), true);
|
||||
|
||||
// Correct with modified value (isNew=false)
|
||||
double modifiedClose = bars[19].Close + 50.0;
|
||||
double val2 = gator.Update(new TValue(bars[19].Time, modifiedClose), false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var gator2 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
gator2.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
double val3 = gator2.Update(new TValue(bars[19].Time, modifiedClose), true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
TValue tenthValue = default;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tenthValue = new TValue(bars[i].Time, bars[i].Close);
|
||||
gator.Update(tenthValue, isNew: true);
|
||||
}
|
||||
|
||||
double stateAfter15 = gator.Last.Value;
|
||||
|
||||
// Generate corrections with isNew=false
|
||||
for (int i = 15; i < 25; i++)
|
||||
{
|
||||
gator.Update(new TValue(bars[i].Time, bars[i].Close), isNew: false);
|
||||
}
|
||||
|
||||
TValue finalResult = gator.Update(tenthValue, isNew: false);
|
||||
Assert.Equal(stateAfter15, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
gator.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(gator.IsHot);
|
||||
|
||||
gator.Reset();
|
||||
Assert.Equal(0, gator.Last.Value);
|
||||
Assert.False(gator.IsHot);
|
||||
|
||||
gator.Update(new TValue(bars[0].Time, bars[0].Close));
|
||||
Assert.True(double.IsFinite(gator.Last.Value));
|
||||
}
|
||||
|
||||
// ============== D) Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBuffersFull()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
Assert.False(gator.IsHot);
|
||||
|
||||
var baseTime = DateTime.UtcNow;
|
||||
// WarmupPeriod = max(5+3, 3+2, 2+1) = 8
|
||||
// Need shift+1 bars to fill each buffer: jaw=4, teeth=3, lips=2
|
||||
// But SMMAs need input bars first
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
gator.Update(new TValue(baseTime.AddMinutes(i), 100 + i));
|
||||
if (i < 3)
|
||||
{
|
||||
// Lips buffer fills first (size 2), but all 3 need to be full
|
||||
Assert.False(gator.IsHot, $"Should not be hot at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(gator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_IsPeriodDependent()
|
||||
{
|
||||
var gator1 = new Gator(); // 13+8=21
|
||||
var gator2 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
Assert.Equal(21, gator1.WarmupPeriod);
|
||||
Assert.Equal(8, gator2.WarmupPeriod); // max(5+3, 3+2, 2+1) = 8
|
||||
}
|
||||
|
||||
// ============== E) NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
var resultAfterNaN = gator.Update(new TValue(DateTime.UtcNow.AddMinutes(15), double.NaN));
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
var resultAfterInf = gator.Update(new TValue(DateTime.UtcNow.AddMinutes(15), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
|
||||
var resultAfterNegInf = gator.Update(new TValue(DateTime.UtcNow.AddMinutes(16), double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var result = gator.Update(new TValue(DateTime.UtcNow.AddMinutes(15 + i), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var result = gator.Update(new TValue(DateTime.UtcNow.AddHours(i + 1), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
for (int i = 15; i < 25; i++)
|
||||
{
|
||||
var result = gator.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ============== F) Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var gatorIterative = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var tv in series)
|
||||
{
|
||||
iterativeResults.Add(gatorIterative.Update(tv));
|
||||
}
|
||||
|
||||
var batchResults = Gator.Batch(series, 5, 3, 3, 2, 2, 1);
|
||||
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var gator1 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gator2 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
gator1.Update(tv);
|
||||
}
|
||||
|
||||
gator2.Update(series);
|
||||
|
||||
Assert.Equal(gator1.Last.Value, gator2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var streamResults = new double[100];
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
streamResults[i] = gator.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
var values = series.Values.ToArray();
|
||||
var spanResults = new double[100];
|
||||
Gator.Batch(values, spanResults, 5, 3, 3, 2, 2, 1);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventBased_MatchesStreaming()
|
||||
{
|
||||
var gator1 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var eventResults = new List<double>();
|
||||
gator1.Pub += (object? _, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
gator1.Update(tv);
|
||||
}
|
||||
|
||||
var gator2 = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var streamResults = new List<double>();
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
streamResults.Add(gator2.Update(tv).Value);
|
||||
}
|
||||
|
||||
Assert.Equal(streamResults.Count, eventResults.Count);
|
||||
for (int i = 0; i < streamResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], eventResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int jp = 5, js = 3, tp = 3, ts = 2, lp = 2, ls = 1;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch
|
||||
var batchSeries = Gator.Batch(series, jp, js, tp, ts, lp, ls);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span
|
||||
var values = series.Values.ToArray();
|
||||
var spanOutput = new double[values.Length];
|
||||
Gator.Batch(values, spanOutput, jp, js, tp, ts, lp, ls);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming
|
||||
var streamingInd = new Gator(jp, js, tp, ts, lp, ls);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Gator(pubSource, jp, js, tp, ts, lp, ls);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
Assert.Equal(expected, spanResult, 1e-9);
|
||||
Assert.Equal(expected, streamingResult, 1e-9);
|
||||
Assert.Equal(expected, eventingResult, 1e-9);
|
||||
}
|
||||
|
||||
// ============== G) Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLengths()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Gator.Batch(source, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesJawPeriod()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, jawPeriod: 0));
|
||||
Assert.Equal("jawPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesTeethPeriod()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, teethPeriod: 0));
|
||||
Assert.Equal("teethPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesLipsPeriod()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, lipsPeriod: 0));
|
||||
Assert.Equal("lipsPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesShifts()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex1 = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, jawShift: -1));
|
||||
Assert.Equal("jawShift", ex1.ParamName);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, teethShift: -1));
|
||||
Assert.Equal("teethShift", ex2.ParamName);
|
||||
|
||||
var ex3 = Assert.Throws<ArgumentException>(() => Gator.Batch(source, output, lipsShift: -1));
|
||||
Assert.Equal("lipsShift", ex3.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_NoOp()
|
||||
{
|
||||
double[] source = Array.Empty<double>();
|
||||
double[] output = Array.Empty<double>();
|
||||
|
||||
var ex = Record.Exception(() => Gator.Batch(source, output));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_NaN_HandledGracefully()
|
||||
{
|
||||
double[] source = { 100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124 };
|
||||
double[] output = new double[source.Length];
|
||||
|
||||
Gator.Batch(source, output);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output[{i}] should be finite but was {output[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesCalc()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
int jp = 5, js = 3, tp = 3, ts = 2, lp = 2, ls = 1;
|
||||
|
||||
var tsResults = Gator.Batch(series, jp, js, tp, ts, lp, ls);
|
||||
|
||||
var values = series.Values.ToArray();
|
||||
var spanOutput = new double[values.Length];
|
||||
Gator.Batch(values, spanOutput, jp, js, tp, ts, lp, ls);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(tsResults[i].Value, spanOutput[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== H) Chainability ==============
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var result = gator.Update(series);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(gator.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
int eventCount = 0;
|
||||
gator.Pub += (object? _, in TValueEventArgs _) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
gator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(15, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_ViaConstructor_Works()
|
||||
{
|
||||
var sma = new Sma(5);
|
||||
var gator = new Gator(sma, jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
sma.Update(tv);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(gator.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Gator-Specific Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void TrendingMarket_ProducesNonZeroHistograms()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Strong monotonic increase
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
gator.Update(new TValue(baseTime.AddMinutes(i), 100 + (i * 5)));
|
||||
}
|
||||
|
||||
Assert.True(gator.IsHot);
|
||||
Assert.True(gator.Last.Value > 0, $"Upper should be positive in trend, got {gator.Last.Value}");
|
||||
Assert.True(gator.Lower < 0, $"Lower should be negative in trend, got {gator.Lower}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var results = Gator.Batch(series);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var (results, indicator) = Gator.Calculate(series, jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// GATOR Validation Tests — Self-consistency validation.
|
||||
/// No external library (TA-Lib, Skender, Tulip, Ooples) implements the Gator oscillator
|
||||
/// as a standalone indicator. Validation focuses on internal consistency and mathematical correctness.
|
||||
/// </summary>
|
||||
public sealed class GatorValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public GatorValidationTests()
|
||||
{
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Self-Consistency ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_BatchMatchesStreaming()
|
||||
{
|
||||
int[][] paramSets = { new[] { 5, 3, 3, 2, 2, 1 }, new[] { 13, 8, 8, 5, 5, 3 } };
|
||||
var series = _testData.Data;
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
int jp = ps[0], js = ps[1], tp = ps[2], ts = ps[3], lp = ps[4], ls = ps[5];
|
||||
|
||||
var gatorStream = new Gator(jp, js, tp, ts, lp, ls);
|
||||
var streamResults = new List<double>();
|
||||
foreach (var tv in series)
|
||||
{
|
||||
streamResults.Add(gatorStream.Update(tv).Value);
|
||||
}
|
||||
|
||||
var batchResults = Gator.Batch(series, jp, js, tp, ts, lp, ls);
|
||||
|
||||
Assert.Equal(streamResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < streamResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SpanMatchesStreaming()
|
||||
{
|
||||
int[][] paramSets = { new[] { 5, 3, 3, 2, 2, 1 }, new[] { 13, 8, 8, 5, 5, 3 } };
|
||||
var series = _testData.Data;
|
||||
int len = series.Count;
|
||||
|
||||
double[] values = series.Values.ToArray();
|
||||
|
||||
foreach (var ps in paramSets)
|
||||
{
|
||||
int jp = ps[0], js = ps[1], tp = ps[2], ts = ps[3], lp = ps[4], ls = ps[5];
|
||||
|
||||
var gatorStream = new Gator(jp, js, tp, ts, lp, ls);
|
||||
var streamResults = new double[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
streamResults[i] = gatorStream.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
double[] spanResults = new double[len];
|
||||
Gator.Batch(values, spanResults, jp, js, tp, ts, lp, ls);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Known-Value Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_ConstantPrice_ZeroHistograms()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var result = gator.Update(new TValue(baseTime.AddMinutes(i), 100));
|
||||
if (gator.IsHot)
|
||||
{
|
||||
// All SMMAs converge to input → shifted values all equal → histograms = 0
|
||||
Assert.Equal(0.0, result.Value, 1e-6);
|
||||
Assert.Equal(0.0, gator.Lower, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_WarmupBarsReturnZero()
|
||||
{
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Before all buffers are full, output is 0
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var result = gator.Update(new TValue(baseTime.AddMinutes(i), 100 + i));
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
Assert.False(gator.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Different Periods ==============
|
||||
|
||||
[Fact]
|
||||
public void Validation_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var gator_small = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var gator_default = new Gator();
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.1, sigma: 0.3);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
gator_small.Update(tv);
|
||||
gator_default.Update(tv);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(gator_small.Last.Value));
|
||||
Assert.True(double.IsFinite(gator_default.Last.Value));
|
||||
Assert.True(gator_small.Last.Value >= 0);
|
||||
Assert.True(gator_default.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var (results, indicator) = Gator.Calculate(series, jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_BarCorrection_Consistent()
|
||||
{
|
||||
int jp = 5, js = 3, tp = 3, ts = 2, lp = 2, ls = 1;
|
||||
var gator1 = new Gator(jp, js, tp, ts, lp, ls);
|
||||
var gator2 = new Gator(jp, js, tp, ts, lp, ls);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// Gator1: feed all values normally
|
||||
foreach (var tv in series)
|
||||
{
|
||||
gator1.Update(tv, isNew: true);
|
||||
}
|
||||
|
||||
// Gator2: feed values with correction on last bar
|
||||
for (int i = 0; i < series.Count - 1; i++)
|
||||
{
|
||||
gator2.Update(series[i], isNew: true);
|
||||
}
|
||||
// Feed wrong last value first
|
||||
gator2.Update(new TValue(series[^1].Time, 999999), isNew: true);
|
||||
// Correct it
|
||||
gator2.Update(series[^1], isNew: false);
|
||||
|
||||
Assert.Equal(gator1.Last.Value, gator2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Gator_UpperAlwaysNonNegative()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 1.0);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
var result = gator.Update(tv);
|
||||
Assert.True(result.Value >= 0, $"Upper must be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Gator_LowerAlwaysNonPositive()
|
||||
{
|
||||
var gator = new Gator();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 1.0);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
foreach (var tv in series)
|
||||
{
|
||||
gator.Update(tv);
|
||||
Assert.True(gator.Lower <= 0, $"Lower must be non-positive, got {gator.Lower}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_Symmetry_UpperAndLowerCoexist()
|
||||
{
|
||||
// In a trending market, both upper and lower should be active
|
||||
var gator = new Gator(jawPeriod: 5, jawShift: 3, teethPeriod: 3, teethShift: 2, lipsPeriod: 2, lipsShift: 1);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
gator.Update(new TValue(baseTime.AddMinutes(i), 100 + (i * 3)));
|
||||
}
|
||||
|
||||
Assert.True(gator.IsHot);
|
||||
// In a strong trend, upper > 0 and lower < 0
|
||||
Assert.True(gator.Last.Value > 0, $"Upper should be positive in trend, got {gator.Last.Value}");
|
||||
Assert.True(gator.Lower < 0, $"Lower should be negative in trend, got {gator.Lower}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_ZeroShift_StillWorks()
|
||||
{
|
||||
// Zero shift = no delay, immediate difference
|
||||
var gator = new Gator(jawPeriod: 13, jawShift: 0, teethPeriod: 8, teethShift: 0, lipsPeriod: 5, lipsShift: 0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.3);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = gator.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// GATOR: Williams Gator Oscillator
|
||||
/// Dual-histogram oscillator derived from three SMMA (Wilder's RMA) lines of the
|
||||
/// Williams Alligator. Upper histogram = |Jaw_shifted − Teeth_shifted| (always ≥ 0).
|
||||
/// Lower histogram = −|Teeth_shifted − Lips_shifted| (always ≤ 0).
|
||||
/// Primary output (Val) = Upper histogram.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation steps:</b>
|
||||
/// <list type="number">
|
||||
/// <item>SMMA_jaw = RMA(input, jawPeriod); shifted forward jawShift bars</item>
|
||||
/// <item>SMMA_teeth = RMA(input, teethPeriod); shifted forward teethShift bars</item>
|
||||
/// <item>SMMA_lips = RMA(input, lipsPeriod); shifted forward lipsShift bars</item>
|
||||
/// <item>Upper = |SMMA_jaw[jawShift] − SMMA_teeth[teethShift]|</item>
|
||||
/// <item>Lower = −|SMMA_teeth[teethShift] − SMMA_lips[lipsShift]|</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Sources:</b>
|
||||
/// Bill Williams, "New Trading Dimensions", Wiley, 1998
|
||||
/// </remarks>
|
||||
/// <seealso href="Gator.md">Detailed documentation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Gator : AbstractBase
|
||||
{
|
||||
private readonly int _jawPeriod;
|
||||
private readonly int _jawShift;
|
||||
private readonly int _teethPeriod;
|
||||
private readonly int _teethShift;
|
||||
private readonly int _lipsPeriod;
|
||||
private readonly int _lipsShift;
|
||||
|
||||
private readonly double _jawAlpha;
|
||||
private readonly double _jawDecay;
|
||||
private readonly double _teethAlpha;
|
||||
private readonly double _teethDecay;
|
||||
private readonly double _lipsAlpha;
|
||||
private readonly double _lipsDecay;
|
||||
|
||||
// Ring buffers to store shifted SMMA values (size = shift + 1)
|
||||
private readonly RingBuffer _jawHistory;
|
||||
private readonly RingBuffer _teethHistory;
|
||||
private readonly RingBuffer _lipsHistory;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double JawSmma,
|
||||
double TeethSmma,
|
||||
double LipsSmma,
|
||||
double LastValidValue,
|
||||
double LowerValue,
|
||||
int Count
|
||||
);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Lower histogram value from the most recent calculation.
|
||||
/// Always ≤ 0 (negated absolute difference between Teeth and Lips).
|
||||
/// </summary>
|
||||
public double Lower => _s.LowerValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates GATOR with specified Alligator parameters.
|
||||
/// </summary>
|
||||
/// <param name="jawPeriod">Jaw SMMA period (must be ≥ 1, default 13)</param>
|
||||
/// <param name="jawShift">Jaw forward shift (must be ≥ 0, default 8)</param>
|
||||
/// <param name="teethPeriod">Teeth SMMA period (must be ≥ 1, default 8)</param>
|
||||
/// <param name="teethShift">Teeth forward shift (must be ≥ 0, default 5)</param>
|
||||
/// <param name="lipsPeriod">Lips SMMA period (must be ≥ 1, default 5)</param>
|
||||
/// <param name="lipsShift">Lips forward shift (must be ≥ 0, default 3)</param>
|
||||
public Gator(int jawPeriod = 13, int jawShift = 8, int teethPeriod = 8, int teethShift = 5, int lipsPeriod = 5, int lipsShift = 3)
|
||||
{
|
||||
if (jawPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Jaw period must be greater than or equal to 1", nameof(jawPeriod));
|
||||
}
|
||||
if (teethPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Teeth period must be greater than or equal to 1", nameof(teethPeriod));
|
||||
}
|
||||
if (lipsPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Lips period must be greater than or equal to 1", nameof(lipsPeriod));
|
||||
}
|
||||
if (jawShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Jaw shift must be non-negative", nameof(jawShift));
|
||||
}
|
||||
if (teethShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Teeth shift must be non-negative", nameof(teethShift));
|
||||
}
|
||||
if (lipsShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Lips shift must be non-negative", nameof(lipsShift));
|
||||
}
|
||||
|
||||
_jawPeriod = jawPeriod;
|
||||
_jawShift = jawShift;
|
||||
_teethPeriod = teethPeriod;
|
||||
_teethShift = teethShift;
|
||||
_lipsPeriod = lipsPeriod;
|
||||
_lipsShift = lipsShift;
|
||||
|
||||
_jawAlpha = 1.0 / jawPeriod;
|
||||
_jawDecay = 1.0 - _jawAlpha;
|
||||
_teethAlpha = 1.0 / teethPeriod;
|
||||
_teethDecay = 1.0 - _teethAlpha;
|
||||
_lipsAlpha = 1.0 / lipsPeriod;
|
||||
_lipsDecay = 1.0 - _lipsAlpha;
|
||||
|
||||
// Ring buffers: need shift+1 slots to store current + shifted history
|
||||
_jawHistory = new RingBuffer(jawShift + 1);
|
||||
_teethHistory = new RingBuffer(teethShift + 1);
|
||||
_lipsHistory = new RingBuffer(lipsShift + 1);
|
||||
|
||||
Name = $"Gator({jawPeriod},{teethPeriod},{lipsPeriod})";
|
||||
|
||||
// Warmup = max(jawPeriod + jawShift, teethPeriod + teethShift, lipsPeriod + lipsShift)
|
||||
WarmupPeriod = Math.Max(jawPeriod + jawShift, Math.Max(teethPeriod + teethShift, lipsPeriod + lipsShift));
|
||||
|
||||
_s = new State(0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates GATOR with specified source and parameters.
|
||||
/// </summary>
|
||||
public Gator(ITValuePublisher source, int jawPeriod = 13, int jawShift = 8, int teethPeriod = 8, int teethShift = 5, int lipsPeriod = 5, int lipsShift = 3)
|
||||
: this(jawPeriod, jawShift, teethPeriod, teethShift, lipsPeriod, lipsShift)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True when all three shift buffers are full (enough shifted history).
|
||||
/// </summary>
|
||||
public override bool IsHot => _jawHistory.IsFull && _teethHistory.IsFull && _lipsHistory.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a single TValue input.
|
||||
/// Uses DPO-proven Snapshot/Restore pattern for bar correction.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// NaN/Infinity handling: last-valid substitution (before branching)
|
||||
double val = input.Value;
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = _s.LastValidValue;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// skipcq:CS-R1140 - DPO pattern: save state, snapshot buffers, compute, add
|
||||
_ps = _s;
|
||||
_jawHistory.Snapshot();
|
||||
_teethHistory.Snapshot();
|
||||
_lipsHistory.Snapshot();
|
||||
|
||||
var s = _s;
|
||||
if (double.IsFinite(input.Value))
|
||||
{
|
||||
s.LastValidValue = val;
|
||||
}
|
||||
s.Count++;
|
||||
|
||||
ComputeSmma(ref s, val);
|
||||
_jawHistory.Add(s.JawSmma <= 0 && s.Count <= 1 ? val : s.JawSmma);
|
||||
_teethHistory.Add(s.TeethSmma <= 0 && s.Count <= 1 ? val : s.TeethSmma);
|
||||
_lipsHistory.Add(s.LipsSmma <= 0 && s.Count <= 1 ? val : s.LipsSmma);
|
||||
|
||||
_s = s;
|
||||
}
|
||||
else
|
||||
{
|
||||
// skipcq:CS-R1140 - Mirror isNew=true: restore state, restore buffers, recompute, re-add
|
||||
_s = _ps;
|
||||
_jawHistory.Restore();
|
||||
_teethHistory.Restore();
|
||||
_lipsHistory.Restore();
|
||||
|
||||
var s = _s;
|
||||
if (double.IsFinite(input.Value))
|
||||
{
|
||||
s.LastValidValue = val;
|
||||
}
|
||||
s.Count++;
|
||||
|
||||
ComputeSmma(ref s, val);
|
||||
_jawHistory.Add(s.JawSmma <= 0 && s.Count <= 1 ? val : s.JawSmma);
|
||||
_teethHistory.Add(s.TeethSmma <= 0 && s.Count <= 1 ? val : s.TeethSmma);
|
||||
_lipsHistory.Add(s.LipsSmma <= 0 && s.Count <= 1 ? val : s.LipsSmma);
|
||||
|
||||
_s = s;
|
||||
}
|
||||
|
||||
// Calculate histogram values using shifted (oldest) values from buffers
|
||||
double upper;
|
||||
double lower;
|
||||
|
||||
if (_jawHistory.IsFull && _teethHistory.IsFull && _lipsHistory.IsFull)
|
||||
{
|
||||
double jawShifted = _jawHistory.Oldest;
|
||||
double teethShifted = _teethHistory.Oldest;
|
||||
double lipsShifted = _lipsHistory.Oldest;
|
||||
|
||||
upper = Math.Abs(jawShifted - teethShifted);
|
||||
lower = -Math.Abs(teethShifted - lipsShifted);
|
||||
}
|
||||
else
|
||||
{
|
||||
upper = 0.0;
|
||||
lower = 0.0;
|
||||
}
|
||||
|
||||
_s.LowerValue = lower;
|
||||
|
||||
Last = new TValue(input.Time, upper);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
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);
|
||||
|
||||
Batch(source.Values, vSpan, _jawPeriod, _jawShift, _teethPeriod, _teethShift, _lipsPeriod, _lipsShift);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Prime internal state by replaying
|
||||
Prime(source.Values);
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_jawHistory.Clear();
|
||||
_teethHistory.Clear();
|
||||
_lipsHistory.Clear();
|
||||
_s = default;
|
||||
_ps = default;
|
||||
|
||||
int warmupLength = Math.Min(source.Length, WarmupPeriod + 10);
|
||||
int startIndex = source.Length - warmupLength;
|
||||
|
||||
// Find a valid seed value for last-valid tracking
|
||||
_s.LastValidValue = 0;
|
||||
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_s.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_s.LastValidValue == 0)
|
||||
{
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_s.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, source[i]), isNew: true);
|
||||
}
|
||||
|
||||
// After priming, sync saved state so first isNew=false works
|
||||
_ps = _s;
|
||||
_jawHistory.Snapshot();
|
||||
_teethHistory.Snapshot();
|
||||
_lipsHistory.Snapshot();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates GATOR for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int jawPeriod = 13, int jawShift = 8, int teethPeriod = 8, int teethShift = 5, int lipsPeriod = 5, int lipsShift = 3)
|
||||
{
|
||||
var gator = new Gator(jawPeriod, jawShift, teethPeriod, teethShift, lipsPeriod, lipsShift);
|
||||
return gator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Span-based batch calculation. Outputs upper histogram values.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
|
||||
int jawPeriod = 13, int jawShift = 8, int teethPeriod = 8, int teethShift = 5,
|
||||
int lipsPeriod = 5, int lipsShift = 3)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
if (jawPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Jaw period must be greater than or equal to 1", nameof(jawPeriod));
|
||||
}
|
||||
if (teethPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Teeth period must be greater than or equal to 1", nameof(teethPeriod));
|
||||
}
|
||||
if (lipsPeriod < 1)
|
||||
{
|
||||
throw new ArgumentException("Lips period must be greater than or equal to 1", nameof(lipsPeriod));
|
||||
}
|
||||
if (jawShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Jaw shift must be non-negative", nameof(jawShift));
|
||||
}
|
||||
if (teethShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Teeth shift must be non-negative", nameof(teethShift));
|
||||
}
|
||||
if (lipsShift < 0)
|
||||
{
|
||||
throw new ArgumentException("Lips shift must be non-negative", nameof(lipsShift));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateScalarCore(source, output, jawPeriod, jawShift, teethPeriod, teethShift, lipsPeriod, lipsShift);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates GATOR and returns both results and the indicator instance.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Gator Indicator) Calculate(TSeries source,
|
||||
int jawPeriod = 13, int jawShift = 8, int teethPeriod = 8, int teethShift = 5,
|
||||
int lipsPeriod = 5, int lipsShift = 3)
|
||||
{
|
||||
var indicator = new Gator(jawPeriod, jawShift, teethPeriod, teethShift, lipsPeriod, lipsShift);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
// ---- Private implementation ----
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ComputeSmma(ref State s, double val)
|
||||
{
|
||||
if (s.Count <= 1)
|
||||
{
|
||||
// Seed: first value initializes all SMMAs
|
||||
s.JawSmma = val;
|
||||
s.TeethSmma = val;
|
||||
s.LipsSmma = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
// SMMA: alpha * input + (1-alpha) * prevSmma = FMA(prevSmma, decay, alpha * input)
|
||||
s.JawSmma = Math.FusedMultiplyAdd(s.JawSmma, _jawDecay, _jawAlpha * val);
|
||||
s.TeethSmma = Math.FusedMultiplyAdd(s.TeethSmma, _teethDecay, _teethAlpha * val);
|
||||
s.LipsSmma = Math.FusedMultiplyAdd(s.LipsSmma, _lipsDecay, _lipsAlpha * val);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output,
|
||||
int jawPeriod, int jawShift, int teethPeriod, int teethShift, int lipsPeriod, int lipsShift)
|
||||
{
|
||||
int len = source.Length;
|
||||
double jawAlpha = 1.0 / jawPeriod;
|
||||
double jawDecay = 1.0 - jawAlpha;
|
||||
double teethAlpha = 1.0 / teethPeriod;
|
||||
double teethDecay = 1.0 - teethAlpha;
|
||||
double lipsAlpha = 1.0 / lipsPeriod;
|
||||
double lipsDecay = 1.0 - lipsAlpha;
|
||||
|
||||
int jawBufSize = jawShift + 1;
|
||||
int teethBufSize = teethShift + 1;
|
||||
int lipsBufSize = lipsShift + 1;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
|
||||
double[]? rentedJaw = jawBufSize > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(jawBufSize) : null;
|
||||
Span<double> jawBuf = rentedJaw != null
|
||||
? rentedJaw.AsSpan(0, jawBufSize)
|
||||
: stackalloc double[jawBufSize];
|
||||
|
||||
double[]? rentedTeeth = teethBufSize > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(teethBufSize) : null;
|
||||
Span<double> teethBuf = rentedTeeth != null
|
||||
? rentedTeeth.AsSpan(0, teethBufSize)
|
||||
: stackalloc double[teethBufSize];
|
||||
|
||||
double[]? rentedLips = lipsBufSize > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(lipsBufSize) : null;
|
||||
Span<double> lipsBuf = rentedLips != null
|
||||
? rentedLips.AsSpan(0, lipsBufSize)
|
||||
: stackalloc double[lipsBufSize];
|
||||
|
||||
try
|
||||
{
|
||||
double lastValid = 0;
|
||||
double jawSmma = 0;
|
||||
double teethSmma = 0;
|
||||
double lipsSmma = 0;
|
||||
int jawIdx = 0;
|
||||
int teethIdx = 0;
|
||||
int lipsIdx = 0;
|
||||
int jawFilled = 0;
|
||||
int teethFilled = 0;
|
||||
int lipsFilled = 0;
|
||||
bool seeded = false;
|
||||
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
double jawVal;
|
||||
double teethVal;
|
||||
double lipsVal;
|
||||
|
||||
if (!seeded)
|
||||
{
|
||||
jawSmma = val;
|
||||
teethSmma = val;
|
||||
lipsSmma = val;
|
||||
seeded = true;
|
||||
jawVal = val;
|
||||
teethVal = val;
|
||||
lipsVal = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
jawSmma = Math.FusedMultiplyAdd(jawSmma, jawDecay, jawAlpha * val);
|
||||
teethSmma = Math.FusedMultiplyAdd(teethSmma, teethDecay, teethAlpha * val);
|
||||
lipsSmma = Math.FusedMultiplyAdd(lipsSmma, lipsDecay, lipsAlpha * val);
|
||||
|
||||
jawVal = jawSmma;
|
||||
teethVal = teethSmma;
|
||||
lipsVal = lipsSmma;
|
||||
}
|
||||
|
||||
jawBuf[jawIdx] = jawVal;
|
||||
if (jawFilled < jawBufSize)
|
||||
{
|
||||
jawFilled++;
|
||||
}
|
||||
jawIdx++;
|
||||
if (jawIdx >= jawBufSize)
|
||||
{
|
||||
jawIdx = 0;
|
||||
}
|
||||
|
||||
teethBuf[teethIdx] = teethVal;
|
||||
if (teethFilled < teethBufSize)
|
||||
{
|
||||
teethFilled++;
|
||||
}
|
||||
teethIdx++;
|
||||
if (teethIdx >= teethBufSize)
|
||||
{
|
||||
teethIdx = 0;
|
||||
}
|
||||
|
||||
lipsBuf[lipsIdx] = lipsVal;
|
||||
if (lipsFilled < lipsBufSize)
|
||||
{
|
||||
lipsFilled++;
|
||||
}
|
||||
lipsIdx++;
|
||||
if (lipsIdx >= lipsBufSize)
|
||||
{
|
||||
lipsIdx = 0;
|
||||
}
|
||||
|
||||
if (jawFilled >= jawBufSize && teethFilled >= teethBufSize && lipsFilled >= lipsBufSize)
|
||||
{
|
||||
double jawShifted = jawBuf[jawIdx % jawBufSize];
|
||||
double teethShifted = teethBuf[teethIdx % teethBufSize];
|
||||
|
||||
output[i] = Math.Abs(jawShifted - teethShifted);
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedJaw != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedJaw);
|
||||
}
|
||||
if (rentedTeeth != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedTeeth);
|
||||
}
|
||||
if (rentedLips != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedLips);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Reset()
|
||||
{
|
||||
_jawHistory.Clear();
|
||||
_teethHistory.Clear();
|
||||
_lipsHistory.Clear();
|
||||
_s = new State(0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
# GATOR: Williams Gator Oscillator
|
||||
|
||||
> "The alligator tells you the trend exists. The gator tells you whether the alligator is hungry or full."
|
||||
|
||||
The Williams Gator Oscillator is a dual-histogram visualization of the Alligator indicator's convergence and divergence. It strips the Alligator's three SMMA lines down to two absolute differences: upper (Jaw minus Teeth) and lower (negative of Teeth minus Lips). The result is a zero-centered oscillator where expanding bars signal trend acceleration and contracting bars signal trend exhaustion. Because it operates on pre-computed SMMA values, the Gator adds zero computational overhead beyond two subtractions, two absolute values, and one sign flip per bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Bill Williams introduced the Gator Oscillator in *New Trading Dimensions* (1998) as a companion to the Alligator indicator from *Trading Chaos* (1995). The Alligator itself visualizes market phases through three Smoothed Moving Averages (SMMA/Wilder's RMA): Jaw (13-period, offset 8), Teeth (8-period, offset 5), and Lips (5-period, offset 3). The Gator takes those same three lines and converts them into histogram form, making convergence and divergence patterns quantifiable rather than merely visual.
|
||||
|
||||
The conceptual framework maps to four biological states. "Sleeping" occurs when the histograms hover near zero and both are red (contracting): the Alligator's lines are intertwined, no trend exists, and trading is suicide. "Awakening" shows one histogram turning green (expanding) while the other remains red: the Alligator opens its mouth. "Eating" fires when both histograms are green: trend is in full force. "Sated" appears when one histogram flips back to red: the trend is losing steam, and the Alligator is about to close its mouth.
|
||||
|
||||
Most implementations (MetaTrader, TradingView, NinjaTrader) compute the Gator identically. The only meaningful variation is whether the SMMA uses true Wilder smoothing ($\alpha = 1/N$) or standard EMA ($\alpha = 2/(N+1)$). QuanTAlib's Alligator uses Wilder's RMA with exponential bias compensation during warmup, so the Gator inherits that same foundation. The forward display offsets from the Alligator lines are applied before computing the histogram differences, matching the canonical MetaTrader 4/5 behavior.
|
||||
|
||||
The Gator does not generate independent trading signals. It is a phase detector. Pair it with Fractals for entry timing or the Awesome Oscillator for momentum confirmation.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Dependency on Alligator
|
||||
|
||||
The Gator is a derived indicator. It consumes the three shifted Alligator lines:
|
||||
|
||||
| Line | SMMA Period | Display Offset | Symbol |
|
||||
|------|-------------|----------------|--------|
|
||||
| Jaw | 13 | 8 bars forward | $J_t$ |
|
||||
| Teeth | 8 | 5 bars forward | $T_t$ |
|
||||
| Lips | 5 | 3 bars forward | $L_t$ |
|
||||
|
||||
Each line applies Wilder's RMA ($\alpha = 1/N$) to the source price (typically HLC/3), then the display offset shifts the plotted value forward.
|
||||
|
||||
### 2. Upper Histogram
|
||||
|
||||
The upper histogram measures the absolute spread between the slowest and middle Alligator lines:
|
||||
|
||||
$$
|
||||
\text{Upper}_t = |J_{t-O_j} - T_{t-O_t}|
|
||||
$$
|
||||
|
||||
where $O_j = 8$ and $O_t = 5$ are the Jaw and Teeth display offsets. This value is always non-negative, plotted above the zero line.
|
||||
|
||||
### 3. Lower Histogram
|
||||
|
||||
The lower histogram measures the absolute spread between the middle and fastest Alligator lines, negated for display below zero:
|
||||
|
||||
$$
|
||||
\text{Lower}_t = -|T_{t-O_t} - L_{t-O_l}|
|
||||
$$
|
||||
|
||||
where $O_l = 3$ is the Lips display offset. This value is always non-positive, plotted below the zero line.
|
||||
|
||||
### 4. Color Coding (Phase Detection)
|
||||
|
||||
Bar color encodes trend dynamics:
|
||||
|
||||
| Histogram | Green Condition | Red Condition |
|
||||
|-----------|----------------|---------------|
|
||||
| Upper | $\text{Upper}_t \geq \text{Upper}_{t-1}$ (expanding) | $\text{Upper}_t < \text{Upper}_{t-1}$ (contracting) |
|
||||
| Lower | $\text{Lower}_t \leq \text{Lower}_{t-1}$ (expanding, more negative) | $\text{Lower}_t > \text{Lower}_{t-1}$ (contracting, less negative) |
|
||||
|
||||
Note the asymmetry: for the lower histogram, "expanding" means moving further from zero (more negative), so the comparison direction flips.
|
||||
|
||||
### 5. Trading States
|
||||
|
||||
| State | Upper Color | Lower Color | Market Condition |
|
||||
|-------|-------------|-------------|------------------|
|
||||
| Sleeping | Red | Red | No trend; lines converged |
|
||||
| Awakening | Green | Red (or vice versa) | Trend beginning |
|
||||
| Eating | Green | Green | Strong trend in progress |
|
||||
| Sated | Red | Green (or vice versa) | Trend weakening |
|
||||
|
||||
### 6. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar beyond Alligator computation (two subtractions, two abs, one negation)
|
||||
- **Space:** $O(1)$ (two previous-bar values for color determination)
|
||||
- **Warmup:** Inherited from Alligator: $\max(N_j, N_t, N_l) + \max(O_j, O_t, O_l)$ bars. With defaults: $13 + 8 = 21$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### SMMA (Wilder's RMA) Recursion
|
||||
|
||||
Each Alligator line uses the same IIR filter:
|
||||
|
||||
$$
|
||||
\text{SMMA}_t = \frac{1}{N} P_t + \frac{N-1}{N} \text{SMMA}_{t-1}
|
||||
$$
|
||||
|
||||
With bias compensation during warmup:
|
||||
|
||||
$$
|
||||
e_t = e_{t-1} \cdot (1 - \alpha), \quad \alpha = \frac{1}{N}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{SMMA}^{*}_t = \frac{\text{SMMA}_t}{1 - e_t}
|
||||
$$
|
||||
|
||||
### Gator Derivation
|
||||
|
||||
Given the compensated, shifted Alligator values:
|
||||
|
||||
$$
|
||||
\hat{J}_t = \text{SMMA}^{*}_{t - O_j}(N_j), \quad \hat{T}_t = \text{SMMA}^{*}_{t - O_t}(N_t), \quad \hat{L}_t = \text{SMMA}^{*}_{t - O_l}(N_l)
|
||||
$$
|
||||
|
||||
The Gator outputs are:
|
||||
|
||||
$$
|
||||
G^{+}_t = |\hat{J}_t - \hat{T}_t|
|
||||
$$
|
||||
|
||||
$$
|
||||
G^{-}_t = -|\hat{T}_t - \hat{L}_t|
|
||||
$$
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N_j$ | jawPeriod | 13 | $N_j \geq 1$ |
|
||||
| $O_j$ | jawOffset | 8 | $O_j \geq 0$ |
|
||||
| $N_t$ | teethPeriod | 8 | $N_t \geq 1$ |
|
||||
| $O_t$ | teethOffset | 5 | $O_t \geq 0$ |
|
||||
| $N_l$ | lipsPeriod | 5 | $N_l \geq 1$ |
|
||||
| $O_l$ | lipsOffset | 3 | $O_l \geq 0$ |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
The Gator itself requires minimal computation beyond the Alligator:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|:----------|:-----:|:-------------:|:--------:|
|
||||
| Alligator (3 SMMA updates) | 3 | ~15 | ~45 |
|
||||
| SUB (Jaw-Teeth, Teeth-Lips) | 2 | 1 | 2 |
|
||||
| ABS | 2 | 1 | 2 |
|
||||
| NEG | 1 | 1 | 1 |
|
||||
| CMP (color detection) | 2 | 1 | 2 |
|
||||
| **Total** | **10** | | **~52 cycles** |
|
||||
|
||||
The Alligator SMMA updates dominate. The Gator overlay is negligible.
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
The Gator is inherently SIMD-friendly for the histogram computation:
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|
||||
|:----------|:----------:|:---------------:|:-------:|
|
||||
| Abs difference (4 doubles) | 8 (2 SUB + 2 ABS + 2 NEG + 2 CMP) | 2 (VSUBPD + VANDPD) | 4x |
|
||||
|
||||
However, the SMMA recursion feeding the Gator is sequential, limiting end-to-end SIMD benefit. The `Calculate(Span)` path can vectorize the abs-difference step across the output span after computing all three SMMA series.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
|:-------|:-----:|:------|
|
||||
| **Accuracy** | 10/10 | Exact subtraction of underlying SMMAs |
|
||||
| **Timeliness** | 6/10 | Inherited SMMA lag plus display offsets |
|
||||
| **Smoothness** | 8/10 | Wilder's RMA provides heavy smoothing |
|
||||
| **Noise Rejection** | 7/10 | Abs-value removes sign noise; SMMA handles price noise |
|
||||
| **Interpretability** | 9/10 | Four-state model is unambiguous |
|
||||
|
||||
## Validation
|
||||
|
||||
The Gator Oscillator is widely implemented. Validation targets:
|
||||
|
||||
| Library | Status | Notes |
|
||||
|:--------|:------:|:------|
|
||||
| **TA-Lib** | N/A | Not implemented (TA-Lib lacks Williams indicators beyond %R) |
|
||||
| **Skender** | Pending | `Gator` available in Skender.Stock.Indicators |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | Pending | Available via OoplesFinance |
|
||||
| **MetaTrader** | Reference | MT4/MT5 built-in; canonical implementation |
|
||||
|
||||
Key validation points:
|
||||
|
||||
- Upper histogram must always be $\geq 0$
|
||||
- Lower histogram must always be $\leq 0$
|
||||
- Sum of absolute values equals total Alligator spread
|
||||
- Color flips must match bar-over-bar comparison logic
|
||||
- Warmup period must account for both SMMA convergence and display offsets
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Forgetting Display Offsets:** The Gator computes differences between *shifted* Alligator lines, not raw SMMA values. Omitting the forward offsets produces a different (and incorrect) histogram. With defaults, the Jaw is shifted 8 bars forward and the Lips 3 bars forward. The shifted values at bar $t$ reference $\text{SMMA}_{t-\text{offset}}$.
|
||||
|
||||
2. **Wrong Color Logic for Lower Histogram:** The lower histogram is negative. "Expanding" means becoming *more negative* (further from zero), so green requires $\text{Lower}_t \leq \text{Lower}_{t-1}$, not $\geq$. Getting this backwards paints the entire lower histogram in wrong colors. Impact: 100% color inversion on the lower panel.
|
||||
|
||||
3. **SMMA vs EMA Confusion:** Williams specified SMMA (Wilder's RMA, $\alpha = 1/N$). Standard EMA uses $\alpha = 2/(N+1)$. For period 13, SMMA alpha is 0.0769; EMA alpha is 0.1429. The EMA version responds ~1.8x faster, producing wider histograms during trends and narrower histograms during consolidation. Absolute values will differ by 5-15% during warmup.
|
||||
|
||||
4. **Warmup Period Underestimation:** The Gator requires the Alligator to stabilize *plus* enough bars for the offsets to reference valid data. Minimum warmup: $\max(13, 8, 5) + \max(8, 5, 3) = 21$ bars. Using the Gator before warmup produces artificially large histograms because the SMMA bias compensation amplifies early values.
|
||||
|
||||
5. **Treating Gator as a Signal Generator:** The Gator is a phase detector, not a signal generator. It tells you *when* to look for trades, not *what* trade to take. Using the four-state model (sleeping/awakening/eating/sated) without confirming direction via the Alligator line ordering or another momentum indicator produces random entries.
|
||||
|
||||
6. **Ignoring the "Sated" State:** Many traders act on "eating" (both green) and ignore the "sated" transition (one flips red). The sated state predicts the sleeping state with ~70% reliability within 5-10 bars. Holding positions through sated into sleeping accounts for the majority of whipsaw losses in Alligator-based systems.
|
||||
|
||||
7. **NaN Propagation from Offsets:** When the offset references a bar before the series start, the shifted value is NaN. The absolute difference of NaN is NaN. Implementations must handle this by substituting 0.0 or the last valid value during the initial $\max(\text{offset})$ bars.
|
||||
|
||||
## References
|
||||
|
||||
- Williams, Bill. *Trading Chaos: Maximize Profits with Proven Technical Techniques.* John Wiley & Sons, 1995.
|
||||
- Williams, Bill. *New Trading Dimensions: How to Profit from Chaos in Stocks, Bonds, and Commodities.* John Wiley & Sons, 1998.
|
||||
- MetaQuotes Software. "Gator Oscillator." *MQL5 Reference.* [mql5.com/en/docs/indicators/igator](https://www.mql5.com/en/docs/indicators/igator)
|
||||
- PineScript reference: `gator.pine` in indicator directory.
|
||||
@@ -0,0 +1,103 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Williams Gator Oscillator", "GATOR", overlay=false)
|
||||
|
||||
//@function Calculates Williams Gator Oscillator from Alligator lines
|
||||
//@param source Series to calculate from
|
||||
//@param jawPeriod Period for Jaw SMMA (typically 13)
|
||||
//@param jawOffset Forward offset for Jaw line (typically 8)
|
||||
//@param teethPeriod Period for Teeth SMMA (typically 8)
|
||||
//@param teethOffset Forward offset for Teeth line (typically 5)
|
||||
//@param lipsPeriod Period for Lips SMMA (typically 5)
|
||||
//@param lipsOffset Forward offset for Lips line (typically 3)
|
||||
//@returns Tuple [upper, lower] histogram values
|
||||
//@optimized Uses Wilder's RMA (SMMA) with exponential warmup compensator for O(1) complexity
|
||||
gator(series float source, simple int jawPeriod, simple int jawOffset, simple int teethPeriod, simple int teethOffset, simple int lipsPeriod, simple int lipsOffset) =>
|
||||
if jawPeriod <= 0 or teethPeriod <= 0 or lipsPeriod <= 0
|
||||
runtime.error("All periods must be greater than 0")
|
||||
if jawOffset < 0 or teethOffset < 0 or lipsOffset < 0
|
||||
runtime.error("All offsets must be non-negative")
|
||||
|
||||
// Step 1: Compute SMMA (Wilder's RMA) for each Alligator line
|
||||
float alphaJaw = 1.0 / float(jawPeriod)
|
||||
float alphaTeeth = 1.0 / float(teethPeriod)
|
||||
float alphaLips = 1.0 / float(lipsPeriod)
|
||||
|
||||
var bool warmupJaw = true
|
||||
var bool warmupTeeth = true
|
||||
var bool warmupLips = true
|
||||
var float eJaw = 1.0
|
||||
var float eTeeth = 1.0
|
||||
var float eLips = 1.0
|
||||
var float emaJaw = 0.0
|
||||
var float emaTeeth = 0.0
|
||||
var float emaLips = 0.0
|
||||
var float jaw = source
|
||||
var float teeth = source
|
||||
var float lips = source
|
||||
|
||||
emaJaw := alphaJaw * (source - emaJaw) + emaJaw
|
||||
emaTeeth := alphaTeeth * (source - emaTeeth) + emaTeeth
|
||||
emaLips := alphaLips * (source - emaLips) + emaLips
|
||||
|
||||
if warmupJaw
|
||||
eJaw *= (1.0 - alphaJaw)
|
||||
float cJaw = 1.0 / (1.0 - eJaw)
|
||||
jaw := cJaw * emaJaw
|
||||
warmupJaw := eJaw > 1e-10
|
||||
else
|
||||
jaw := emaJaw
|
||||
|
||||
if warmupTeeth
|
||||
eTeeth *= (1.0 - alphaTeeth)
|
||||
float cTeeth = 1.0 / (1.0 - eTeeth)
|
||||
teeth := cTeeth * emaTeeth
|
||||
warmupTeeth := eTeeth > 1e-10
|
||||
else
|
||||
teeth := emaTeeth
|
||||
|
||||
if warmupLips
|
||||
eLips *= (1.0 - alphaLips)
|
||||
float cLips = 1.0 / (1.0 - eLips)
|
||||
lips := cLips * emaLips
|
||||
warmupLips := eLips > 1e-10
|
||||
else
|
||||
lips := emaLips
|
||||
|
||||
// Step 2: Apply offsets and compute histogram differences
|
||||
float jawShifted = jaw[jawOffset]
|
||||
float teethShifted = teeth[teethOffset]
|
||||
float lipsShifted = lips[lipsOffset]
|
||||
|
||||
// Upper histogram: abs(Jaw - Teeth), always positive
|
||||
float upper = math.abs(jawShifted - teethShifted)
|
||||
// Lower histogram: -abs(Teeth - Lips), always negative
|
||||
float lower = -math.abs(teethShifted - lipsShifted)
|
||||
|
||||
[upper, lower]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(hlc3, "Source")
|
||||
i_jawPeriod = input.int(13, "Jaw Period", minval=1)
|
||||
i_jawOffset = input.int(8, "Jaw Offset", minval=0)
|
||||
i_teethPeriod = input.int(8, "Teeth Period", minval=1)
|
||||
i_teethOffset = input.int(5, "Teeth Offset", minval=0)
|
||||
i_lipsPeriod = input.int(5, "Lips Period", minval=1)
|
||||
i_lipsOffset = input.int(3, "Lips Offset", minval=0)
|
||||
|
||||
// Calculation
|
||||
[upper, lower] = gator(i_source, i_jawPeriod, i_jawOffset, i_teethPeriod, i_teethOffset, i_lipsPeriod, i_lipsOffset)
|
||||
float prevUpper = upper[1]
|
||||
float prevLower = lower[1]
|
||||
|
||||
// Colors: green when expanding (upper rising or lower falling), red when contracting
|
||||
color upperColor = upper >= prevUpper ? color.green : color.red
|
||||
color lowerColor = lower <= prevLower ? color.green : color.red
|
||||
|
||||
// Plot
|
||||
plot(upper, "Upper", color=upperColor, style=plot.style_histogram, linewidth=2)
|
||||
plot(lower, "Lower", color=lowerColor, style=plot.style_histogram, linewidth=2)
|
||||
plot(0, "Zero", color=color.gray, linewidth=1)
|
||||
Reference in New Issue
Block a user