Files

411 lines
13 KiB
C#
Raw Permalink Normal View History

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;
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;
_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;
2026-01-30 12:47:25 -08:00
_p_index = _index;
_index++;
}
else
{
// Restore previous state
_state = _p_state;
2026-01-30 12:47:25 -08:00
_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)
2026-01-25 16:01:45 -08:00
{
trend = 1; // Bullish
2026-01-25 16:01:45 -08:00
}
else if (close >= finalUpper)
2026-01-25 16:01:45 -08:00
{
trend = -1; // Bearish
2026-01-25 16:01:45 -08:00
}
else
2026-01-25 16:01:45 -08:00
{
trend = prevTrend;
2026-01-25 16:01:45 -08:00
}
}
// 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;
}
/// <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)
{
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(
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
{
currentTrend = 1;
2026-01-25 16:01:45 -08:00
}
else if (c >= finalUpper)
2026-01-25 16:01:45 -08:00
{
currentTrend = -1;
2026-01-25 16:01:45 -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
}