using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// STBANDS: Super Trend Bands /// An ATR-based dynamic support/resistance channel indicator that adapts to price action. /// Bands only move in the direction favorable to the current trend, creating trailing /// stop-loss levels that follow price movement. /// /// /// The STBands calculation process: /// 1. Calculate ATR over the specified period /// 2. Basic upper band = HL2 + (multiplier × ATR) /// 3. Basic lower band = HL2 - (multiplier × ATR) /// 4. Final upper band: min(basic_upper, prev_upper) unless price closed above prev_upper /// 5. Final lower band: max(basic_lower, prev_lower) unless price closed below prev_lower /// 6. Trend: -1 (bearish) when price ≥ upper, +1 (bullish) when price ≤ lower /// /// Key characteristics: /// - Upper band only moves down (tightens) in downtrends /// - Lower band only moves up (tightens) in uptrends /// - Provides trailing stop-loss levels /// - Trend direction signals potential reversals /// /// Sources: /// Olivier Seban - Original SuperTrend concept /// https://www.tradingview.com/wiki/SuperTrend /// [SkipLocalsInit] public sealed class Stbands : AbstractBase { private readonly double _multiplier; private readonly RingBuffer _trBuffer; private const int DefaultPeriod = 10; private const double DefaultMultiplier = 3.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 FinalUpper, double FinalLower, int Trend, double PrevClose, bool IsInitialized); private State _state; private State _p_state; private int _index; private int _p_index; public override bool IsHot => _index >= WarmupPeriod; /// /// Upper band (resistance level) /// public TValue Upper { get; private set; } /// /// Lower band (support level) /// public TValue Lower { get; private set; } /// /// Trend direction: +1 = bullish, -1 = bearish /// public TValue Trend { get; private set; } /// /// Band width (Upper - Lower) /// public TValue Width { get; private set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stbands(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; _trBuffer = new RingBuffer(period); WarmupPeriod = period; Name = $"Stbands({period},{multiplier:F1})"; Init(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Init() { _index = 0; _p_index = 0; _state = new State(0, 0, 1, 0, false); _p_state = _state; _trBuffer.Clear(); Upper = new TValue(DateTime.UtcNow, 0); Lower = new TValue(DateTime.UtcNow, 0); Trend = new TValue(DateTime.UtcNow, 1); Width = new TValue(DateTime.UtcNow, 0); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double GetFiniteValue(double value, double fallback) => double.IsFinite(value) ? value : fallback; [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { // State management for bar correction if (isNew) { _p_state = _state; _p_index = _index; _index++; } else { // Restore previous state _state = _p_state; _index = _p_index; } double high = GetFiniteValue(input.High, _state.PrevClose); double low = GetFiniteValue(input.Low, _state.PrevClose); double close = GetFiniteValue(input.Close, _state.PrevClose); double prevClose = _state.IsInitialized ? _state.PrevClose : close; // Calculate True Range double hl = high - low; double hpc = Math.Abs(high - prevClose); double lpc = Math.Abs(low - prevClose); double tr = Math.Max(hl, Math.Max(hpc, lpc)); // Add TR to buffer with bar correction support _trBuffer.Add(tr, isNew); // Calculate ATR using buffer's maintained running sum double atr = _trBuffer.Count > 0 ? _trBuffer.Sum / _trBuffer.Count : tr; // Calculate HL2 double hl2 = (high + low) / 2.0; // Calculate basic bands double basicUpper = hl2 + (_multiplier * atr); double basicLower = hl2 - (_multiplier * atr); double finalUpper; double finalLower; int trend; if (!_state.IsInitialized) { // First bar initialization finalUpper = basicUpper; finalLower = basicLower; trend = 1; } else { double prevUpper = _state.FinalUpper; double prevLower = _state.FinalLower; int prevTrend = _state.Trend; // Upper band: only moves down unless price broke above finalUpper = (basicUpper < prevUpper || prevClose > prevUpper) ? basicUpper : prevUpper; // Lower band: only moves up unless price broke below finalLower = (basicLower > prevLower || prevClose < prevLower) ? basicLower : prevLower; // Determine trend if (close <= finalLower) { trend = 1; // Bullish } else if (close >= finalUpper) { trend = -1; // Bearish } else { trend = prevTrend; } } // Update state _state = new State(finalUpper, finalLower, trend, close, true); // Update output values Upper = new TValue(input.Time, finalUpper); Lower = new TValue(input.Time, finalLower); Trend = new TValue(input.Time, trend); Width = new TValue(input.Time, finalUpper - finalLower); // Last returns the band corresponding to trend direction double result = trend > 0 ? finalLower : finalUpper; Last = new TValue(input.Time, result); return Last; } /// /// Updates with TValue - requires High, Low, Close data so this uses the value as Close /// with High = Low = Close (not recommended, use TBar overload instead) /// public override TValue Update(TValue input, bool isNew = true) { // Convert to TBar with O=H=L=C=value, V=0 TBar bar = new(input.Time, input.Value, input.Value, input.Value, input.Value, 0); return Update(bar, isNew); } /// /// Updates the indicator with a bar series and returns the super trend series. /// public TSeries Update(TBarSeries 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 bar = source[i]; Update(bar, isNew: true); result.Add(Last.Time, Last.Value, isNew: true); } return result; } /// /// Updates the indicator with a new time series and returns the result 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() { _trBuffer.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++) { // Treat as close price only Update(new TValue(startTime + i * step.Value, source[i]), isNew: true); } } /// /// Calculates Super Trend Bands for the entire bar series. /// public static TSeries Batch(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { Stbands stbands = new(period, multiplier); return stbands.Update(source); } /// /// Calculates Super Trend Bands across OHLC data using spans. /// public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span upper, Span lower, Span trend, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { int len = high.Length; if (len != low.Length || len != close.Length || len != upper.Length || len != lower.Length || len != trend.Length) { throw new ArgumentException("All spans must have the same length.", nameof(high)); } 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; } // Use stackalloc for TR buffer if small enough Span trBuffer = period <= 256 ? stackalloc double[period] : new double[period]; int head = 0; int count = 0; double trSum = 0; double finalUpper = 0; double finalLower = 0; int currentTrend = 1; double prevClose = close[0]; for (int i = 0; i < len; i++) { double h = double.IsFinite(high[i]) ? high[i] : prevClose; double l = double.IsFinite(low[i]) ? low[i] : prevClose; double c = double.IsFinite(close[i]) ? close[i] : prevClose; // Calculate True Range double hl = h - l; double hpc = i > 0 ? Math.Abs(h - prevClose) : 0; double lpc = i > 0 ? Math.Abs(l - prevClose) : 0; double tr = i > 0 ? Math.Max(hl, Math.Max(hpc, lpc)) : hl; // Update running sum with ring buffer if (count == period) { trSum -= trBuffer[head]; count--; } trSum += tr; count++; trBuffer[head] = tr; head = (head + 1) % period; // Calculate ATR double atr = count > 0 ? trSum / count : tr; // Calculate HL2 and basic bands double hl2 = (h + l) / 2.0; double basicUpper = hl2 + (multiplier * atr); double basicLower = hl2 - (multiplier * atr); if (i == 0) { finalUpper = basicUpper; finalLower = basicLower; currentTrend = 1; } else { // Upper band: only moves down unless price broke above finalUpper = (basicUpper < finalUpper || prevClose > finalUpper) ? basicUpper : finalUpper; // Lower band: only moves up unless price broke below finalLower = (basicLower > finalLower || prevClose < finalLower) ? basicLower : finalLower; // Determine trend if (c <= finalLower) { currentTrend = 1; } else if (c >= finalUpper) { currentTrend = -1; } } upper[i] = finalUpper; lower[i] = finalLower; trend[i] = currentTrend; prevClose = c; } } public static (TSeries Results, Stbands Indicator) Calculate(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier) { var indicator = new Stbands(period, multiplier); TSeries results = indicator.Update(source); return (results, indicator); } }