using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using static System.Math; namespace QuanTAlib; /// /// MSTOCH: Ehlers MESA Stochastic. /// Three-stage pipeline: (1) Roofing Filter = 2-pole Butterworth HP + Super Smoother, /// (2) Standard stochastic on roofing-filtered data, /// (3) Super Smoother of stochastic output. Result clamped to [0,1]. /// All IIR stages are O(1); only the min/max scan in stage 2 is O(stochLength). /// Reference: John F. Ehlers, "Cycle Analytics for Traders" (2013), Chapter 6. /// [SkipLocalsInit] public sealed class Mstoch : ITValuePublisher { private readonly int _stochLength; private readonly int _hpLength; private readonly int _ssLength; // Precomputed IIR coefficients (readonly — fixed at construction) private readonly double _hpC1; private readonly double _hpC2; private readonly double _hpC3; private readonly double _ssC1; private readonly double _ssC2; private readonly double _ssC3; // Ring buffer for Filt values (stage-2 stochastic window) private readonly double[] _filtBuf; [StructLayout(LayoutKind.Auto)] private record struct State( double Src1, // src[t-1] double Src2, // src[t-2] double Hp1, // HP[t-1] double Hp2, // HP[t-2] double Filt1, // Filt[t-1] double Filt2, // Filt[t-2] double Stoc1, // stoc[t-1] (for stage-3 input average) double Mstoc1, // mstoc[t-1] double Mstoc2, // mstoc[t-2] double LastValidSrc, // NaN substitution int BufHead, // ring buffer write head int Count); // bars seen private State _s; private State _ps; public string Name { get; } public int WarmupPeriod { get; } public TValue Last { get; private set; } public bool IsHot => _s.Count >= WarmupPeriod; public event TValuePublishedHandler? Pub; public Mstoch(int stochLength = 20, int hpLength = 48, int ssLength = 10) { if (stochLength < 2) { throw new ArgumentException("Stochastic length must be >= 2", nameof(stochLength)); } if (hpLength < 1) { throw new ArgumentException("HP length must be >= 1", nameof(hpLength)); } if (ssLength < 1) { throw new ArgumentException("SS length must be >= 1", nameof(ssLength)); } _stochLength = stochLength; _hpLength = hpLength; _ssLength = ssLength; // Precompute HP coefficients double hpArg = Sqrt(2.0) * PI / hpLength; double hpExp = Exp(-hpArg); _hpC2 = 2.0 * hpExp * Cos(hpArg); _hpC3 = -(hpExp * hpExp); _hpC1 = (1.0 + _hpC2 - _hpC3) / 4.0; // Precompute Super Smoother coefficients double ssArg = Sqrt(2.0) * PI / ssLength; double ssExp = Exp(-ssArg); _ssC2 = 2.0 * ssExp * Cos(ssArg); _ssC3 = -(ssExp * ssExp); _ssC1 = 1.0 - _ssC2 - _ssC3; _filtBuf = new double[stochLength]; _s = new State(0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, double.NaN, 0, 0); _ps = _s; Name = $"Mstoch({stochLength},{hpLength},{ssLength})"; WarmupPeriod = stochLength + ssLength + 2; // conservative estimate } public Mstoch(ITValuePublisher source, int stochLength = 20, int hpLength = 48, int ssLength = 10) : this(stochLength, hpLength, ssLength) { source.Pub += (object? _, in TValueEventArgs e) => Update(e.Value, e.IsNew); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; double src = input.Value; if (double.IsFinite(src)) { s.LastValidSrc = src; } else { src = double.IsNaN(s.LastValidSrc) ? 0.0 : s.LastValidSrc; } // === Stage 1: Highpass (2-pole Butterworth, removes trend) === // HP = c1*(src - 2*src1 + src2) + c2*hp1 + c3*hp2 double hp = Math.FusedMultiplyAdd( _hpC1, src - 2.0 * s.Src1 + s.Src2, Math.FusedMultiplyAdd(_hpC2, s.Hp1, _hpC3 * s.Hp2)); // === Stage 1: Super Smoother of HP => Filt === // Filt = c1*(hp + hp1)/2 + c2*filt1 + c3*filt2 double filtIn = (hp + s.Hp1) * 0.5; double filt = Math.FusedMultiplyAdd( _ssC1, filtIn, Math.FusedMultiplyAdd(_ssC2, s.Filt1, _ssC3 * s.Filt2)); // === Stage 2: Stochastic on Filt ring buffer === int head = s.BufHead; _filtBuf[head] = filt; int count = s.Count + (isNew ? 1 : 0); if (isNew) { s.Count = count; s.BufHead = (head + 1) % _stochLength; } int filled = Min(count, _stochLength); double highestC = filt; double lowestC = filt; int startHead = isNew ? s.BufHead : head; // new head after increment for (int i = 0; i < filled; i++) { int idx = (startHead - 1 - i + _stochLength) % _stochLength; // For isNew path, startHead = new s.BufHead, so idx wraps correctly double val = _filtBuf[idx]; if (val > highestC) { highestC = val; } if (val < lowestC) { lowestC = val; } } double rangeVal = highestC - lowestC; double stoc = rangeVal > 0.0 ? (filt - lowestC) / rangeVal : 0.5; // === Stage 3: Super Smoother of stochastic === double mstocIn = (stoc + s.Stoc1) * 0.5; double mstoc = Math.FusedMultiplyAdd( _ssC1, mstocIn, Math.FusedMultiplyAdd(_ssC2, s.Mstoc1, _ssC3 * s.Mstoc2)); double result = Max(0.0, Min(1.0, mstoc)); // Update state s.Src2 = s.Src1; s.Src1 = src; s.Hp2 = s.Hp1; s.Hp1 = hp; s.Filt2 = s.Filt1; s.Filt1 = filt; s.Stoc1 = stoc; s.Mstoc2 = s.Mstoc1; s.Mstoc1 = mstoc; _s = s; Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } public TSeries Update(TSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var vSpan = CollectionsMarshal.AsSpan(v); Batch(source.Values, vSpan, _stochLength, _hpLength, _ssLength); source.Times.CopyTo(CollectionsMarshal.AsSpan(t)); // Prime internal state for continued streaming from the last bars Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return new TSeries(t, v); } public void Reset() { Array.Clear(_filtBuf); _s = new State(0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, double.NaN, 0, 0); _ps = _s; Last = default; } // === Static Batch (span) === [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan src, Span output, int stochLength = 20, int hpLength = 48, int ssLength = 10) { if (stochLength < 2) { throw new ArgumentException("Stochastic length must be >= 2", nameof(stochLength)); } if (hpLength < 1) { throw new ArgumentException("HP length must be >= 1", nameof(hpLength)); } if (ssLength < 1) { throw new ArgumentException("SS length must be >= 1", nameof(ssLength)); } if (output.Length < src.Length) { throw new ArgumentException("Output span must be at least as long as input", nameof(output)); } int len = src.Length; if (len == 0) { return; } // Precompute coefficients double hpArg = Sqrt(2.0) * PI / hpLength; double hpExp = Exp(-hpArg); double hpC2 = 2.0 * hpExp * Cos(hpArg); double hpC3 = -(hpExp * hpExp); double hpC1 = (1.0 + hpC2 - hpC3) / 4.0; double ssArg = Sqrt(2.0) * PI / ssLength; double ssExp = Exp(-ssArg); double ssC2 = 2.0 * ssExp * Cos(ssArg); double ssC3 = -(ssExp * ssExp); double ssC1 = 1.0 - ssC2 - ssC3; const int StackallocThreshold = 256; double[]? rentedFilt = null; double[]? rentedBuf = null; scoped Span filtArr; scoped Span filtBuf; if (len <= StackallocThreshold) { filtArr = stackalloc double[len]; } else { rentedFilt = ArrayPool.Shared.Rent(len); filtArr = rentedFilt.AsSpan(0, len); } if (stochLength <= StackallocThreshold) { filtBuf = stackalloc double[stochLength]; } else { rentedBuf = ArrayPool.Shared.Rent(stochLength); filtBuf = rentedBuf.AsSpan(0, stochLength); } filtBuf.Clear(); try { // Pass 1: compute HP + Filt for all bars double prevSrc2 = 0.0, prevSrc1 = 0.0; double prevHp2 = 0.0, prevHp1 = 0.0; double prevFilt2 = 0.0, prevFilt1 = 0.0; for (int i = 0; i < len; i++) { double srcVal = src[i]; double s; if (double.IsFinite(srcVal)) { s = srcVal; } else if (i > 0) { s = src[i - 1]; } else { s = 0.0; } double hp = Math.FusedMultiplyAdd( hpC1, s - 2.0 * prevSrc1 + prevSrc2, Math.FusedMultiplyAdd(hpC2, prevHp1, hpC3 * prevHp2)); double filtIn = (hp + prevHp1) * 0.5; double filt = Math.FusedMultiplyAdd( ssC1, filtIn, Math.FusedMultiplyAdd(ssC2, prevFilt1, ssC3 * prevFilt2)); filtArr[i] = filt; prevSrc2 = prevSrc1; prevSrc1 = s; prevHp2 = prevHp1; prevHp1 = hp; prevFilt2 = prevFilt1; prevFilt1 = filt; } // Pass 2: stochastic + super smoother int bufHead = 0; double prevStoc1 = 0.5; double prevMstoc2 = 0.5, prevMstoc1 = 0.5; for (int i = 0; i < len; i++) { double filt = filtArr[i]; filtBuf[bufHead] = filt; bufHead = (bufHead + 1) % stochLength; int filled = Min(i + 1, stochLength); double highestC = filt; double lowestC = filt; for (int k = 0; k < filled; k++) { int idx = (bufHead - 1 - k + stochLength) % stochLength; double val = filtBuf[idx]; if (val > highestC) { highestC = val; } if (val < lowestC) { lowestC = val; } } double rangeVal = highestC - lowestC; double stoc = rangeVal > 0.0 ? (filt - lowestC) / rangeVal : 0.5; double mstocIn = (stoc + prevStoc1) * 0.5; double mstoc = Math.FusedMultiplyAdd( ssC1, mstocIn, Math.FusedMultiplyAdd(ssC2, prevMstoc1, ssC3 * prevMstoc2)); output[i] = Max(0.0, Min(1.0, mstoc)); prevStoc1 = stoc; prevMstoc2 = prevMstoc1; prevMstoc1 = mstoc; } } finally { if (rentedFilt != null) { ArrayPool.Shared.Return(rentedFilt); } if (rentedBuf != null) { ArrayPool.Shared.Return(rentedBuf); } } } public static TSeries Batch(TSeries source, int stochLength = 20, int hpLength = 48, int ssLength = 10) { if (source == null || source.Count == 0) { return new TSeries([], []); } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); Batch(source.Values, CollectionsMarshal.AsSpan(v), stochLength, hpLength, ssLength); source.Times.CopyTo(CollectionsMarshal.AsSpan(t)); return new TSeries(t, v); } public static (TSeries Result, Mstoch Indicator) Calculate( TSeries source, int stochLength = 20, int hpLength = 48, int ssLength = 10) { var indicator = new Mstoch(stochLength, hpLength, ssLength); var result = indicator.Update(source); return (result, indicator); } }