using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// APZ: Adaptive Price Zone
///
///
/// The Adaptive Price Zone (APZ) is a volatility-based technical indicator developed by
/// Lee Leibfarth. It uses a double-smoothed exponential moving average (EMA) with a
/// modified smoothing factor based on sqrt(period) to create adaptive bands around price.
///
/// Calculation:
/// smoothing_period = sqrt(period)
/// alpha = 2 / (smoothing_period + 1)
/// EMA1_price = alpha × price + (1 - alpha) × EMA1_price[1]
/// EMA2_price = alpha × EMA1_price + (1 - alpha) × EMA2_price[1] (middle line)
/// EMA1_range = alpha × (high - low) + (1 - alpha) × EMA1_range[1]
/// EMA2_range = alpha × EMA1_range + (1 - alpha) × EMA2_range[1] (adaptive range)
/// upper = middle + (multiplier × adaptive_range)
/// lower = middle - (multiplier × adaptive_range)
///
/// Key characteristics:
/// - Uses compound warmup compensation for nested EMAs: compensator = 1/(1-beta²)
/// - Faster response than standard EMAs due to sqrt(period) smoothing
/// - Bands adapt to volatility via the high-low range
/// - O(1) complexity per update
///
/// Sources:
/// Leibfarth, Lee (2006). "Trading With An Adaptive Price Zone," Technical Analysis of
/// Stocks & Commodities, Volume 24:9.
///
[SkipLocalsInit]
public sealed class Apz : ITValuePublisher
{
private readonly int _period;
private readonly double _multiplier;
private readonly double _alpha;
private readonly double _beta;
private readonly double _betaSquared;
private readonly TBarPublishedHandler _barHandler;
private const double ConvergenceThreshold = 1e-10;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Ema1Price,
double Ema2Price,
double Ema1Range,
double Ema2Range,
double E, // Warmup decay factor
double LastValidPrice,
double LastValidHigh,
double LastValidLow,
bool IsHot
)
{
public static State New() => new()
{
Ema1Price = 0,
Ema2Price = 0,
Ema1Range = 0,
Ema2Range = 0,
E = 1.0,
LastValidPrice = double.NaN,
LastValidHigh = double.NaN,
LastValidLow = double.NaN,
IsHot = false,
};
}
private State _state;
private State _p_state;
///
/// Display name for the indicator.
///
public string Name { get; }
///
/// Number of periods before the indicator is considered "hot" (valid).
///
public int WarmupPeriod { get; }
///
/// Current middle band value (double-smoothed EMA of price).
///
public TValue Last { get; private set; }
///
/// Current upper band value.
///
public TValue Upper { get; private set; }
///
/// Current lower band value.
///
public TValue Lower { get; private set; }
///
/// True if the indicator has converged (warmup decay below threshold).
///
public bool IsHot => _state.IsHot;
///
/// Event triggered when a new TValue is available.
///
public event TValuePublishedHandler? Pub;
///
/// Creates APZ with specified period and multiplier.
///
/// Lookback period (sqrt applied internally for smoothing, must be > 0)
/// Multiplier for band width (must be > 0, default: 2.0)
public Apz(int period, double multiplier = 2.0)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
_period = period;
_multiplier = multiplier;
double smoothPeriod = Math.Sqrt(period);
_alpha = 2.0 / (smoothPeriod + 1.0);
_beta = 1.0 - _alpha;
_betaSquared = _beta * _beta;
Name = $"Apz({period},{multiplier:F2})";
// Warmup is based on EMA convergence - use period as approximation
WarmupPeriod = period;
_state = State.New();
_p_state = _state;
_barHandler = HandleBar;
}
///
/// Creates APZ with TBarSeries source.
///
public Apz(TBarSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
{
Prime(source);
source.Pub += _barHandler;
}
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
///
/// Helper to invoke the Pub event.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew = true)
{
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
///
/// Gets valid input values, using last-value substitution for non-finite inputs.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (double price, double high, double low) GetValidValues(double price, double high, double low)
{
if (double.IsFinite(price))
{
_state.LastValidPrice = price;
}
else
{
price = _state.LastValidPrice;
}
if (double.IsFinite(high))
{
_state.LastValidHigh = high;
}
else
{
high = _state.LastValidHigh;
}
if (double.IsFinite(low))
{
_state.LastValidLow = low;
}
else
{
low = _state.LastValidLow;
}
return (price, high, low);
}
///
/// Core calculation with warmup compensation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (double middle, double upper, double lower) Compute(double price, double range)
{
// Double-smoothed EMA for price
_state.Ema1Price = Math.FusedMultiplyAdd(_state.Ema1Price, _beta, _alpha * price);
_state.Ema2Price = Math.FusedMultiplyAdd(_state.Ema2Price, _beta, _alpha * _state.Ema1Price);
// Double-smoothed EMA for range
_state.Ema1Range = Math.FusedMultiplyAdd(_state.Ema1Range, _beta, _alpha * range);
_state.Ema2Range = Math.FusedMultiplyAdd(_state.Ema2Range, _beta, _alpha * _state.Ema1Range);
double middle = _state.Ema2Price;
double adaptiveRange = _state.Ema2Range;
// Apply compound warmup compensation
if (!_state.IsHot)
{
_state.E *= _betaSquared;
double compensator = 1.0 / (1.0 - _state.E);
middle *= compensator;
adaptiveRange *= compensator;
if (_state.E <= ConvergenceThreshold)
{
_state.IsHot = true;
}
}
double bandWidth = _multiplier * adaptiveRange;
return (middle, middle + bandWidth, middle - bandWidth);
}
///
/// Updates the indicator with a TBar input.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
var (price, high, low) = GetValidValues(input.Close, input.High, input.Low);
// Handle first value initialization
if (double.IsNaN(_state.LastValidPrice))
{
Last = new TValue(input.Time, double.NaN);
Upper = new TValue(input.Time, double.NaN);
Lower = new TValue(input.Time, double.NaN);
PubEvent(Last, isNew);
return Last;
}
double range = high - low;
if (range < 0)
{
range = 0; // Safety check
}
var (middle, upper, lower) = Compute(price, range);
Last = new TValue(input.Time, middle);
Upper = new TValue(input.Time, upper);
Lower = new TValue(input.Time, lower);
PubEvent(Last, isNew);
return Last;
}
///
/// Updates the indicator with a TBarSeries.
///
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List(len);
var vMiddle = new List(len);
var tUpper = new List(len);
var vUpper = new List(len);
var tLower = new List(len);
var vLower = new List(len);
CollectionsMarshal.SetCount(tMiddle, len);
CollectionsMarshal.SetCount(vMiddle, len);
CollectionsMarshal.SetCount(tUpper, len);
CollectionsMarshal.SetCount(vUpper, len);
CollectionsMarshal.SetCount(tLower, len);
CollectionsMarshal.SetCount(vLower, len);
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
// Use batch calculation and capture final state for continued streaming
var finalState = BatchWithState(source.High.Values, source.Low.Values, source.Close.Values,
new BatchOutputs(vMiddleSpan, vUpperSpan, vLowerSpan), _period, _multiplier);
source.Times.CopyTo(tSpan);
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
// Restore state from batch calculation (no re-processing)
_state = new State(
Ema1Price: finalState.Ema1Price,
Ema2Price: finalState.Ema2Price,
Ema1Range: finalState.Ema1Range,
Ema2Range: finalState.Ema2Range,
E: finalState.E,
LastValidPrice: finalState.LastValidPrice,
LastValidHigh: finalState.LastValidHigh,
LastValidLow: finalState.LastValidLow,
IsHot: finalState.IsHot
);
_p_state = _state;
// Update Last/Upper/Lower from final computed values
if (len > 0)
{
var lastTime = source.Times[len - 1];
Last = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vMiddleSpan[len - 1]);
Upper = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vUpperSpan[len - 1]);
Lower = new TValue(new DateTime(lastTime, DateTimeKind.Utc), vLowerSpan[len - 1]);
}
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
///
/// Initializes the indicator state using the provided TBarSeries history.
///
public void Prime(TBarSeries source)
{
if (source.Count == 0)
{
return;
}
// Reset state
_state = State.New();
_p_state = _state;
// Use all available data for priming to ensure proper convergence
const int startIndex = 0;
// Find first valid values in the data
if (double.IsNaN(_state.LastValidPrice))
{
for (int i = startIndex; i < source.Count; i++)
{
var bar = source[i];
if (double.IsFinite(bar.Close))
{
_state.LastValidPrice = bar.Close;
_state.LastValidHigh = bar.High;
_state.LastValidLow = bar.Low;
break;
}
}
}
// Feed the warmup data
for (int i = startIndex; i < source.Count; i++)
{
var bar = source[i];
var (price, high, low) = GetValidValues(bar.Close, bar.High, bar.Low);
if (double.IsFinite(price))
{
double range = Math.Max(0, high - low);
var (middle, upper, lower) = Compute(price, range);
Last = new TValue(bar.Time, middle);
Upper = new TValue(bar.Time, upper);
Lower = new TValue(bar.Time, lower);
}
}
_p_state = _state;
}
///
/// Resets the indicator state.
///
public void Reset()
{
_state = State.New();
_p_state = _state;
Last = default;
Upper = default;
Lower = default;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Static Batch Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
///
/// Output buffers for batch APZ calculation.
///
[StructLayout(LayoutKind.Auto)]
#pragma warning disable S1104 // Fields should not have public accessibility
public ref struct BatchOutputs
{
/// Output middle band (double-smoothed EMA of price)
public Span Middle;
/// Output upper band
public Span Upper;
/// Output lower band
public Span Lower;
#pragma warning restore S1104
///
/// Creates a new BatchOutputs instance.
///
public BatchOutputs(Span middle, Span upper, Span lower)
{
Middle = middle;
Upper = upper;
Lower = lower;
}
}
///
/// Internal state for scalar calculation.
///
[StructLayout(LayoutKind.Auto)]
private ref struct ScalarState
{
internal double Ema1Price;
internal double Ema2Price;
internal double Ema1Range;
internal double Ema2Range;
internal double E;
internal double LastValidPrice;
internal double LastValidHigh;
internal double LastValidLow;
internal bool IsHot;
}
///
/// Calculates APZ for the entire TBarSeries using a new instance.
///
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double multiplier = 2.0)
{
var apz = new Apz(period, multiplier);
return apz.Update(source);
}
///
/// Calculates APZ in-place using spans for maximum performance.
/// Zero-allocation method.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
BatchOutputs outputs,
int period,
double multiplier = 2.0)
{
int len = close.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("Input spans must have the same length", nameof(high));
}
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return;
}
CalculateScalarCore(high, low, close, outputs, period, multiplier);
}
///
/// Batch calculation that returns final state for continued streaming.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ScalarState BatchWithState(
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
BatchOutputs outputs,
int period,
double multiplier)
{
int len = close.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("Input spans must have the same length", nameof(high));
}
if (outputs.Middle.Length < len || outputs.Upper.Length < len || outputs.Lower.Length < len)
{
throw new ArgumentException("Output buffers must be at least as long as input", nameof(outputs));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
if (multiplier <= 0)
{
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
}
if (len == 0)
{
return new ScalarState();
}
return CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
BatchOutputs outputs,
int period,
double multiplier)
{
_ = CalculateScalarCoreWithState(high, low, close, outputs, period, multiplier);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ScalarState CalculateScalarCoreWithState(
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
BatchOutputs outputs,
int period,
double multiplier)
{
int len = close.Length;
double smoothPeriod = Math.Sqrt(period);
double alpha = 2.0 / (smoothPeriod + 1.0);
double beta = 1.0 - alpha;
double betaSquared = beta * beta;
Span middle = outputs.Middle;
Span upper = outputs.Upper;
Span lower = outputs.Lower;
var state = new ScalarState
{
Ema1Price = 0,
Ema2Price = 0,
Ema1Range = 0,
Ema2Range = 0,
E = 1.0,
LastValidPrice = double.NaN,
LastValidHigh = double.NaN,
LastValidLow = double.NaN,
IsHot = false,
};
// Seed first valid values
SeedFirstValidValues(high, low, close, ref state);
for (int i = 0; i < len; i++)
{
double price = close[i];
double h = high[i];
double l = low[i];
// Get valid values
if (double.IsFinite(price))
{
state.LastValidPrice = price;
}
else
{
price = state.LastValidPrice;
}
if (double.IsFinite(h))
{
state.LastValidHigh = h;
}
else
{
h = state.LastValidHigh;
}
if (double.IsFinite(l))
{
state.LastValidLow = l;
}
else
{
l = state.LastValidLow;
}
// Handle first valid value
if (double.IsNaN(price))
{
middle[i] = double.NaN;
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
double range = Math.Max(0, h - l);
// Double-smoothed EMA for price
state.Ema1Price = Math.FusedMultiplyAdd(state.Ema1Price, beta, alpha * price);
state.Ema2Price = Math.FusedMultiplyAdd(state.Ema2Price, beta, alpha * state.Ema1Price);
// Double-smoothed EMA for range
state.Ema1Range = Math.FusedMultiplyAdd(state.Ema1Range, beta, alpha * range);
state.Ema2Range = Math.FusedMultiplyAdd(state.Ema2Range, beta, alpha * state.Ema1Range);
double mid = state.Ema2Price;
double adaptiveRange = state.Ema2Range;
// Apply compound warmup compensation
if (!state.IsHot)
{
state.E *= betaSquared;
double compensator = 1.0 / (1.0 - state.E);
mid *= compensator;
adaptiveRange *= compensator;
if (state.E <= ConvergenceThreshold)
{
state.IsHot = true;
}
}
double bandWidth = multiplier * adaptiveRange;
middle[i] = mid;
upper[i] = mid + bandWidth;
lower[i] = mid - bandWidth;
}
return state;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SeedFirstValidValues(
ReadOnlySpan high,
ReadOnlySpan low,
ReadOnlySpan close,
ref ScalarState state)
{
int len = close.Length;
for (int k = 0; k < len; k++)
{
// Check all three values are finite before assigning state
if (double.IsFinite(close[k]) && double.IsFinite(high[k]) && double.IsFinite(low[k]))
{
state.LastValidPrice = close[k];
state.LastValidHigh = high[k];
state.LastValidLow = low[k];
break;
}
}
}
///
/// Runs a high-performance batch calculation and returns a "Hot" APZ instance.
///
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Apz Indicator) Calculate(TBarSeries source, int period, double multiplier = 2.0)
{
var apz = new Apz(period, multiplier);
var results = apz.Update(source);
return (results, apz);
}
}