2026-01-24 19:07:52 -08:00
|
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
|
|
|
|
namespace QuanTAlib;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
[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;
|
2026-01-30 12:47:25 -08:00
|
|
|
|
private int _p_index;
|
2026-01-24 19:07:52 -08:00
|
|
|
|
|
|
|
|
|
|
public override bool IsHot => _index >= WarmupPeriod;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Upper band (resistance level)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TValue Upper { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Lower band (support level)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TValue Lower { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Trend direction: +1 = bullish, -1 = bearish
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TValue Trend { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Band width (Upper - Lower)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
2026-01-30 12:47:25 -08:00
|
|
|
|
_p_index = 0;
|
2026-01-24 23:07:09 -08:00
|
|
|
|
_state = new State(0, 0, 1, 0, false);
|
2026-01-24 19:07:52 -08:00
|
|
|
|
_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;
|
2026-01-30 12:47:25 -08:00
|
|
|
|
_p_index = _index;
|
2026-01-24 19:07:52 -08:00
|
|
|
|
_index++;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Restore previous state
|
|
|
|
|
|
_state = _p_state;
|
2026-01-30 12:47:25 -08:00
|
|
|
|
_index = _p_index;
|
2026-01-24 19:07:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-24 19:07:52 -08:00
|
|
|
|
trend = 1; // Bullish
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-24 19:07:52 -08:00
|
|
|
|
else if (close >= finalUpper)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-24 19:07:52 -08:00
|
|
|
|
trend = -1; // Bearish
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-24 19:07:52 -08:00
|
|
|
|
else
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-24 19:07:52 -08:00
|
|
|
|
trend = prevTrend;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-24 19:07:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update state
|
2026-01-24 23:07:09 -08:00
|
|
|
|
_state = new State(finalUpper, finalLower, trend, close, true);
|
2026-01-24 19:07:52 -08:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Updates the indicator with a bar series and returns the super trend series.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Updates the indicator with a new time series and returns the result series.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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<double> 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Calculates Super Trend Bands for the entire bar series.
|
|
|
|
|
|
/// </summary>
|
2026-02-10 21:33:16 -08:00
|
|
|
|
public static TSeries Batch(TBarSeries source, int period = DefaultPeriod, double multiplier = DefaultMultiplier)
|
2026-01-24 19:07:52 -08:00
|
|
|
|
{
|
|
|
|
|
|
Stbands stbands = new(period, multiplier);
|
|
|
|
|
|
return stbands.Update(source);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Calculates Super Trend Bands across OHLC data using spans.
|
|
|
|
|
|
/// </summary>
|
2026-02-10 21:33:16 -08:00
|
|
|
|
public static void Batch(
|
2026-01-24 19:07:52 -08:00
|
|
|
|
ReadOnlySpan<double> high,
|
|
|
|
|
|
ReadOnlySpan<double> low,
|
|
|
|
|
|
ReadOnlySpan<double> close,
|
|
|
|
|
|
Span<double> upper,
|
|
|
|
|
|
Span<double> lower,
|
|
|
|
|
|
Span<double> 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<double> 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)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-24 19:07:52 -08:00
|
|
|
|
currentTrend = 1;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-24 19:07:52 -08:00
|
|
|
|
else if (c >= finalUpper)
|
2026-01-25 16:01:45 -08:00
|
|
|
|
{
|
2026-01-24 19:07:52 -08:00
|
|
|
|
currentTrend = -1;
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|
2026-01-24 19:07:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
upper[i] = finalUpper;
|
|
|
|
|
|
lower[i] = finalLower;
|
|
|
|
|
|
trend[i] = currentTrend;
|
|
|
|
|
|
prevClose = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|