using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// DECO: Decycler Oscillator
///
///
/// Ehlers' Decycler Oscillator isolates market cycles by computing the difference
/// between two 2-pole Butterworth high-pass filters with different cutoff periods.
/// The shorter HP filter passes more cycle content; the longer HP filter passes less.
/// Their difference reveals the intermediate-frequency band where tradable cycles live.
///
/// Formula (Ehlers, TASC September 2015, Equation 4-2):
///
/// α = (cos(0.707 × 360/period) + sin(0.707 × 360/period) - 1) / cos(0.707 × 360/period)
/// HP[n] = (1 - α/2)² × (x[n] - 2×x[n-1] + x[n-2]) + 2×(1-α)×HP[n-1] - (1-α)²×HP[n-2]
/// DECO = HP_long - HP_short
///
///
/// The 0.707 factor (1/√2) places the filter at the -3 dB point of the Butterworth response.
///
/// References:
/// John F. Ehlers, "Decyclers", Technical Analysis of Stocks & Commodities, September 2015
/// John F. Ehlers, "Cycle Analytics for Traders", Wiley, 2013, Chapter 4
///
[SkipLocalsInit]
public sealed class Deco : AbstractBase
{
private readonly int _shortPeriod;
private readonly int _longPeriod;
// Precomputed HP filter coefficients for short-period filter
private readonly double _a1Short; // (1 - α/2)²
private readonly double _b1Short; // 2 × (1 - α)
private readonly double _c1Short; // -(1 - α)²
// Precomputed HP filter coefficients for long-period filter
private readonly double _a1Long;
private readonly double _b1Long;
private readonly double _c1Long;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double HpShort1,
double HpShort2,
double HpLong1,
double HpLong2,
double Price1,
double Price2,
int Count,
double LastValidValue);
private State _s;
private State _ps;
public override bool IsHot => _s.Count >= WarmupPeriod;
/// Short-period HP cutoff.
public int ShortPeriod => _shortPeriod;
/// Long-period HP cutoff.
public int LongPeriod => _longPeriod;
///
/// Creates a Decycler Oscillator with specified cutoff periods.
///
/// Short HP cutoff period (must be > 0).
/// Long HP cutoff period (must be > shortPeriod).
public Deco(int shortPeriod = 30, int longPeriod = 60)
{
if (shortPeriod <= 0)
{
throw new ArgumentException("Short period must be greater than 0.", nameof(shortPeriod));
}
if (longPeriod <= shortPeriod)
{
throw new ArgumentException("Long period must be greater than short period.", nameof(longPeriod));
}
_shortPeriod = shortPeriod;
_longPeriod = longPeriod;
// Precompute Butterworth HP coefficients: α = (cos(0.707×360/p) + sin(0.707×360/p) - 1) / cos(0.707×360/p)
double rad = 0.707 * 2.0 * Math.PI; // 0.707 × 360° in radians
double argShort = rad / shortPeriod;
double alphaShort = (Math.Cos(argShort) + Math.Sin(argShort) - 1.0) / Math.Cos(argShort);
double oneMinusAlphaHalfShort = 1.0 - alphaShort * 0.5;
double oneMinusAlphaShort = 1.0 - alphaShort;
_a1Short = oneMinusAlphaHalfShort * oneMinusAlphaHalfShort;
_b1Short = 2.0 * oneMinusAlphaShort;
_c1Short = -(oneMinusAlphaShort * oneMinusAlphaShort);
double argLong = rad / longPeriod;
double alphaLong = (Math.Cos(argLong) + Math.Sin(argLong) - 1.0) / Math.Cos(argLong);
double oneMinusAlphaHalfLong = 1.0 - alphaLong * 0.5;
double oneMinusAlphaLong = 1.0 - alphaLong;
_a1Long = oneMinusAlphaHalfLong * oneMinusAlphaHalfLong;
_b1Long = 2.0 * oneMinusAlphaLong;
_c1Long = -(oneMinusAlphaLong * oneMinusAlphaLong);
Name = $"Deco({shortPeriod},{longPeriod})";
WarmupPeriod = longPeriod;
_s = default;
_ps = default;
}
///
/// Creates a chained Decycler Oscillator.
///
public Deco(ITValuePublisher source, int shortPeriod = 30, int longPeriod = 60) : this(shortPeriod, longPeriod)
{
source.Pub += Handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew) { _ps = _s; } else { _s = _ps; }
var s = _s;
double value = input.Value;
if (!double.IsFinite(value))
{
value = double.IsFinite(s.LastValidValue) ? s.LastValidValue : 0.0;
}
else
{
s = s with { LastValidValue = value };
}
double hpShort, hpLong;
if (s.Count < 2)
{
// Not enough history for 2-pole HP — output zero
hpShort = 0.0;
hpLong = 0.0;
s = s with
{
HpShort1 = 0.0,
HpShort2 = 0.0,
HpLong1 = 0.0,
HpLong2 = 0.0,
};
}
else
{
// HP[n] = a1*(x[n] - 2*x[n-1] + x[n-2]) + b1*HP[n-1] + c1*HP[n-2]
double diff = value - 2.0 * s.Price1 + s.Price2;
hpShort = Math.FusedMultiplyAdd(_a1Short, diff, Math.FusedMultiplyAdd(_b1Short, s.HpShort1, _c1Short * s.HpShort2));
hpLong = Math.FusedMultiplyAdd(_a1Long, diff, Math.FusedMultiplyAdd(_b1Long, s.HpLong1, _c1Long * s.HpLong2));
s = s with
{
HpShort2 = s.HpShort1,
HpShort1 = hpShort,
HpLong2 = s.HpLong1,
HpLong1 = hpLong,
};
}
// DECO = HP_long - HP_short (long-period HP passes fewer cycles → more trend-like)
double deco = hpLong - hpShort;
_s = s with { Price2 = s.Price1, Price1 = value, Count = s.Count + 1 };
Last = new TValue(input.Time, deco);
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, _shortPeriod, _longPeriod);
source.Times.CopyTo(tSpan);
// Replay to set internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
foreach (double value in source)
{
Update(new TValue(DateTime.UtcNow, value));
}
}
public override void Reset()
{
_s = default;
_ps = default;
Last = default;
}
///
/// Calculates DECO for an entire series.
///
public static TSeries Batch(TSeries source, int shortPeriod = 30, int longPeriod = 60)
{
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, shortPeriod, longPeriod);
source.Times.CopyTo(tSpan);
return new TSeries(t, v);
}
///
/// Span-based batch DECO calculation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span output, int shortPeriod = 30, int longPeriod = 60)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length.", nameof(output));
}
if (shortPeriod <= 0)
{
throw new ArgumentException("Short period must be greater than 0.", nameof(shortPeriod));
}
if (longPeriod <= shortPeriod)
{
throw new ArgumentException("Long period must be greater than short period.", nameof(longPeriod));
}
int len = source.Length;
if (len == 0) { return; }
double rad = 0.707 * 2.0 * Math.PI;
double argShort = rad / shortPeriod;
double alphaShort = (Math.Cos(argShort) + Math.Sin(argShort) - 1.0) / Math.Cos(argShort);
double omahShort = 1.0 - alphaShort * 0.5;
double omaShort = 1.0 - alphaShort;
double a1S = omahShort * omahShort;
double b1S = 2.0 * omaShort;
double c1S = -(omaShort * omaShort);
double argLong = rad / longPeriod;
double alphaLong = (Math.Cos(argLong) + Math.Sin(argLong) - 1.0) / Math.Cos(argLong);
double omahLong = 1.0 - alphaLong * 0.5;
double omaLong = 1.0 - alphaLong;
double a1L = omahLong * omahLong;
double b1L = 2.0 * omaLong;
double c1L = -(omaLong * omaLong);
double hpS1 = 0, hpS2 = 0, hpL1 = 0, hpL2 = 0;
double price1 = 0, price2 = 0;
double lastValid = 0;
for (int i = 0; i < len; i++)
{
double val = source[i];
if (!double.IsFinite(val)) { val = lastValid; } else { lastValid = val; }
if (i < 2)
{
output[i] = 0.0;
}
else
{
double diff = val - 2.0 * price1 + price2;
double hpS = Math.FusedMultiplyAdd(a1S, diff, Math.FusedMultiplyAdd(b1S, hpS1, c1S * hpS2));
double hpL = Math.FusedMultiplyAdd(a1L, diff, Math.FusedMultiplyAdd(b1L, hpL1, c1L * hpL2));
output[i] = hpL - hpS;
hpS2 = hpS1; hpS1 = hpS;
hpL2 = hpL1; hpL1 = hpL;
}
price2 = price1;
price1 = val;
}
}
///
/// Calculates DECO and returns both results and a primed indicator.
///
public static (TSeries Results, Deco Indicator) Calculate(TSeries source,
int shortPeriod = 30, int longPeriod = 60)
{
var ind = new Deco(shortPeriod, longPeriod);
var results = ind.Update(source);
return (results, ind);
}
}