using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// SSF-DSP: SSF-Based Detrended Synthetic Price - Ehlers' oscillator that removes trend /// from price using dual Super Smooth Filters with quarter-cycle and half-cycle periods. /// /// /// The SSF-based Detrended Synthetic Price indicator creates a synthetic price series /// that oscillates around zero by subtracting a half-cycle SSF from a quarter-cycle SSF. /// Unlike the EMA-based DSP, this version uses Super Smooth Filters which provide /// better smoothing characteristics with minimal lag. /// /// Formula: /// fast_period = max(2, round(period / 4)) /// slow_period = max(3, round(period / 2)) /// arg = sqrt(2) * PI / period /// c1 = 1 - c2 - c3 /// c2 = 2 * exp(-arg) * cos(arg) /// c3 = -exp(-arg)^2 /// input = (price + price[1]) / 2 /// SSF = c1 * input + c2 * SSF[1] + c3 * SSF[2] /// SSF-DSP = SSF_fast - SSF_slow /// /// Properties: /// - Oscillates around zero /// - Removes trend to highlight cycles /// - Super Smooth Filter provides better noise rejection than EMA /// - Quarter-cycle SSF responds quickly to price changes /// - Half-cycle SSF provides the trend reference /// - Crossings above zero indicate bullish momentum /// - Crossings below zero indicate bearish momentum /// /// Key Insight: /// The Super Smooth Filter is a 2-pole Butterworth-style IIR filter that /// provides excellent smoothing with zero lag at the cutoff frequency. /// [SkipLocalsInit] public sealed class Ssfdsp : AbstractBase { private readonly double _c1Fast, _c2Fast, _c3Fast; private readonly double _c1Slow, _c2Slow, _c3Slow; private readonly int _slowPeriod; // State record for snapshot/restore [StructLayout(LayoutKind.Auto)] private record struct State( double SsfFast1, double SsfFast2, double SsfSlow1, double SsfSlow2, double PrevInput, int Count, double LastValidValue ); private State _s; private State _ps; public override bool IsHot => _s.Count >= _slowPeriod * 2; /// /// Creates a new SSF-based Detrended Synthetic Price indicator. /// /// The dominant cycle period (must be >= 4). public Ssfdsp(int period = 40) { if (period < 4) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 4."); } // Calculate fast (quarter-cycle) and slow (half-cycle) periods int fastPeriod = Math.Max(2, (int)Math.Round(period / 4.0)); _slowPeriod = Math.Max(3, (int)Math.Round(period / 2.0)); // Precompute SSF coefficients: sqrt(2) * PI / period double sqrt2Pi = Math.Sqrt(2.0) * Math.PI; // Fast SSF coefficients double argFast = sqrt2Pi / fastPeriod; double expFast = Math.Exp(-argFast); _c2Fast = 2.0 * expFast * Math.Cos(argFast); _c3Fast = -expFast * expFast; _c1Fast = 1.0 - _c2Fast - _c3Fast; // Slow SSF coefficients double argSlow = sqrt2Pi / _slowPeriod; double expSlow = Math.Exp(-argSlow); _c2Slow = 2.0 * expSlow * Math.Cos(argSlow); _c3Slow = -expSlow * expSlow; _c1Slow = 1.0 - _c2Slow - _c3Slow; Name = $"SsfDsp({period})"; WarmupPeriod = _slowPeriod * 2; // Initialize state _s = new State(0, 0, 0, 0, 0, 0, 0); _ps = _s; } /// /// Creates a chained SSF-based Detrended Synthetic Price indicator. /// /// The source indicator to chain from. /// The dominant cycle period. public Ssfdsp(ITValuePublisher source, int period = 40) : this(period) { ArgumentNullException.ThrowIfNull(source); source.Pub += HandleInput; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleInput(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; // Handle non-finite values double value = input.Value; if (!double.IsFinite(value)) { value = s.LastValidValue; } else { s = s with { LastValidValue = value }; } // SSF uses averaged input: (current + previous) / 2 double avgInput = (value + s.PrevInput) * 0.5; // Initialize on first values double ssfFast, ssfSlow; if (s.Count == 0) { // First bar: initialize all SSF values to input ssfFast = avgInput; ssfSlow = avgInput; s = s with { SsfFast1 = avgInput, SsfFast2 = avgInput, SsfSlow1 = avgInput, SsfSlow2 = avgInput }; } else if (s.Count == 1) { // Second bar: use simple average ssfFast = avgInput; ssfSlow = avgInput; s = s with { SsfFast2 = s.SsfFast1, SsfFast1 = avgInput, SsfSlow2 = s.SsfSlow1, SsfSlow1 = avgInput }; } else { // Apply SSF recursion: SSF = c1*input + c2*SSF[1] + c3*SSF[2] ssfFast = Math.FusedMultiplyAdd(_c1Fast, avgInput, Math.FusedMultiplyAdd(_c2Fast, s.SsfFast1, _c3Fast * s.SsfFast2)); ssfSlow = Math.FusedMultiplyAdd(_c1Slow, avgInput, Math.FusedMultiplyAdd(_c2Slow, s.SsfSlow1, _c3Slow * s.SsfSlow2)); s = s with { SsfFast2 = s.SsfFast1, SsfFast1 = ssfFast, SsfSlow2 = s.SsfSlow1, SsfSlow1 = ssfSlow }; } // SSF-DSP = fast SSF - slow SSF double ssfdsp = ssfFast - ssfSlow; // Update state _s = s with { PrevInput = value, Count = s.Count + 1 }; Last = new TValue(input.Time, ssfdsp); 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); // Single pass: advance state and fill output in one iteration int i = 0; foreach (var tv in source) { var result = Update(tv); tSpan[i] = tv.Time; vSpan[i] = result.Value; i++; } return new TSeries(t, v); } public override void Reset() { _s = new State(0, 0, 0, 0, 0, 0, 0); _ps = _s; Last = default; } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double value in source) { Update(new TValue(DateTime.UtcNow, value)); } } /// /// Calculates SSF-DSP for a time series. /// public static TSeries Batch(TSeries source, int period = 40) { var ssfdsp = new Ssfdsp(period); return ssfdsp.Update(source); } /// /// Calculates SSF-DSP in-place using a pre-allocated output span. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int period = 40) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (period < 4) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 4."); } int len = source.Length; if (len == 0) { return; } // Calculate fast (quarter-cycle) and slow (half-cycle) periods int fastPeriod = Math.Max(2, (int)Math.Round(period / 4.0)); int slowPeriod = Math.Max(3, (int)Math.Round(period / 2.0)); // Precompute SSF coefficients double sqrt2Pi = Math.Sqrt(2.0) * Math.PI; double argFast = sqrt2Pi / fastPeriod; double expFast = Math.Exp(-argFast); double c2Fast = 2.0 * expFast * Math.Cos(argFast); double c3Fast = -expFast * expFast; double c1Fast = 1.0 - c2Fast - c3Fast; double argSlow = sqrt2Pi / slowPeriod; double expSlow = Math.Exp(-argSlow); double c2Slow = 2.0 * expSlow * Math.Cos(argSlow); double c3Slow = -expSlow * expSlow; double c1Slow = 1.0 - c2Slow - c3Slow; double ssfFast1 = 0, ssfFast2 = 0; double ssfSlow1 = 0, ssfSlow2 = 0; double prevInput = 0; double lastValid = 0; for (int i = 0; i < len; i++) { double val = source[i]; if (!double.IsFinite(val)) { val = lastValid; } else { lastValid = val; } // SSF uses averaged input double avgInput = (val + prevInput) * 0.5; prevInput = val; double ssfFast, ssfSlow; if (i == 0) { ssfFast = avgInput; ssfSlow = avgInput; ssfFast1 = ssfFast2 = avgInput; ssfSlow1 = ssfSlow2 = avgInput; } else if (i == 1) { ssfFast = avgInput; ssfSlow = avgInput; ssfFast2 = ssfFast1; ssfFast1 = avgInput; ssfSlow2 = ssfSlow1; ssfSlow1 = avgInput; } else { ssfFast = Math.FusedMultiplyAdd(c1Fast, avgInput, Math.FusedMultiplyAdd(c2Fast, ssfFast1, c3Fast * ssfFast2)); ssfSlow = Math.FusedMultiplyAdd(c1Slow, avgInput, Math.FusedMultiplyAdd(c2Slow, ssfSlow1, c3Slow * ssfSlow2)); ssfFast2 = ssfFast1; ssfFast1 = ssfFast; ssfSlow2 = ssfSlow1; ssfSlow1 = ssfSlow; } output[i] = ssfFast - ssfSlow; } } public static (TSeries Results, Ssfdsp Indicator) Calculate(TSeries source, int period = 40) { var indicator = new Ssfdsp(period); TSeries results = indicator.Update(source); return (results, indicator); } }