using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// FI: Force Index
///
///
/// Measures buying/selling pressure as EMA-smoothed raw force.
/// Input via Update(TValue) expects pre-computed raw force = (close − prevClose) × volume.
/// The Quantower adapter handles OHLCV decomposition.
///
/// Calculation: FI = EMA(rawForce, period) with exponential warmup compensation.
///
/// Reference Pine Script implementation
[SkipLocalsInit]
public sealed class Fi : 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 LastValid);
private State _s;
private State _ps;
public override bool IsHot => !_s.Warmup;
public Fi(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 = $"Fi({period})";
WarmupPeriod = period;
_s = new State(Ema: 0, E: 1.0, Warmup: true, Index: 0, LastValid: 0);
_ps = _s;
}
public Fi(ITValuePublisher src, int period = 13) : this(period)
{
src.Pub += (object? sender, in TValueEventArgs e) =>
{
Update(e.Value, e.IsNew);
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
double value = input.Value;
if (!double.IsFinite(value))
{
value = s.LastValid;
}
else
{
s.LastValid = value;
}
double result;
if (s.Index == 0)
{
s.Ema = value;
result = value;
}
else
{
s.Ema = Math.FusedMultiplyAdd(s.Ema, _decay, _alpha * value);
if (s.Warmup)
{
s.E *= _decay;
double c = s.E > 1e-10 ? 1.0 / (1.0 - s.E) : 1.0;
result = s.Ema * c;
if (s.E <= 1e-10)
{
s.Warmup = false;
}
}
else
{
result = s.Ema;
}
}
if (isNew)
{
s.Index++;
}
_s = s;
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
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, LastValid: 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);
}
[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];
output[0] = ema;
double e = 1.0;
bool warmup = true;
for (int i = 1; i < len; i++)
{
double value = source[i];
if (!double.IsFinite(value))
{
value = output[i - 1];
}
ema = Math.FusedMultiplyAdd(ema, beta, alpha * value);
if (warmup)
{
e *= beta;
double c = e > 1e-10 ? 1.0 / (1.0 - e) : 1.0;
output[i] = ema * c;
if (e <= 1e-10)
{
warmup = false;
}
}
else
{
output[i] = ema;
}
}
}
public static (TSeries Results, Fi Indicator) Calculate(TSeries source, int period = 13)
{
var indicator = new Fi(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}