Files
QuanTAlib/lib/cycles/stc/Stc.cs
T
Miha Kralj da4e56bf40 feat: Add new CodeQL extension for C# and SonarLint configuration
- Introduced a new CodeQL extension for C# in `.github/codeql/extensions/quantalib-csharp/codeql-pack.yml`.
- Added SonarLint configuration in `.sonarlint/CSharp/SonarLint.xml` and `.sonarlint/csharp.ruleset` to suppress specific rules for high-performance indicators.
- Removed outdated `.vscode/launch.json` configurations.
- Updated `.vscode/tasks.json` to streamline build and test tasks, including renaming and consolidating tasks.
- Modified `Directory.Build.props` to enhance SARIF output directory handling and integrate SonarLint rules.
- Refactored various indicator classes to improve code clarity and maintainability, including updates to method parameters for consistency.
- Added XML documentation comments to several classes and methods for better code understanding.
- Improved numerical stability in calculations by replacing direct comparisons with `double.Epsilon` checks in multiple classes.
2026-01-21 23:05:38 -06:00

561 lines
18 KiB
C#

using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
public enum StcSmoothing { None = 0, Ema = 1, Sigmoid = 2, Digital = 3 }
[SkipLocalsInit]
public sealed class Stc : AbstractBase
{
private readonly StcSmoothing _smoothing;
private readonly double _fastAlpha;
private readonly double _slowAlpha;
private readonly double _dAlpha;
private readonly RingBuffer _macdBuf;
private readonly RingBuffer _stoch1Buf;
private readonly ITValuePublisher? _publisher;
private readonly TValuePublishedHandler? _handler;
private bool _isNew;
[StructLayout(LayoutKind.Sequential)]
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
private struct State
{
public double FastEma;
public double SlowEma;
public double Stoch1Ema;
public double Stoch2Ema;
public double PrevStc;
public double LastFiniteInput;
public bool HasFiniteInput;
public double MacdMin;
public double MacdMax;
public double Stoch1Min;
public double Stoch1Max;
}
#pragma warning restore CA1066
private State _s, _ps;
private int _samples;
public Stc(
int kPeriod = 10,
int dPeriod = 3,
int fastLength = 23,
int slowLength = 50,
StcSmoothing smoothing = StcSmoothing.Ema)
{
ArgumentOutOfRangeException.ThrowIfLessThan(kPeriod, 2);
ArgumentOutOfRangeException.ThrowIfLessThan(dPeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(fastLength, 2);
ArgumentOutOfRangeException.ThrowIfLessThan(slowLength, 2);
_smoothing = smoothing;
_fastAlpha = 2.0 / (fastLength + 1.0);
_slowAlpha = 2.0 / (slowLength + 1.0);
_dAlpha = 2.0 / (dPeriod + 1.0);
int bufSize = kPeriod;
_macdBuf = new RingBuffer(bufSize);
_stoch1Buf = new RingBuffer(bufSize);
Name = $"Stc(k={kPeriod},d={dPeriod},fast={fastLength},slow={slowLength},{smoothing})";
WarmupPeriod = slowLength + bufSize;
Reset();
}
public Stc(ITValuePublisher source, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema)
: this(kPeriod, dPeriod, fastLength, slowLength, smoothing)
{
_publisher = source;
_handler = Handle;
source.Pub += _handler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
public bool IsNew => _isNew;
public override bool IsHot => _samples >= WarmupPeriod;
public override void Reset()
{
_s = new State
{
FastEma = double.NaN,
SlowEma = double.NaN,
Stoch1Ema = double.NaN,
Stoch2Ema = double.NaN,
PrevStc = double.NaN,
LastFiniteInput = double.NaN,
HasFiniteInput = false,
MacdMin = double.PositiveInfinity,
MacdMax = double.NegativeInfinity,
Stoch1Min = double.PositiveInfinity,
Stoch1Max = double.NegativeInfinity,
};
_ps = _s;
_samples = 0;
_macdBuf.Clear();
_stoch1Buf.Clear();
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Clamp100(double x)
{
if (double.IsNaN(x)) return x;
return Math.Clamp(x, 0, 100);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UpdateMinMax(double added, double removed, bool hasRemoved, RingBuffer buf, ref double min, ref double max)
{
if (double.IsNaN(added)) return;
bool expandMin = added < min;
bool expandMax = added > max;
if (!hasRemoved)
{
if (expandMin) min = added;
if (expandMax) max = added;
return;
}
// Use relative tolerance for floating-point comparison
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
bool removedMin = Math.Abs(removed - min) <= tolerance;
bool removedMax = Math.Abs(removed - max) <= tolerance;
if (expandMin) min = added;
if (expandMax) max = added;
if ((removedMin && !expandMin) || (removedMax && !expandMax))
{
var span = buf.IsFull ? buf.InternalBuffer : buf.GetSpan();
min = double.PositiveInfinity;
max = double.NegativeInfinity;
foreach (double v in span)
{
if (double.IsNaN(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
}
}
}
// Overload for Span based buffers (Calculate)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UpdateMinMax(double added, double removed, bool hasRemoved, ReadOnlySpan<double> buf, ref double min, ref double max)
{
if (double.IsNaN(added)) return;
bool expandMin = added < min;
bool expandMax = added > max;
if (!hasRemoved)
{
if (expandMin) min = added;
if (expandMax) max = added;
return;
}
// Use relative tolerance for floating-point comparison
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
bool removedMin = Math.Abs(removed - min) <= tolerance;
bool removedMax = Math.Abs(removed - max) <= tolerance;
if (expandMin) min = added;
if (expandMax) max = added;
if ((removedMin && !expandMin) || (removedMax && !expandMax))
{
min = double.PositiveInfinity;
max = double.NegativeInfinity;
foreach (double v in buf)
{
if (double.IsNaN(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// skipcq: CS-R1140
public override TValue Update(TValue input, bool isNew = true)
{
_isNew = isNew;
if (isNew) _ps = _s;
else _s = _ps;
var s = _s;
double x = input.Value;
if (!double.IsFinite(x))
{
if (!s.HasFiniteInput)
{
Last = new TValue(input.Time, double.NaN);
PubEvent(Last, isNew);
return Last;
}
x = s.LastFiniteInput;
}
else
{
s.LastFiniteInput = x;
s.HasFiniteInput = true;
}
// 1) MACD
s.FastEma = double.IsNaN(s.FastEma) ? x : Math.FusedMultiplyAdd(_fastAlpha, x - s.FastEma, s.FastEma);
s.SlowEma = double.IsNaN(s.SlowEma) ? x : Math.FusedMultiplyAdd(_slowAlpha, x - s.SlowEma, s.SlowEma);
double macd = s.FastEma - s.SlowEma;
double removedMacd = 0;
bool hasRemovedMacd;
if (isNew)
{
hasRemovedMacd = _macdBuf.IsFull;
removedMacd = _macdBuf.Add(macd);
}
else
{
removedMacd = _macdBuf.Newest;
hasRemovedMacd = _macdBuf.Count > 0;
_macdBuf.UpdateNewest(macd);
}
UpdateMinMax(macd, removedMacd, hasRemovedMacd, _macdBuf, ref s.MacdMin, ref s.MacdMax);
// 2) Stoch1 of MACD
double stoch1Raw;
if (_macdBuf.IsFull)
{
double span = s.MacdMax - s.MacdMin;
if (span > double.Epsilon)
stoch1Raw = 100.0 * (macd - s.MacdMin) / span;
else
stoch1Raw = double.IsNaN(s.Stoch1Ema) ? 50.0 : s.Stoch1Ema;
stoch1Raw = Clamp100(stoch1Raw);
}
else
{
stoch1Raw = 50.0;
}
// Smooth Stoch1
if (!double.IsNaN(stoch1Raw))
{
s.Stoch1Ema = double.IsNaN(s.Stoch1Ema)
? stoch1Raw
: Math.FusedMultiplyAdd(_dAlpha, stoch1Raw - s.Stoch1Ema, s.Stoch1Ema);
}
double stoch1 = double.NaN;
if (!double.IsNaN(s.Stoch1Ema))
{
stoch1 = Clamp100(s.Stoch1Ema);
double removedStoch1 = 0;
bool hasRemovedStoch1;
if (isNew)
{
hasRemovedStoch1 = _stoch1Buf.IsFull;
removedStoch1 = _stoch1Buf.Add(stoch1);
}
else
{
removedStoch1 = _stoch1Buf.Newest;
hasRemovedStoch1 = _stoch1Buf.Count > 0;
_stoch1Buf.UpdateNewest(stoch1);
}
UpdateMinMax(stoch1, removedStoch1, hasRemovedStoch1, _stoch1Buf, ref s.Stoch1Min, ref s.Stoch1Max);
}
// 3) Stoch2 of Stoch1
double stoch2Raw;
if (_stoch1Buf.IsFull)
{
double span = s.Stoch1Max - s.Stoch1Min;
if (span > double.Epsilon)
stoch2Raw = 100.0 * (stoch1 - s.Stoch1Min) / span;
else
stoch2Raw = double.IsNaN(s.Stoch2Ema) ? stoch1 : s.Stoch2Ema;
stoch2Raw = Clamp100(stoch2Raw);
}
else
{
stoch2Raw = stoch1;
}
// 4) Final Smooth
double stc = double.NaN;
if (!double.IsNaN(stoch2Raw))
{
switch (_smoothing)
{
case StcSmoothing.Ema:
s.Stoch2Ema = double.IsNaN(s.Stoch2Ema)
? stoch2Raw
: Math.FusedMultiplyAdd(_dAlpha, stoch2Raw - s.Stoch2Ema, s.Stoch2Ema);
stc = Clamp100(s.Stoch2Ema);
break;
case StcSmoothing.Sigmoid:
stc = 100.0 / (1.0 + Math.Exp(-0.1 * (stoch2Raw - 50.0)));
break;
case StcSmoothing.Digital:
if (stoch2Raw > 75) stc = 100;
else if (stoch2Raw < 25) stc = 0;
else stc = double.IsNaN(s.PrevStc) ? stoch2Raw : s.PrevStc;
break;
case StcSmoothing.None:
stc = stoch2Raw;
break;
default:
stc = stoch2Raw;
break;
}
s.PrevStc = stc;
}
if (isNew) _samples++;
_s = s;
Last = new TValue(input.Time, stc);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
var result = new TSeries();
foreach (var item in source)
result.Add(Update(item, isNew: true));
return result;
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double v in source)
Update(new TValue(DateTime.MinValue, v), isNew: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void Dispose(bool disposing)
{
if (disposing && _publisher != null && _handler != null)
{
_publisher.Pub -= _handler;
}
base.Dispose(disposing);
}
// skipcq: CS-R1140
public static void Calculate(ReadOnlySpan<double> source, Span<double> output,
int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
double fastAlpha = 2.0 / (fastLength + 1.0);
double slowAlpha = 2.0 / (slowLength + 1.0);
double dAlpha = 2.0 / (dPeriod + 1.0);
double fastEma = double.NaN;
double slowEma = double.NaN;
double stoch1Ema = double.NaN;
double stoch2Ema = double.NaN;
double prevStc = double.NaN;
double lastFiniteInput = double.NaN;
bool hasFiniteInput = false;
const int StackallocThreshold = 256;
double[]? rentedMacd = null;
double[]? rentedStoch1 = null;
scoped Span<double> macdBuf;
scoped Span<double> stoch1Buf;
if (kPeriod <= StackallocThreshold)
{
macdBuf = stackalloc double[kPeriod];
stoch1Buf = stackalloc double[kPeriod];
}
else
{
rentedMacd = ArrayPool<double>.Shared.Rent(kPeriod);
macdBuf = rentedMacd.AsSpan(0, kPeriod);
rentedStoch1 = ArrayPool<double>.Shared.Rent(kPeriod);
stoch1Buf = rentedStoch1.AsSpan(0, kPeriod);
}
try
{
int macdIdx = 0;
int stoch1Idx = 0;
int macdCount = 0;
int stoch1Count = 0;
double macdMin = double.PositiveInfinity;
double macdMax = double.NegativeInfinity;
double stoch1Min = double.PositiveInfinity;
double stoch1Max = double.NegativeInfinity;
for (int i = 0; i < source.Length; i++)
{
double x = source[i];
if (!double.IsFinite(x))
{
if (!hasFiniteInput)
{
output[i] = double.NaN;
continue;
}
x = lastFiniteInput;
}
else
{
lastFiniteInput = x;
hasFiniteInput = true;
}
// 1) MACD
fastEma = double.IsNaN(fastEma) ? x : Math.FusedMultiplyAdd(fastAlpha, x - fastEma, fastEma);
slowEma = double.IsNaN(slowEma) ? x : Math.FusedMultiplyAdd(slowAlpha, x - slowEma, slowEma);
double macd = fastEma - slowEma;
// Buffer MACD
bool macdHasRemoved = macdCount == kPeriod;
double macdRemoved = macdBuf[macdIdx];
macdBuf[macdIdx] = macd;
macdIdx = (macdIdx + 1) % kPeriod;
if (!macdHasRemoved) macdCount++;
ReadOnlySpan<double> macdValidSpan = macdBuf.Slice(0, macdCount);
UpdateMinMax(macd, macdRemoved, macdHasRemoved, macdValidSpan, ref macdMin, ref macdMax);
// 2) Stoch1
double stoch1Raw;
if (macdCount == kPeriod)
{
double span = macdMax - macdMin;
if (span > double.Epsilon)
stoch1Raw = 100.0 * (macd - macdMin) / span;
else
stoch1Raw = double.IsNaN(stoch1Ema) ? 50.0 : stoch1Ema;
stoch1Raw = Clamp100(stoch1Raw);
}
else
{
stoch1Raw = 50.0;
}
// Smooth Stoch1
if (!double.IsNaN(stoch1Raw))
{
stoch1Ema = double.IsNaN(stoch1Ema)
? stoch1Raw
: Math.FusedMultiplyAdd(dAlpha, stoch1Raw - stoch1Ema, stoch1Ema);
}
double stoch1 = double.NaN;
if (!double.IsNaN(stoch1Ema))
{
stoch1 = Clamp100(stoch1Ema);
// Buffer Stoch1
bool stochHasRemoved = stoch1Count == kPeriod;
double stochRemoved = stoch1Buf[stoch1Idx];
stoch1Buf[stoch1Idx] = stoch1;
stoch1Idx = (stoch1Idx + 1) % kPeriod;
if (!stochHasRemoved) stoch1Count++;
ReadOnlySpan<double> stochValidSpan = stoch1Buf.Slice(0, stoch1Count);
UpdateMinMax(stoch1, stochRemoved, stochHasRemoved, stochValidSpan, ref stoch1Min, ref stoch1Max);
}
// 3) Stoch2
double stoch2Raw;
if (stoch1Count == kPeriod)
{
double span = stoch1Max - stoch1Min;
if (span > double.Epsilon)
stoch2Raw = 100.0 * (stoch1 - stoch1Min) / span;
else
stoch2Raw = double.IsNaN(stoch2Ema) ? stoch1 : stoch2Ema;
stoch2Raw = Clamp100(stoch2Raw);
}
else
{
stoch2Raw = stoch1;
}
// 4) Final Smooth
double stc = double.NaN;
if (!double.IsNaN(stoch2Raw))
{
if (smoothing == StcSmoothing.Ema)
{
stoch2Ema = double.IsNaN(stoch2Ema)
? stoch2Raw
: Math.FusedMultiplyAdd(dAlpha, stoch2Raw - stoch2Ema, stoch2Ema);
stc = Clamp100(stoch2Ema);
}
else if (smoothing == StcSmoothing.Sigmoid)
{
stc = 100.0 / (1.0 + Math.Exp(-0.1 * (stoch2Raw - 50.0)));
}
else if (smoothing == StcSmoothing.Digital)
{
if (stoch2Raw > 75) stc = 100;
else if (stoch2Raw < 25) stc = 0;
else stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc;
}
else
{
stc = stoch2Raw;
}
prevStc = stc;
}
output[i] = stc;
}
}
finally
{
if (rentedMacd != null)
ArrayPool<double>.Shared.Return(rentedMacd);
if (rentedStoch1 != null)
ArrayPool<double>.Shared.Return(rentedStoch1);
}
}
}