using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// EFI: Elder Force Index
///
///
/// Measures force of price movements by combining price change with volume.
/// Large positive values indicate strong buying; large negative indicates selling pressure.
///
/// Calculation: EFI = (Close - prev_Close) × Volume,
/// Smoothed_EFI = EMA(EFI, period).
///
/// Detailed documentation
/// Reference Pine Script implementation
[SkipLocalsInit]
public sealed class Efi : ITValuePublisher
{
private readonly int _period;
private readonly double _alpha;
private readonly double _beta;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double PrevClose;
public double Ema;
public double E;
public bool Warmup;
public int Index;
public double LastValid;
}
private State _s;
private State _ps;
///
/// Display name for the indicator.
///
public string Name { get; }
public event TValuePublishedHandler? Pub;
///
/// Current EFI value.
///
public TValue Last { get; private set; }
///
/// True if the indicator has processed enough bars.
///
public bool IsHot => _s.Index >= _period;
///
/// Warmup period required before the indicator is considered hot.
///
public int WarmupPeriod => _period;
///
/// Creates a new EFI indicator.
///
/// Lookback period for EMA smoothing (default: 13)
/// Thrown when period is less than 1.
public Efi(int period = 13)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_alpha = 2.0 / (period + 1.0);
_beta = 1.0 - _alpha;
Name = $"EFI({period})";
_s = new State
{
PrevClose = double.NaN,
Ema = 0,
E = 1.0,
Warmup = true,
Index = 0,
LastValid = 0
};
_ps = _s;
}
///
/// Resets the indicator state.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_s = new State
{
PrevClose = double.NaN,
Ema = 0,
E = 1.0,
Warmup = true,
Index = 0,
LastValid = 0
};
_ps = _s;
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
double close = input.Close;
double volume = input.Volume;
// Validate inputs
if (!double.IsFinite(close))
{
close = s.LastValid;
}
else
{
s.LastValid = close;
}
if (!double.IsFinite(volume))
{
volume = 0;
}
// Calculate raw force
double rawForce;
if (double.IsNaN(s.PrevClose))
{
rawForce = 0;
}
else
{
rawForce = (close - s.PrevClose) * volume;
}
// Update EMA with bias correction
double result;
if (s.Index == 0)
{
s.Ema = 0;
result = rawForce;
}
else
{
// EMA: ema = alpha * (value - ema) + ema = alpha * value + beta * ema
s.Ema = Math.FusedMultiplyAdd(_alpha, rawForce - s.Ema, s.Ema);
if (s.Warmup)
{
s.E *= _beta;
double c = 1.0 / (1.0 - s.E);
result = c * s.Ema;
if (s.E <= 1e-10)
{
s.Warmup = false;
}
}
else
{
result = s.Ema;
}
}
if (isNew)
{
s.PrevClose = close;
s.Index++;
}
_s = s;
Last = new TValue(input.Time, result);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
///
/// Updates EFI with a TValue input.
///
///
/// EFI requires OHLCV bar data to calculate price change and volume.
/// Use Update(TBar) instead.
///
#pragma warning disable S2325 // Method signature must match ITValuePublisher contract
public TValue Update(TValue input, bool isNew = true)
#pragma warning restore S2325
{
throw new NotSupportedException(
"EFI requires OHLCV bar data to calculate price change and volume. " +
"Use Update(TBar) instead.");
}
public TSeries Update(TBarSeries 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);
}
///
/// Initializes the indicator state using the provided bar series history.
///
/// Historical bar data.
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
public static TSeries Batch(TBarSeries source, int period = 13)
{
if (source.Count == 0)
{
return [];
}
var t = source.Open.Times.ToArray();
var v = new double[source.Count];
Batch(source.Close.Values, source.Volume.Values, v, period);
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span output, int period = 13)
{
if (close.Length != volume.Length)
{
throw new ArgumentException("Close and Volume spans must be of the same length", nameof(volume));
}
if (close.Length != output.Length)
{
throw new ArgumentException("Output span must be of the same length as input", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int len = close.Length;
if (len == 0)
{
return;
}
double alpha = 2.0 / (period + 1.0);
double beta = 1.0 - alpha;
// First bar: no previous close, so raw force = 0
output[0] = 0;
double ema = 0;
double e = 1.0;
bool warmup = true;
for (int i = 1; i < len; i++)
{
double rawForce = (close[i] - close[i - 1]) * volume[i];
// EMA update with bias correction
ema = Math.FusedMultiplyAdd(alpha, rawForce - ema, ema);
if (warmup)
{
e *= beta;
double c = 1.0 / (1.0 - e);
output[i] = c * ema;
if (e <= 1e-10)
{
warmup = false;
}
}
else
{
output[i] = ema;
}
}
}
public static (TSeries Results, Efi Indicator) Calculate(TBarSeries source, int period = 13)
{
var indicator = new Efi(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}