using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// ERI: Elder Ray Index
///
///
/// 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: EMA = EMA(close, period) with exponential warmup compensation,
/// then BullPower = high − EMA, BearPower = low − EMA.
///
/// Reference Pine Script implementation
[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;
///
/// Bear Power = Low − EMA(Close). Updated after each Update call.
///
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);
}
///
/// Updates with a TBar (High, Low, Close). Returns Bull Power as the primary value.
/// Bear Power is accessible via the BearPower property.
///
[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;
}
///
/// Updates with a single TValue (treated as close price with high=low=close).
/// For proper ERI computation, use Update(TBar) instead.
///
[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(source.Count);
var v = new List(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 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);
}
///
/// 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.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan source, Span 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);
}
}