using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// III: Intraday Intensity Index
///
///
/// Volume-weighted indicator measuring buying/selling pressure based on close position in range.
/// Range: -1 (close at low) to +1 (close at high) times volume; indicates distribution vs accumulation.
///
/// Calculation: Position = (2 × Close - High - Low) / (High - Low),
/// Raw_III = Position × Volume, III = SMA(Raw_III, period) or cumulative sum.
///
/// Detailed documentation
/// Reference Pine Script implementation
[SkipLocalsInit]
public sealed class Iii : ITValuePublisher
{
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double Sum;
public double CumulativeValue;
public int Head;
public int Count;
public double LastValidValue;
}
private State _s;
private State _ps;
private readonly int _period;
private readonly bool _cumulative;
private readonly double[] _buffer;
public string Name { get; }
public int WarmupPeriod { get; }
public TValue Last { get; private set; }
public bool IsHot { get; private set; }
public event TValuePublishedHandler? Pub;
///
/// Initializes a new instance of the Iii class.
///
/// The smoothing period for SMA calculation (default: 14)
/// Whether to accumulate values (default: false)
/// Thrown when period is less than 1
public Iii(int period = 14, bool cumulative = false)
{
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_cumulative = cumulative;
_buffer = new double[period];
WarmupPeriod = period;
Name = cumulative ? $"Iii({period},Cum)" : $"Iii({period})";
_s = new State { LastValidValue = 0.0 };
_ps = _s;
}
///
/// Updates the indicator with a new bar.
///
/// The bar data containing High, Low, Close, and Volume
/// Whether this is a new bar or an update to the current bar
/// The calculated III value
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
double high = bar.High;
double low = bar.Low;
double close = bar.Close;
double volume = Math.Max(bar.Volume, 1.0); // Ensure minimum volume of 1
// Calculate price range
double range = high - low;
// Calculate position multiplier: where close falls in the range
// +1 when close = high, -1 when close = low, 0 when close = midpoint
double positionMultiplier = range > 0 ? (2.0 * close - high - low) / range : 0.0;
// Calculate raw III
double rawIii = positionMultiplier * volume;
// Handle NaN/Infinity
if (!double.IsFinite(rawIii))
{
rawIii = s.LastValidValue;
}
else
{
s.LastValidValue = rawIii;
}
// Update cumulative value
if (isNew)
{
s.CumulativeValue += rawIii;
}
// SMA calculation using ring buffer (for non-cumulative mode)
if (isNew && s.Count >= _period)
{
s.Sum -= _buffer[s.Head];
}
if (isNew)
{
_buffer[s.Head] = rawIii;
s.Sum += rawIii;
s.Head = (s.Head + 1) % _period;
if (s.Count < _period)
{
s.Count++;
}
}
else
{
// For bar correction, update the previous value in buffer
int prevHead = (s.Head + _period - 1) % _period;
double oldValue = _buffer[prevHead];
s.Sum = s.Sum - oldValue + rawIii;
_buffer[prevHead] = rawIii;
// Recalculate cumulative by removing old and adding new
s.CumulativeValue = s.CumulativeValue - oldValue + rawIii;
}
// Calculate result based on mode
double result;
if (_cumulative)
{
result = s.CumulativeValue;
}
else
{
// For SMA: divide by s.Count during warmup, _period once fully warmed
int divisor = s.Count < _period ? s.Count : _period;
result = divisor > 0 ? s.Sum / divisor : 0.0;
}
_s = s;
IsHot = s.Count >= _period;
Last = new TValue(bar.Time, result);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
///
/// TValue input is not supported for III - requires TBar (OHLCV) data.
///
#pragma warning disable S2325 // Method signature must match ITValuePublisher contract
public TValue Update(TValue value, bool isNew = true)
#pragma warning restore S2325
{
throw new NotSupportedException("III requires TBar (OHLCV) data. Use Update(TBar) instead.");
}
///
/// Updates III with a bar series.
///
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);
}
///
/// Resets the indicator to its initial state.
///
public void Reset()
{
_s = new State { LastValidValue = 0.0 };
_ps = _s;
Array.Clear(_buffer);
IsHot = false;
Last = default;
}
///
/// 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);
}
}
///
/// Calculates III for a series of bars.
///
/// The input bar series
/// The smoothing period
/// Whether to use cumulative mode
/// A TSeries containing the III values
public static TSeries Batch(TBarSeries bars, int period = 14, bool cumulative = false)
{
if (bars.Count == 0)
{
return [];
}
var t = bars.Open.Times.ToArray();
var v = new double[bars.Count];
Batch(bars.High.Values, bars.Low.Values, bars.Close.Values, bars.Volume.Values, v, period, cumulative);
return new TSeries(t, v);
}
///
/// Calculates III values using span-based processing.
///
/// Source high prices
/// Source low prices
/// Source close prices
/// Source volumes
/// Output span for III values
/// The smoothing period
/// Whether to use cumulative mode
/// Thrown when spans have different lengths
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan high, ReadOnlySpan low,
ReadOnlySpan close, ReadOnlySpan volume, Span output,
int period = 14, bool cumulative = false)
{
if (high.Length != low.Length)
{
throw new ArgumentException("High and low spans must have the same length", nameof(low));
}
if (high.Length != close.Length)
{
throw new ArgumentException("High and close spans must have the same length", nameof(close));
}
if (high.Length != volume.Length)
{
throw new ArgumentException("High and volume spans must have the same length", nameof(volume));
}
if (high.Length != output.Length)
{
throw new ArgumentException("Output span must have the same length as input", nameof(output));
}
if (period < 1)
{
throw new ArgumentException("Period must be >= 1", nameof(period));
}
int length = high.Length;
if (length == 0)
{
return;
}
const int StackallocThreshold = 256;
double[]? rentedBuffer = null;
scoped Span rawIii;
if (length <= StackallocThreshold)
{
rawIii = stackalloc double[length];
}
else
{
rentedBuffer = System.Buffers.ArrayPool.Shared.Rent(length);
rawIii = rentedBuffer.AsSpan(0, length);
}
try
{
// Calculate raw III values
for (int i = 0; i < length; i++)
{
double range = high[i] - low[i];
double vol = Math.Max(volume[i], 1.0);
double positionMultiplier = range > 0 ? (2.0 * close[i] - high[i] - low[i]) / range : 0.0;
rawIii[i] = positionMultiplier * vol;
if (!double.IsFinite(rawIii[i]))
{
rawIii[i] = i > 0 ? rawIii[i - 1] : 0.0;
}
}
if (cumulative)
{
// Cumulative mode
double cumulativeSum = 0;
for (int i = 0; i < length; i++)
{
cumulativeSum += rawIii[i];
output[i] = cumulativeSum;
}
}
else
{
// Apply SMA smoothing
double sum = 0;
for (int i = 0; i < length; i++)
{
sum += rawIii[i];
if (i >= period)
{
sum -= rawIii[i - period];
output[i] = sum / period;
}
else
{
// During warmup, divide by actual sample count
output[i] = sum / (i + 1);
}
}
}
}
finally
{
if (rentedBuffer != null)
{
System.Buffers.ArrayPool.Shared.Return(rentedBuffer);
}
}
}
public static (TSeries Results, Iii Indicator) Calculate(TBarSeries bars, int period = 14, bool cumulative = false)
{
var indicator = new Iii(period, cumulative);
TSeries results = indicator.Update(bars);
return (results, indicator);
}
}