using System.Runtime.CompilerServices;
namespace QuanTAlib;
///
/// ADOSC: Accumulation/Distribution Oscillator (Chaikin Oscillator)
///
///
/// Measures momentum of the ADL using dual EMAs. Positive values indicate accumulation momentum;
/// negative indicates distribution. Standard parameters: fast=3, slow=10.
///
/// Calculation: ADOSC = EMA(ADL, fast) - EMA(ADL, slow).
///
/// Detailed documentation
/// Reference Pine Script implementation
[SkipLocalsInit]
public sealed class Adosc : ITValuePublisher
{
private readonly Ad _ad;
private readonly Ema _emaFast;
private readonly Ema _emaSlow;
///
/// Display name for the indicator.
///
public string Name { get; }
public event TValuePublishedHandler? Pub;
///
/// Current ADOSC value.
///
public TValue Last { get; private set; }
///
/// True if the indicator has enough data to produce valid results.
///
public bool IsHot => _emaSlow.IsHot;
///
/// The number of bars required to warm up the indicator.
///
public int WarmupPeriod { get; }
///
/// Creates ADOSC with specified periods.
///
/// Fast EMA period (default 3)
/// Slow EMA period (default 10)
public Adosc(int fastPeriod = 3, int slowPeriod = 10)
{
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
_ad = new Ad();
_emaFast = new Ema(fastPeriod);
_emaSlow = new Ema(slowPeriod);
WarmupPeriod = slowPeriod;
Name = $"Adosc({fastPeriod},{slowPeriod})";
}
///
/// Resets the indicator state.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_ad.Reset();
_emaFast.Reset();
_emaSlow.Reset();
Last = default;
}
///
/// Updates the indicator with a new ADL value.
///
/// The new ADL value
/// Whether this is a new value or an update to the last value
/// The updated ADOSC value
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
var eFast = _emaFast.Update(input, isNew);
var eSlow = _emaSlow.Update(input, isNew);
double adosc = eFast.Value - eSlow.Value;
Last = new TValue(input.Time, adosc);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
///
/// Updates the indicator with a new bar.
///
/// The new bar data
/// Whether this is a new bar or an update to the last bar
/// The updated ADOSC value
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
var adl = _ad.Update(input, isNew);
return Update(adl, isNew);
}
///
/// Updates the indicator with a series of bars.
///
/// The source series of bars
/// The ADOSC 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);
}
// EMA compensator threshold (same as in Ema.cs)
private const double COMPENSATOR_THRESHOLD = 1e-10;
///
/// 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 ADOSC for the entire series using a new instance.
///
/// Input series
/// Fast EMA period (default 3)
/// Slow EMA period (default 10)
/// ADOSC series
public static TSeries Batch(TBarSeries source, int fastPeriod = 3, int slowPeriod = 10)
{
var adosc = new Adosc(fastPeriod, slowPeriod);
return adosc.Update(source);
}
///
/// Calculates ADOSC for the entire span using a single-pass algorithm.
/// Zero allocation for maximum performance.
/// Uses compensator pattern from EMA for proper early-stage bias correction.
///
/// High prices
/// Low prices
/// Close prices
/// Volume
/// Output span
/// Fast EMA period (default 3)
/// Slow EMA period (default 10)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int fastPeriod = 3, int slowPeriod = 10)
{
if (high.Length != low.Length || high.Length != close.Length ||
high.Length != volume.Length || high.Length != output.Length)
{
throw new ArgumentException("All spans must be of the same length.", nameof(output));
}
if (fastPeriod <= 0)
{
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
}
if (slowPeriod <= 0)
{
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
}
if (fastPeriod >= slowPeriod)
{
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
}
int len = high.Length;
if (len == 0)
{
return;
}
// EMA parameters (same formula as Ema.cs: alpha = 2 / (period + 1))
double alphaFast = 2.0 / (fastPeriod + 1);
double alphaSlow = 2.0 / (slowPeriod + 1);
double decayFast = 1.0 - alphaFast;
double decaySlow = 1.0 - alphaSlow;
// State variables (no heap allocations)
double adl = 0;
double emaFast = 0;
double emaSlow = 0;
double eFast = 1.0; // Compensation factor for fast EMA (starts at 1, decays toward 0)
double eSlow = 1.0; // Compensation factor for slow EMA
bool fastCompensated = false;
bool slowCompensated = false;
// Single pass: compute ADL, both EMAs, and output in one loop
for (int i = 0; i < len; i++)
{
double h = high[i];
double l = low[i];
double c = close[i];
double vol = volume[i];
// 1. Compute Money Flow Multiplier and Volume
double hl = h - l;
double mfm = 0;
if (hl > double.Epsilon)
{
mfm = (c - l - (h - c)) / hl;
}
double mfv = mfm * vol;
// 2. Update ADL (cumulative)
adl += mfv;
// 3. Update Fast EMA with FMA (same pattern as Ema.cs Compute method)
// state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * input)
emaFast = Math.FusedMultiplyAdd(emaFast, decayFast, alphaFast * adl);
// 4. Update Slow EMA with FMA
emaSlow = Math.FusedMultiplyAdd(emaSlow, decaySlow, alphaSlow * adl);
// 5. Compute compensated EMA values (same logic as Ema.cs Compute method)
// Compensator decays: e *= decay, then result = ema / (1 - e) until e <= threshold
double fastValue = 0, slowValue = 0;
if (!fastCompensated)
{
eFast *= decayFast;
if (eFast <= COMPENSATOR_THRESHOLD)
{
fastCompensated = true;
fastValue = emaFast;
}
else
{
fastValue = emaFast / (1.0 - eFast);
}
}
else
{
fastValue = emaFast;
}
if (!slowCompensated)
{
eSlow *= decaySlow;
if (eSlow <= COMPENSATOR_THRESHOLD)
{
slowCompensated = true;
slowValue = emaSlow;
}
else
{
slowValue = emaSlow / (1.0 - eSlow);
}
}
else
{
slowValue = emaSlow;
}
output[i] = fastValue - slowValue;
}
}
public static (TSeries Results, Adosc Indicator) Calculate(TBarSeries source, int fastPeriod = 3, int slowPeriod = 10)
{
var indicator = new Adosc(fastPeriod, slowPeriod);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}