using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// Theil: Theil's T Index (generalized entropy measure of inequality)
///
///
/// Measures the inequality or concentration of values within a sliding window.
/// Based on information theory, the Theil T Index quantifies how far a distribution
/// deviates from perfect equality. Values must be positive.
///
/// Calculation:
/// T = (1/n) × Σ (xᵢ/μ) × ln(xᵢ/μ)
///
/// where μ = mean of all values in the window, n = count of valid positive values.
///
/// Properties:
/// - T = 0 indicates perfect equality (all values identical)
/// - Higher T indicates greater inequality/concentration
/// - Decomposable: total inequality = between-group + within-group
/// - Scale-invariant: multiplying all values by a constant doesn't change T
///
[SkipLocalsInit]
public sealed class Theil : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _buffer;
private double _lastValidValue;
private readonly TValuePublishedHandler _handler;
public override bool IsHot => _buffer.IsFull;
///
/// Creates a new Theil T Index indicator.
///
/// The lookback period (must be >= 2).
public Theil(int period)
{
if (period < 2)
{
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
}
_period = period;
_buffer = new RingBuffer(period);
Name = $"Theil({period})";
WarmupPeriod = period;
_handler = Handle;
}
///
/// Creates a chaining constructor that subscribes to a source indicator.
///
public Theil(ITValuePublisher src, int period) : this(period)
{
src.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double value = input.Value;
// NaN/Infinity guard: substitute last valid value
if (!double.IsFinite(value) || value <= 0)
{
value = _lastValidValue;
}
else
{
_lastValidValue = value;
}
if (isNew)
{
_buffer.Add(value);
}
else
{
_buffer.UpdateNewest(value);
}
double theil = ComputeTheil(_buffer.GetSpan());
Last = new TValue(input.Time, theil);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List(len);
var v = new List(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Reset running state before priming
_buffer.Clear();
_lastValidValue = 0;
// Prime the state
int primeStart = Math.Max(0, len - _period);
for (int i = primeStart; i < len; i++)
{
Update(source[i]);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
public override void Reset()
{
_buffer.Clear();
_lastValidValue = 0;
Last = default;
}
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
DateTime ts = DateTime.MinValue;
foreach (double value in source)
{
Update(new TValue(ts, value));
if (step.HasValue)
{
ts = ts.Add(step.Value);
}
}
}
public static TSeries Batch(TSeries source, int period)
{
var theil = new Theil(period);
return theil.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period < 2)
{
throw new ArgumentException("Period must be greater than or equal to 2", nameof(period));
}
int len = source.Length;
if (len == 0)
{
return;
}
CalculateScalarCore(source, output, period);
}
public static (TSeries Results, Theil Indicator) Calculate(TSeries source, int period)
{
var indicator = new Theil(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period)
{
int len = source.Length;
const int StackallocThreshold = 256;
double[]? rentedWindow = null;
scoped Span windowBuf;
if (period <= StackallocThreshold)
{
windowBuf = stackalloc double[period];
}
else
{
rentedWindow = ArrayPool.Shared.Rent(period);
windowBuf = rentedWindow.AsSpan(0, period);
}
try
{
// Persistent lastValidValue across all iterations — matches streaming Update behavior
double lastValidValue = 0;
for (int i = 0; i < len; i++)
{
int windowStart = Math.Max(0, i - period + 1);
int windowLen = i - windowStart + 1;
// Copy window values with NaN/non-positive substitution using persistent lastValidValue
double windowLastValid = lastValidValue;
for (int j = 0; j < windowLen; j++)
{
double wv = source[windowStart + j];
if (!double.IsFinite(wv) || wv <= 0)
{
wv = windowLastValid;
}
else
{
windowLastValid = wv;
}
windowBuf[j] = wv;
}
// Update the persistent value with the last valid seen in this window
if (windowLastValid > 0)
{
lastValidValue = windowLastValid;
}
output[i] = ComputeTheil(windowBuf[..windowLen]);
}
}
finally
{
if (rentedWindow is not null)
{
ArrayPool.Shared.Return(rentedWindow);
}
}
}
///
/// Computes Theil's T Index from a span of positive values.
/// T = (1/n) × Σ (xᵢ/μ) × ln(xᵢ/μ)
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeTheil(ReadOnlySpan values)
{
int n = values.Length;
if (n < 2)
{
return 0;
}
// Compute mean of positive values
double sum = 0;
int validCount = 0;
for (int i = 0; i < n; i++)
{
double v = values[i];
if (v > 0)
{
sum += v;
validCount++;
}
}
if (validCount == 0 || sum <= 0)
{
return double.NaN;
}
double mean = sum / validCount;
double invMean = 1.0 / mean;
// Compute Theil T: (1/n) × Σ (xᵢ/μ) × ln(xᵢ/μ)
double theilSum = 0;
for (int i = 0; i < n; i++)
{
double v = values[i];
if (v > 0)
{
double ratio = v * invMean;
theilSum += ratio * Math.Log(ratio);
}
}
return theilSum / validCount;
}
}