mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-03 03:47:42 +00:00
411 lines
13 KiB
C#
411 lines
13 KiB
C#
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;
|
||
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;
|
||
_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;
|
||
}
|
||
|
||
/// <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>
|
||
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>
|
||
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)
|
||
{
|
||
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);
|
||
}
|
||
|
||
}
|