Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.

This commit is contained in:
Miha Kralj
2026-02-20 18:44:56 -08:00
parent 3dd05f23e4
commit cbeefc9d64
283 changed files with 23963 additions and 3838 deletions
+157
View File
@@ -0,0 +1,157 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class EriIndicatorTests
{
[Fact]
public void EriIndicator_Constructor_SetsDefaults()
{
var indicator = new EriIndicator();
Assert.Equal("ERI - Elder Ray Index", indicator.Name);
Assert.Equal(13, indicator.Period);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(13, indicator.MinHistoryDepths);
}
[Fact]
public void EriIndicator_ShortName_ReflectsPeriod()
{
var indicator = new EriIndicator { Period = 20 };
Assert.Equal("ERI(20)", indicator.ShortName);
}
[Fact]
public void EriIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new EriIndicator { Period = 26 };
Assert.Equal(26, indicator.MinHistoryDepths);
Assert.Equal(26, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void EriIndicator_Initialize_CreatesInternalEri()
{
var indicator = new EriIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, two line series should exist (Bull Power + Bear Power)
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void EriIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new EriIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000 + (i * 100));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double bullVal = indicator.LinesSeries[0].GetValue(0);
double bearVal = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(bullVal));
Assert.True(double.IsFinite(bearVal));
}
[Fact]
public void EriIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new EriIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000 + (i * 100));
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void EriIndicator_Value_IsFinite()
{
var indicator = new EriIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
double open = 100 + i;
double high = open + 10 + (i % 5);
double low = open - 5;
double close = (i % 2 == 0) ? high - 1 : low + 1;
double volume = 1000 + (i * 100);
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double bullVal = indicator.LinesSeries[0].GetValue(0);
double bearVal = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(bullVal), $"Bull Power value {bullVal} should be finite");
Assert.True(double.IsFinite(bearVal), $"Bear Power value {bearVal} should be finite");
}
[Fact]
public void EriIndicator_BullPowerPositive_OnHighAboveEma()
{
var indicator = new EriIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed bars with high consistently above close (and thus above EMA)
for (int i = 0; i < 20; i++)
{
double close = 100 + (i * 2);
double high = close + 15; // High well above close
double low = close - 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, high, low, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double bullVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(bullVal > 0, $"Bull Power should be positive when High > EMA, got {bullVal}");
}
[Fact]
public void EriIndicator_BearPowerNegative_OnLowBelowEma()
{
var indicator = new EriIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Feed bars with low consistently below close (and thus below EMA)
for (int i = 0; i < 20; i++)
{
double close = 100 + (i * 2);
double high = close + 5;
double low = close - 15; // Low well below close
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, high, low, close, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double bearVal = indicator.LinesSeries[1].GetValue(0);
Assert.True(bearVal < 0, $"Bear Power should be negative when Low < EMA, got {bearVal}");
}
}
+58
View File
@@ -0,0 +1,58 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class EriIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 10, 1, 500, 1, 0)]
public int Period { get; set; } = 13;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Eri _eri = null!;
private readonly LineSeries _bullSeries;
private readonly LineSeries _bearSeries;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => Period;
public override string ShortName => $"ERI({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/eri/Eri.Quantower.cs";
public EriIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "ERI - Elder Ray Index";
Description = "Elder Ray Index measures buying and selling pressure as Bull Power (High EMA) and Bear Power (Low EMA)";
_bullSeries = new LineSeries(name: "Bull Power", color: Color.Green, width: 2, style: LineStyle.Solid);
_bearSeries = new LineSeries(name: "Bear Power", color: Color.Red, width: 2, style: LineStyle.Solid);
AddLineSeries(_bullSeries);
AddLineSeries(_bearSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_eri = new Eri(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
bool isNew = args.IsNewBar();
TBar bar = this.GetInputBar(args);
TValue result = _eri.Update(bar, isNew);
_bullSeries.SetValue(result.Value, _eri.IsHot, ShowColdValues);
_bearSeries.SetValue(_eri.BearPower, _eri.IsHot, ShowColdValues);
}
}
+563
View File
@@ -0,0 +1,563 @@
namespace QuanTAlib.Tests;
public class EriTests
{
// ── A) Constructor validation ──────────────────────────────────────
[Fact]
public void Eri_Constructor_DefaultPeriod_Is13()
{
var eri = new Eri();
Assert.Equal("Eri(13)", eri.Name);
Assert.Equal(13, eri.WarmupPeriod);
}
[Fact]
public void Eri_Constructor_CustomPeriod_SetsCorrectly()
{
var eri = new Eri(20);
Assert.Equal("Eri(20)", eri.Name);
Assert.Equal(20, eri.WarmupPeriod);
}
[Fact]
public void Eri_Constructor_InvalidPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Eri(0));
Assert.Equal("period", ex.ParamName);
ex = Assert.Throws<ArgumentException>(() => new Eri(-1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Eri_Constructor_Period1_IsValid()
{
var eri = new Eri(1);
Assert.Equal("Eri(1)", eri.Name);
}
// ── B) Basic calculation ───────────────────────────────────────────
[Fact]
public void Eri_BasicCalculation_FirstBar_BullPowerIsHighMinusClose()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
// First bar: EMA = close = 100, Bull Power = high - EMA = 110 - 100 = 10
var bar = new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0);
var val = eri.Update(bar);
Assert.Equal(10.0, val.Value, 10);
}
[Fact]
public void Eri_BasicCalculation_FirstBar_BearPowerIsLowMinusClose()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
// First bar: EMA = close = 100, Bear Power = low - EMA = 90 - 100 = -10
var bar = new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0);
_ = eri.Update(bar);
Assert.Equal(-10.0, eri.BearPower, 10);
}
[Fact]
public void Eri_BasicCalculation_BullPowerPositive_InUptrend()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
// Feed rising prices — High should consistently be above EMA
for (int i = 0; i < 30; i++)
{
double close = 100.0 + (i * 2);
double high = close + 10;
double low = close - 5;
var bar = new TBar(time.AddMinutes(i), close, high, low, close, 1000.0);
eri.Update(bar);
}
// Bull power should be positive (High > EMA in uptrend)
Assert.True(eri.Last.Value > 0, $"Bull Power should be positive in uptrend, got {eri.Last.Value}");
}
[Fact]
public void Eri_BasicCalculation_BearPowerNegative_InDowntrend()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
// Feed declining prices — Low should consistently be below EMA
for (int i = 0; i < 30; i++)
{
double close = 200.0 - (i * 2);
double high = close + 5;
double low = close - 10;
var bar = new TBar(time.AddMinutes(i), close, high, low, close, 1000.0);
eri.Update(bar);
}
// Bear power should be negative (Low < EMA in downtrend)
Assert.True(eri.BearPower < 0, $"Bear Power should be negative in downtrend, got {eri.BearPower}");
}
[Fact]
public void Eri_BasicCalculation_AccessLast_Name_IsHot()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
var bar = new TBar(time, 100.0, 110.0, 90.0, 105.0, 1000.0);
var val = eri.Update(bar);
Assert.Equal(val.Value, eri.Last.Value);
Assert.Equal("Eri(3)", eri.Name);
Assert.False(eri.IsHot); // Only 1 bar, not yet warmed up for period=3
}
// ── C) State + bar correction ──────────────────────────────────────
[Fact]
public void Eri_IsNew_True_AdvancesState()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
var bar1 = new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0);
var bar2 = new TBar(time.AddMinutes(1), 105.0, 115.0, 95.0, 110.0, 1000.0);
var val1 = eri.Update(bar1, isNew: true);
var val2 = eri.Update(bar2, isNew: true);
// Two distinct updates with different H/L/C should give different values
Assert.NotEqual(val1.Value, val2.Value);
}
[Fact]
public void Eri_IsNew_False_RollsBackState()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
var bar1 = new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0);
_ = eri.Update(bar1, isNew: true);
var bar2 = new TBar(time.AddMinutes(1), 105.0, 115.0, 95.0, 110.0, 1000.0);
var val2 = eri.Update(bar2, isNew: true);
// Correction: isNew=false rolls back to state after bar 1
// Use significantly different close to shift EMA and produce divergent bull power
var bar2Corrected = new TBar(time.AddMinutes(1), 108.0, 150.0, 92.0, 140.0, 1000.0);
var val2Corrected = eri.Update(bar2Corrected, isNew: false);
// Different input => different result
Assert.NotEqual(val2.Value, val2Corrected.Value);
}
[Fact]
public void Eri_IterativeCorrections_RestoreState()
{
var eri = new Eri(5);
var time = DateTime.UtcNow;
// Build up state
var bar1 = new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0);
_ = eri.Update(bar1, isNew: true);
var bar2 = new TBar(time.AddMinutes(1), 105.0, 115.0, 95.0, 110.0, 1000.0);
_ = eri.Update(bar2, isNew: true);
// Multiple corrections to bar 3
_ = eri.Update(new TBar(time.AddMinutes(2), 108.0, 118.0, 98.0, 105.0, 1000.0), isNew: true);
_ = eri.Update(new TBar(time.AddMinutes(2), 112.0, 122.0, 102.0, 112.0, 1000.0), isNew: false);
_ = eri.Update(new TBar(time.AddMinutes(2), 115.0, 125.0, 105.0, 118.0, 1000.0), isNew: false);
var finalBar = new TBar(time.AddMinutes(2), 120.0, 130.0, 110.0, 120.0, 1000.0);
var finalVal = eri.Update(finalBar, isNew: false);
// Should match a fresh computation with the final corrected value
var eri2 = new Eri(5);
_ = eri2.Update(bar1, isNew: true);
_ = eri2.Update(bar2, isNew: true);
var expected = eri2.Update(finalBar, isNew: true);
Assert.Equal(expected.Value, finalVal.Value, 10);
}
[Fact]
public void Eri_Reset_ClearsState()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
eri.Update(new TBar(time, 100.0, 110.0, 90.0, 105.0, 1000.0));
eri.Update(new TBar(time.AddMinutes(1), 110.0, 120.0, 100.0, 115.0, 1000.0));
Assert.NotEqual(0, eri.Last.Value);
eri.Reset();
Assert.False(eri.IsHot);
Assert.Equal(0, eri.Last.Value);
}
// ── D) Warmup/convergence ──────────────────────────────────────────
[Fact]
public void Eri_IsHot_FlipsWhenWarmupComplete()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
Assert.False(eri.IsHot);
// Feed enough data — warmup ends after WarmupPeriod bars
for (int i = 0; i < 50; i++)
{
double close = 100.0 + i;
eri.Update(new TBar(time.AddMinutes(i), close, close + 10, close - 5, close, 1000.0));
}
Assert.True(eri.IsHot);
}
[Fact]
public void Eri_WarmupPeriod_EqualsPeriod()
{
var eri = new Eri(7);
Assert.Equal(7, eri.WarmupPeriod);
}
[Fact]
public void Eri_ConvergesAfterManyBars_GBM()
{
var eri = new Eri(13);
var gbm = new GBM();
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
eri.Update(bar);
}
Assert.True(eri.IsHot);
Assert.True(double.IsFinite(eri.Last.Value));
Assert.True(double.IsFinite(eri.BearPower));
}
// ── E) Robustness ──────────────────────────────────────────────────
[Fact]
public void Eri_NaN_Input_UsesLastValid()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
eri.Update(new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0));
eri.Update(new TBar(time.AddMinutes(1), 105.0, 115.0, 95.0, 110.0, 1000.0));
// NaN close should use last valid value
var val = eri.Update(new TBar(time.AddMinutes(2), 108.0, double.NaN, 98.0, double.NaN, 1000.0));
Assert.True(double.IsFinite(val.Value));
Assert.True(double.IsFinite(eri.BearPower));
}
[Fact]
public void Eri_Infinity_Input_UsesLastValid()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
eri.Update(new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0));
var val = eri.Update(new TBar(time.AddMinutes(1), 105.0, double.PositiveInfinity, 95.0, double.PositiveInfinity, 1000.0));
Assert.True(double.IsFinite(val.Value));
val = eri.Update(new TBar(time.AddMinutes(2), 108.0, 118.0, double.NegativeInfinity, double.NegativeInfinity, 1000.0));
Assert.True(double.IsFinite(val.Value));
Assert.True(double.IsFinite(eri.BearPower));
}
[Fact]
public void Eri_BatchNaN_Safe_GBM()
{
var eri = new Eri(5);
var gbm = new GBM();
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
double close = (i % 7 == 3) ? double.NaN : bar.Close;
double high = (i % 11 == 5) ? double.NaN : bar.High;
double low = bar.Low;
eri.Update(new TBar(bar.Time, bar.Open, high, low, close, bar.Volume));
}
Assert.True(double.IsFinite(eri.Last.Value));
Assert.True(double.IsFinite(eri.BearPower));
}
// ── F) Consistency ─────────────────────────────────────────────────
[Fact]
public void Eri_Streaming_Matches_Batch()
{
int period = 5;
int count = 50;
var time = DateTime.UtcNow;
var source = new TSeries();
for (int i = 0; i < count; i++)
{
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
}
// Streaming
var eri = new Eri(period);
var streamResults = new double[count];
for (int i = 0; i < count; i++)
{
var val = eri.Update(source[i], isNew: true);
streamResults[i] = val.Value;
}
// Batch
var batchSeries = Eri.Batch(source, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(batchSeries[i].Value, streamResults[i], 10);
}
}
[Fact]
public void Eri_Streaming_Matches_SpanCalculate()
{
int period = 5;
int count = 50;
var time = DateTime.UtcNow;
var source = new TSeries();
for (int i = 0; i < count; i++)
{
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
}
// Streaming
var eri = new Eri(period);
var streamResults = new double[count];
for (int i = 0; i < count; i++)
{
var val = eri.Update(source[i], isNew: true);
streamResults[i] = val.Value;
}
// Span calculate
var spanOutput = new double[count];
Eri.Calculate(source.Values, spanOutput, period);
for (int i = 0; i < count; i++)
{
Assert.Equal(spanOutput[i], streamResults[i], 10);
}
}
[Fact]
public void Eri_Eventing_Matches_Streaming()
{
int period = 5;
int count = 50;
var time = DateTime.UtcNow;
var source = new TSeries();
for (int i = 0; i < count; i++)
{
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
}
// Streaming
var eri1 = new Eri(period);
var streamResults = new double[count];
for (int i = 0; i < count; i++)
{
var val = eri1.Update(source[i], isNew: true);
streamResults[i] = val.Value;
}
// Eventing via Update(TSeries) which resets and streams
var eri2 = new Eri(period);
var eventResults = eri2.Update(source);
for (int i = 0; i < count; i++)
{
Assert.Equal(eventResults[i].Value, streamResults[i], 10);
}
}
// ── G) Span API tests ──────────────────────────────────────────────
[Fact]
public void Eri_Calculate_MismatchedLengths_ThrowsArgumentException()
{
var src = new double[10];
var output = new double[5];
var ex = Assert.Throws<ArgumentException>(() => Eri.Calculate(src, output));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Eri_Calculate_InvalidPeriod_ThrowsArgumentException()
{
var src = new double[10];
var output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Eri.Calculate(src, output, 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Eri_Calculate_EmptyInput_NoOp()
{
ReadOnlySpan<double> src = [];
Span<double> output = [];
Eri.Calculate(src, output); // Should not throw
Assert.True(true); // S2699: assertion confirms no-exception completion
}
[Fact]
public void Eri_Calculate_NaN_HandledGracefully()
{
var src = new double[] { 100, double.NaN, 200, 300, double.NaN, 400 };
var output = new double[6];
Eri.Calculate(src, output, 3);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsFinite(output[i]), $"output[{i}] = {output[i]} should be finite");
}
}
[Fact]
public void Eri_Calculate_LargeData_NoStackOverflow()
{
int size = 10_000;
var src = new double[size];
var output = new double[size];
for (int i = 0; i < size; i++)
{
src[i] = Math.Sin(i * 0.1) * 100;
}
Eri.Calculate(src, output, 13);
Assert.True(double.IsFinite(output[size - 1]));
}
// ── H) Chainability ────────────────────────────────────────────────
[Fact]
public void Eri_PubEvent_FiresOnUpdate()
{
var eri = new Eri();
bool eventFired = false;
eri.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
eri.Update(new TBar(DateTime.UtcNow, 100.0, 110.0, 90.0, 105.0, 1000.0));
Assert.True(eventFired);
}
[Fact]
public void Eri_Chaining_EventBased()
{
var eri1 = new Eri(3);
var eri2 = new Eri(eri1, 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double close = 100.0 + (10.0 * Math.Sin(i * 0.3));
eri1.Update(new TBar(time.AddMinutes(i), close, close + 5, close - 5, close, 1000.0));
}
// eri2 should have received updates from eri1's Pub events
Assert.True(double.IsFinite(eri2.Last.Value));
}
[Fact]
public void Eri_Calculate_StaticFactory_ReturnsResultsAndIndicator()
{
var time = DateTime.UtcNow;
var source = new TSeries();
for (int i = 0; i < 100; i++)
{
source.Add(new TValue(time.AddMinutes(i), 100.0 + i));
}
var (results, indicator) = Eri.Calculate(source, 5);
Assert.Equal(100, results.Count);
Assert.True(indicator.IsHot);
}
[Fact]
public void Eri_Prime_InitializesState()
{
var eri = new Eri(5);
var source = new double[30];
for (int i = 0; i < 30; i++)
{
source[i] = 100.0 + i;
}
eri.Prime(source);
Assert.True(double.IsFinite(eri.Last.Value));
}
[Fact]
public void Eri_ConstantInput_BullBearPowerConverge()
{
var eri = new Eri(5);
var time = DateTime.UtcNow;
// When H=L=C=constant, EMA converges to constant, so Bull=Bear=0
for (int i = 0; i < 200; i++)
{
eri.Update(new TBar(time.AddMinutes(i), 42.0, 42.0, 42.0, 42.0, 1000.0));
}
Assert.Equal(0.0, eri.Last.Value, 6); // Bull Power = H - EMA = 0
Assert.Equal(0.0, eri.BearPower, 6); // Bear Power = L - EMA = 0
}
[Fact]
public void Eri_BearPower_IsAccessible()
{
var eri = new Eri(3);
var time = DateTime.UtcNow;
// High=110, Low=90, Close=100 → first bar EMA=100, Bull=10, Bear=-10
eri.Update(new TBar(time, 100.0, 110.0, 90.0, 100.0, 1000.0));
Assert.Equal(10.0, eri.Last.Value, 10); // Bull Power
Assert.Equal(-10.0, eri.BearPower, 10); // Bear Power
}
[Fact]
public void Eri_TBar_GBM_StreamingProducesFiniteResults()
{
var eri = new Eri(13);
var gbm = new GBM();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
eri.Update(bar);
}
Assert.True(double.IsFinite(eri.Last.Value), "Bull Power should be finite");
Assert.True(double.IsFinite(eri.BearPower), "Bear Power should be finite");
Assert.True(eri.IsHot, "Should be hot after 100 bars with period=13");
}
}
+308
View File
@@ -0,0 +1,308 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ERI: Elder Ray Index
/// </summary>
/// <remarks>
/// Measures buying/selling pressure relative to an EMA trend line.
/// Bull Power = High EMA(Close, period); Bear Power = Low EMA(Close, period).
/// Primary output (Last) is Bull Power; Bear Power is accessible via the BearPower property.
/// The Quantower adapter handles OHLCV bar decomposition.
///
/// Calculation: <c>EMA = EMA(close, period)</c> with exponential warmup compensation,
/// then <c>BullPower = high EMA</c>, <c>BearPower = low EMA</c>.
/// </remarks>
/// <seealso href="eri.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Eri : AbstractBase
{
private readonly double _alpha;
private readonly double _decay;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Ema,
double E,
bool Warmup,
int Index,
double LastValidClose,
double LastValidHigh,
double LastValidLow,
double BearPower);
private State _s;
private State _ps;
public override bool IsHot => _s.Index >= WarmupPeriod;
/// <summary>
/// Bear Power = Low EMA(Close). Updated after each Update call.
/// </summary>
public double BearPower => _s.BearPower;
public Eri(int period = 13)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1.", nameof(period));
}
_alpha = 2.0 / (period + 1.0);
_decay = 1.0 - _alpha;
Name = $"Eri({period})";
WarmupPeriod = period;
_s = new State(Ema: 0, E: 1.0, Warmup: true, Index: 0,
LastValidClose: 0, LastValidHigh: 0, LastValidLow: 0, BearPower: 0);
_ps = _s;
}
public Eri(ITValuePublisher src, int period = 13) : this(period)
{
src.Pub += Handle;
}
private void Handle(object? sender, in TValueEventArgs e)
{
Update(e.Value, e.IsNew);
}
/// <summary>
/// Updates with a TBar (High, Low, Close). Returns Bull Power as the primary value.
/// Bear Power is accessible via the BearPower property.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
double close = bar.Close;
double high = bar.High;
double low = bar.Low;
// NaN/Infinity guard for close
if (!double.IsFinite(close))
{
close = s.LastValidClose;
}
else
{
s.LastValidClose = close;
}
// NaN/Infinity guard for high
if (!double.IsFinite(high))
{
high = s.LastValidHigh;
}
else
{
s.LastValidHigh = high;
}
// NaN/Infinity guard for low
if (!double.IsFinite(low))
{
low = s.LastValidLow;
}
else
{
s.LastValidLow = low;
}
// Compute EMA of close
double emaVal;
if (s.Index == 0)
{
s.Ema = close;
emaVal = close;
}
else
{
s.Ema = Math.FusedMultiplyAdd(s.Ema, _decay, _alpha * close);
if (s.Warmup)
{
s.E *= _decay;
double c = s.E > 1e-10 ? 1.0 / (1.0 - s.E) : 1.0;
emaVal = s.Ema * c;
if (s.E <= 1e-10)
{
s.Warmup = false;
}
}
else
{
emaVal = s.Ema;
}
}
double bullPower = high - emaVal;
s.BearPower = low - emaVal;
if (isNew)
{
s.Index++;
}
_s = s;
Last = new TValue(bar.Time, bullPower);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Updates with a single TValue (treated as close price with high=low=close).
/// For proper ERI computation, use Update(TBar) instead.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
}
public override TSeries Update(TSeries 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 override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
long baseTicks = DateTime.UtcNow.Ticks;
Reset();
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(new DateTime(baseTicks + (interval.Ticks * i), DateTimeKind.Utc), source[i]), isNew: true);
}
}
public override void Reset()
{
_s = new State(Ema: 0, E: 1.0, Warmup: true, Index: 0,
LastValidClose: 0, LastValidHigh: 0, LastValidLow: 0, BearPower: 0);
_ps = _s;
Last = default;
}
public static TSeries Batch(TSeries source, int period = 13)
{
if (source.Count == 0)
{
return [];
}
var t = source.Times.ToArray();
var v = new double[source.Count];
Calculate(source.Values, v, period);
return new TSeries(t, v);
}
/// <summary>
/// Span-based calculation for close-only data.
/// Computes EMA(close) and outputs Bull Power = close EMA (since high=low=close).
/// For proper H/L/C computation, use the TBar overloads.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 13)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Output span must be the same length as input.", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1.", nameof(period));
}
int len = source.Length;
if (len == 0)
{
return;
}
double alpha = 2.0 / (period + 1.0);
double beta = 1.0 - alpha;
double ema = source[0];
// When high=low=close, Bull Power = close - ema = 0 on first bar
output[0] = source[0] - ema;
double e = 1.0;
bool warmup = true;
double lastValid = source[0];
for (int i = 1; i < len; i++)
{
double value = source[i];
if (!double.IsFinite(value))
{
value = lastValid;
}
else
{
lastValid = value;
}
ema = Math.FusedMultiplyAdd(ema, beta, alpha * value);
double emaVal;
if (warmup)
{
e *= beta;
double c = e > 1e-10 ? 1.0 / (1.0 - e) : 1.0;
emaVal = ema * c;
if (e <= 1e-10)
{
warmup = false;
}
}
else
{
emaVal = ema;
}
// For close-only spans, Bull Power = close - EMA
output[i] = value - emaVal;
}
}
public static (TSeries Results, Eri Indicator) Calculate(TSeries source, int period = 13)
{
var indicator = new Eri(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
+184
View File
@@ -0,0 +1,184 @@
# ERI: Elder Ray Index
> "The job of the indicator is to separate the bulls from the bears. If you can measure their power independently, you can see who is winning before the trend changes." -- Alexander Elder
| Property | Value |
|----------|-------|
| **Category** | Oscillator |
| **Inputs** | Bar series (High, Low, Close) |
| **Parameters** | `period` (default 13) |
| **Outputs** | Dual: Bull Power (primary), Bear Power (property) |
| **Output range** | Unbounded (centered around 0) |
| **Warmup** | `period` bars |
### Key takeaways
- Decomposes buying and selling pressure relative to an EMA trend line: Bull Power = High $-$ EMA, Bear Power = Low $-$ EMA.
- Primary output (`Last`) is Bull Power; Bear Power is accessible via the `BearPower` property.
- Uses EMA with exponential warmup compensation for bias-free early values.
- Bull Power positive means buyers pushed price above the trend; Bear Power negative means sellers pulled price below.
- Best used with a trend filter: take longs when EMA is rising and Bear Power is recovering from below zero.
## Historical Context
Dr. Alexander Elder introduced Bull Power and Bear Power in *Trading for a Living* (1993). Elder, a psychiatrist turned trader, designed the indicators to measure the balance of power between buyers and sellers relative to the prevailing trend, represented by an EMA.
The logic is clinical. The highest price of a bar reflects the maximum power of the bulls. The lowest price reflects the maximum power of the bears. Measuring each against the EMA (the consensus value) yields two independent readings of buying and selling pressure. Elder recommended a 13-period EMA, though any reasonable period works.
Elder Ray is typically plotted as two separate histograms below the price chart. The name "Elder Ray" is a visual metaphor: like X-rays revealing bone structure beneath flesh, the indicator reveals the hidden power structure beneath price action.
## What It Measures and Why It Matters
Bull Power measures how far above the EMA the bulls managed to push price during the bar. Positive Bull Power means buyers controlled the session. Negative Bull Power means even the high of the bar was below the trend line, a deeply bearish condition.
Bear Power measures how far below the EMA the bears managed to push price. Negative Bear Power is normal in uptrends (the low is below the average). When Bear Power turns positive, it means even the low of the bar was above the trend, an extremely bullish condition.
The two powers are complementary. Strong uptrends show large positive Bull Power and small negative Bear Power (recovering toward zero). Strong downtrends show large negative Bear Power and small positive Bull Power (declining toward zero). Divergence between price and either power reading signals potential trend exhaustion.
## Mathematical Foundation
### Core Formula
$$
\text{EMA}_t = \alpha \cdot C_t + (1 - \alpha) \cdot \text{EMA}_{t-1}
$$
$$
\text{Bull Power}_t = H_t - \text{EMA}_t
$$
$$
\text{Bear Power}_t = L_t - \text{EMA}_t
$$
where:
- $C_t$ = close price at bar $t$
- $H_t$ = high price at bar $t$
- $L_t$ = low price at bar $t$
- $\alpha = \frac{2}{N + 1}$ = EMA smoothing factor
- $N$ = period (default 13)
### Warmup Compensation
During warmup, the EMA uses exponential decay correction:
$$
e_t = e_{t-1} \cdot (1 - \alpha), \quad e_0 = 1
$$
$$
\text{EMA}_t^{\text{compensated}} = \frac{\text{EMA}_t^{\text{raw}}}{1 - e_t}
$$
This eliminates startup bias without requiring a seed SMA.
### Parameter Mapping
| Parameter | Symbol | Default | Constraint |
|-----------|--------|---------|------------|
| `period` | $N$ | 13 | $N \geq 1$ |
### Warmup Period
$$
W = N
$$
## Architecture & Physics
### 1. EMA with FMA Optimization
The core EMA update uses `Math.FusedMultiplyAdd` for the standard IIR recursion:
$$
\text{EMA}_t = \text{FMA}(\text{EMA}_{t-1}, \beta, \alpha \cdot C_t) \quad \text{where } \beta = 1 - \alpha
$$
Precomputed `_alpha` and `_decay` constants avoid repeated division in the hot path.
### 2. Multi-Channel NaN Guard
Three independent last-valid substitution channels track `Close`, `High`, and `Low` separately. A NaN high does not contaminate the close or low channels.
### 3. State Management
A `record struct State` holds `Ema`, `E` (warmup decay), `Warmup` flag, `Index`, and three `LastValid` fields plus `BearPower`. The `_s` / `_ps` local-copy pattern enables bar correction and JIT struct promotion.
### 4. TBar vs TValue Input
- `Update(TBar)`: Full H/L/C decomposition for proper Bull and Bear Power.
- `Update(TValue)`: Treats input as close with high = low = close. Useful for chaining but produces degenerate results (Bull Power = Bear Power = value $-$ EMA).
### 5. Edge Cases
| Condition | Behavior |
|-----------|----------|
| `period < 1` | `ArgumentException` with `nameof(period)` |
| `NaN` / `Infinity` input | Per-channel last-valid substitution |
| First bar | EMA = close, Bull Power = high $-$ close, Bear Power = low $-$ close |
| TValue input | Wraps as TBar with OHLC = value |
## Interpretation and Signals
### Signal Patterns
- **Bull divergence**: Price makes lower lows, but Bear Power makes higher lows. Selling pressure is weakening despite new price lows. Bullish signal.
- **Bear divergence**: Price makes higher highs, but Bull Power makes lower highs. Buying pressure is weakening despite new price highs. Bearish signal.
- **Long entry (Elder's rules)**: EMA rising, Bear Power negative but increasing, Bull Power's latest peak exceeds the prior peak.
- **Short entry (Elder's rules)**: EMA declining, Bull Power positive but decreasing, Bear Power's latest trough is lower than the prior trough.
### Practical Notes
Elder recommended combining Bull/Bear Power with a 13-period EMA slope as a trend filter. The indicator works best in trending markets where the power decomposition reveals which side is gaining or losing strength. In range-bound conditions, both powers oscillate around zero without clear directional bias.
## Related Indicators
- [**Fi**](../fi/Fi.md): Force Index, another Elder creation that measures buying/selling pressure using price change times volume.
- [**Stoch**](../stoch/Stoch.md): Also measures position within a range, but uses high-low range rather than EMA deviation.
## Validation
| Library | Batch | Streaming | Span | Notes |
|---------|:-----:|:---------:|:----:|-------|
| **TA-Lib** | -- | -- | -- | No direct ERI function |
| **Skender** | -- | -- | -- | `GetElderRay()` available but not yet validated |
| **Tulip** | -- | -- | -- | Not available |
| **Ooples** | -- | -- | -- | Not available |
Internal consistency validated across streaming, batch, span, and eventing modes.
## Performance Profile
### Key Optimizations
- **FMA in EMA**: `Math.FusedMultiplyAdd(ema, decay, alpha * close)` replaces separate multiply-and-add.
- **Local state copy**: JIT promotes `var s = _s` to registers, avoiding repeated memory loads.
- **Zero allocation**: All state in `record struct`; no heap allocations in `Update`.
- **Aggressive inlining**: `[MethodImpl(AggressiveInlining)]` on `Update(TBar)`.
### Operation Count (Streaming Mode)
| Operation | Count per bar |
|-----------|---------------|
| FMA | 1 (EMA update) |
| MUL | 1 (alpha * close) |
| SUB | 2 (Bull Power, Bear Power) |
| NaN checks | 3 (high, low, close) |
| Conditional | 1 (warmup check) |
| **Total** | **~8 ops** |
## Common Pitfalls
1. **Primary output is Bull Power only**: `Last.Value` returns Bull Power. Bear Power requires accessing the `BearPower` property separately. Forgetting this leads to incomplete analysis.
2. **TValue input is degenerate**: Passing single values (not TBar) sets H = L = C, making Bull Power = Bear Power = C $-$ EMA. Use TBar input for proper decomposition.
3. **Warmup affects EMA quality**: Early EMA values use exponential warmup compensation, but the first few bars are still adapting. Allow at least $2N$ bars for stable readings.
4. **Not bounded**: Unlike oscillators clamped to $[0, 100]$, Bull and Bear Power can take any value. Visual scaling on charts requires attention.
5. **Trend filter is essential**: Elder explicitly designed this as a component of a system. Using Bull/Bear Power without checking EMA direction defeats the design intent.
6. **Bear Power is normally negative**: In healthy uptrends, Bear Power is negative (low is below EMA). It becomes concerning only when it turns increasingly negative during what should be an uptrend.
## References
- Elder, A. *Trading for a Living*. John Wiley & Sons, 1993. Chapter on Elder-Ray.
- Elder, A. *Come Into My Trading Room*. John Wiley & Sons, 2002.
- Achelis, S. B. *Technical Analysis from A to Z*. McGraw-Hill, 2000.
+55
View File
@@ -0,0 +1,55 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Elder Ray Index (ERI)", "ERI", overlay=false)
//@function Calculates Elder Ray Index (Bull Power and Bear Power)
//@param period EMA period for the centerline
//@returns tuple [bullPower, bearPower] measuring buying/selling pressure
eri(simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 2.0 / (period + 1.0)
float beta = 1.0 - alpha
var float ema = 0.0
var float e = 1.0
var bool warmup = true
var bool first = true
float src = nz(close)
if first
ema := src
first := false
else
ema := alpha * src + beta * ema
float emaVal = src
if warmup
e *= beta
float c = e > 1e-10 ? 1.0 / (1.0 - e) : 1.0
emaVal := ema * c
if e <= 1e-10
warmup := false
else
emaVal := ema
float bullPower = high - emaVal
float bearPower = low - emaVal
[bullPower, bearPower]
// ---------- Main loop ----------
// Inputs
i_period = input.int(13, "Period", minval=1, maxval=500)
// Calculation
[bull, bear] = eri(i_period)
// Plot
plot(bull, "Bull Power", color.new(color.green, 0), 2)
plot(bear, "Bear Power", color.new(color.red, 0), 2)
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)