using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// UBANDS: Ehlers Ultimate Bands
/// A volatility channel indicator using the Ehlers Ultrasmooth Filter (USF) as the middle band
/// with bands defined by the RMS (Root Mean Square) of residuals from the smooth.
///
///
/// The UBANDS calculation process:
/// 1. Calculate the Ehlers Ultrasmooth Filter (USF) of the source
/// 2. Calculate residuals: source - USF
/// 3. Calculate RMS of residuals over the lookback period
/// 4. Upper band = USF + (multiplier × RMS)
/// 5. Lower band = USF - (multiplier × RMS)
///
/// Key characteristics:
/// - USF provides zero-lag smoothing for the center line
/// - RMS-based bands adapt to actual deviation from the smooth
/// - Multiplier controls band width sensitivity
///
/// Sources:
/// John F. Ehlers - Ultimate Bands (2024)
/// https://www.mesasoftware.com/
///
[SkipLocalsInit]
public sealed class Ubands : AbstractBase
{
private readonly double _multiplier;
private readonly double _c2, _c3;
private readonly double _k0, _k1, _k2;
private readonly RingBuffer _residualBuffer;
private const int DefaultPeriod = 20;
private const double DefaultMultiplier = 1.0;
private const double MinMultiplier = 0.001;
private const int MinPeriod = 1;
// State for streaming with bar correction
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Usf1,
double Usf2,
double PrevInput1,
double PrevInput2,
double LastPrice,
int Bars);
private State _state;
private State _p_state;
public override bool IsHot => _state.Bars >= WarmupPeriod;
///
/// Upper band (middle + mult × RMS)
///
public TValue Upper { get; private set; }
///
/// Middle band (Ehlers Ultrasmooth Filter)
///
public TValue Middle { get; private set; }
///
/// Lower band (middle - mult × RMS)
///
public TValue Lower { get; private set; }
///
/// Band width (Upper - Lower = 2 × mult × RMS)
///
public TValue Width { get; private set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ubands(int period = DefaultPeriod, double multiplier = DefaultMultiplier)
{
if (period < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(period),
$"Period must be at least {MinPeriod}.");
}
if (multiplier < MinMultiplier)
{
throw new ArgumentOutOfRangeException(nameof(multiplier),
$"Multiplier must be at least {MinMultiplier}.");
}
_multiplier = multiplier;
_residualBuffer = new RingBuffer(period);
// Calculate USF coefficients (same as Usf.cs)
double sqrt2_pi = Math.Sqrt(2) * Math.PI;
double arg = sqrt2_pi / period;
double exp_arg = Math.Exp(-arg);
_c2 = 2.0 * exp_arg * Math.Cos(arg);
_c3 = -exp_arg * exp_arg;
double c1 = (1.0 + _c2 - _c3) / 4.0;
// Precompute coefficients for FMA optimization
_k0 = 1.0 - c1; // coefficient for val
_k1 = 2.0 * c1 - _c2; // coefficient for PrevInput1
_k2 = -(c1 + _c3); // coefficient for PrevInput2
WarmupPeriod = period;
Name = $"Ubands({period},{multiplier:F1})";
Init();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Init()
{
_state = default;
_p_state = default;
_residualBuffer.Clear();
Upper = new TValue(DateTime.UtcNow, double.NaN);
Middle = new TValue(DateTime.UtcNow, double.NaN);
Lower = new TValue(DateTime.UtcNow, double.NaN);
Width = new TValue(DateTime.UtcNow, double.NaN);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void HandleStateSnapshot(bool isNew)
{
if (isNew)
{
_p_state = _state;
_residualBuffer.Snapshot();
}
else
{
_state = _p_state;
_residualBuffer.Restore();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (double usf, double upper, double lower) Step(double value, bool isNew)
{
HandleStateSnapshot(isNew);
// Handle NaN/Infinity input
if (!double.IsFinite(value))
{
if (_state.Bars == 0)
{
return (double.NaN, double.NaN, double.NaN);
}
value = _state.LastPrice;
}
else
{
_state.LastPrice = value;
}
_state.Bars++;
// Initialize on first bar
if (_state.Bars == 1)
{
_state.Usf1 = value;
_state.Usf2 = value;
_state.PrevInput1 = value;
_state.PrevInput2 = value;
return (value, value, value);
}
// Calculate USF (Ehlers Ultrasmooth Filter)
double usf;
if (_state.Bars < 4)
{
usf = value;
}
else
{
usf = Math.FusedMultiplyAdd(_c3, _state.Usf2,
Math.FusedMultiplyAdd(_c2, _state.Usf1,
Math.FusedMultiplyAdd(_k2, _state.PrevInput2,
Math.FusedMultiplyAdd(_k1, _state.PrevInput1, _k0 * value))));
// Guard against NaN propagation from state
if (!double.IsFinite(usf))
{
usf = value;
}
}
// Update USF state
_state.Usf2 = _state.Usf1;
_state.Usf1 = usf;
_state.PrevInput2 = _state.PrevInput1;
_state.PrevInput1 = value;
// Calculate residual and add to buffer
double residual = value - usf;
_residualBuffer.Add(residual * residual); // Store squared residual
// Calculate RMS from squared residuals
// Use Max(0, Sum) to protect against floating-point drift making Sum slightly negative
double sumSq = _residualBuffer.Sum;
double rms = (_residualBuffer.Count > 0 && sumSq > 0)
? Math.Sqrt(sumSq / _residualBuffer.Count)
: 0;
// Calculate bands
double bandOffset = _multiplier * rms;
double upper = usf + bandOffset;
double lower = usf - bandOffset;
return (usf, upper, lower);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
var (usf, upper, lower) = Step(input.Value, isNew);
// Update output values
Upper = new TValue(input.Time, upper);
Middle = new TValue(input.Time, usf);
Lower = new TValue(input.Time, lower);
Width = new TValue(input.Time, upper - lower);
Last = Middle;
return Last;
}
///
/// Updates the indicator with a time series and returns the middle band series.
///
public override TSeries Update(TSeries source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
int len = source.Count;
TSeries result = new(capacity: len);
for (int i = 0; i < len; i++)
{
var item = source[i];
Update(item, isNew: true);
result.Add(Last.Time, Last.Value, isNew: true);
}
return result;
}
public override void Reset()
{
_residualBuffer.Clear();
Init();
}
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
step ??= TimeSpan.FromSeconds(1);
DateTime startTime = DateTime.UtcNow;
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(startTime + i * step.Value, source[i]), isNew: true);
}
}
///
/// Calculates Ultimate Bands for the entire series.
///
public static TSeries Batch(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier)
{
Ubands ubands = new(period, multiplier);
return ubands.Update(source);
}
///
/// Calculates Ultimate Bands across data using spans.
///
public static void Batch(
ReadOnlySpan source,
Span upper,
Span middle,
Span lower,
int period = DefaultPeriod,
double multiplier = DefaultMultiplier)
{
int len = source.Length;
if (len != upper.Length || len != middle.Length || len != lower.Length)
{
throw new ArgumentException("All spans must have the same length.", nameof(source));
}
if (period < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(period),
$"Period must be at least {MinPeriod}.");
}
if (multiplier < MinMultiplier)
{
throw new ArgumentOutOfRangeException(nameof(multiplier),
$"Multiplier must be at least {MinMultiplier}.");
}
if (len == 0)
{
return;
}
// Calculate USF coefficients
double sqrt2_pi = Math.Sqrt(2) * Math.PI;
double arg = sqrt2_pi / period;
double exp_arg = Math.Exp(-arg);
double c2 = 2.0 * exp_arg * Math.Cos(arg);
double c3 = -exp_arg * exp_arg;
double c1 = (1.0 + c2 - c3) / 4.0;
double k0 = 1.0 - c1;
double k1 = 2.0 * c1 - c2;
double k2 = -(c1 + c3);
// Use stackalloc for residual buffer if small enough
Span residualSqBuffer = period <= 256 ? stackalloc double[period] : new double[period];
int head = 0;
int count = 0;
double sumSq = 0;
double usf1 = 0, usf2 = 0;
double prevInput1 = 0, prevInput2 = 0;
double lastValidValue = double.NaN;
int usfCount = 0;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValidValue = val;
}
else
{
val = double.IsFinite(lastValidValue) ? lastValidValue : 0;
}
// Initialize on first value
if (usfCount == 0)
{
usf1 = val;
usf2 = val;
prevInput1 = val;
prevInput2 = val;
usfCount = 1;
}
// Calculate USF
double usf;
if (usfCount < 4)
{
usf = val;
}
else
{
usf = Math.FusedMultiplyAdd(c3, usf2,
Math.FusedMultiplyAdd(c2, usf1,
Math.FusedMultiplyAdd(k2, prevInput2,
Math.FusedMultiplyAdd(k1, prevInput1, k0 * val))));
}
usf2 = usf1;
usf1 = usf;
prevInput2 = prevInput1;
prevInput1 = val;
usfCount++;
// Calculate residual squared
double residual = val - usf;
double residualSq = residual * residual;
// Update running sum with ring buffer
if (count == period)
{
sumSq -= residualSqBuffer[head];
count--;
}
sumSq += residualSq;
count++;
residualSqBuffer[head] = residualSq;
head = (head + 1) % period;
// Calculate RMS
double rms = count > 0 ? Math.Sqrt(sumSq / count) : 0;
// Calculate bands
double bandOffset = multiplier * rms;
upper[i] = usf + bandOffset;
middle[i] = usf;
lower[i] = usf - bandOffset;
}
}
public static (TSeries Results, Ubands Indicator) Calculate(TSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier)
{
var indicator = new Ubands(period, multiplier);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}