mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 03:07:43 +00:00
365 lines
10 KiB
C#
365 lines
10 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// USI: Ehlers Ultimate Strength Index
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A symmetric RSI replacement that uses the UltimateSmoother filter instead of
|
|
/// Wilder's exponential smoothing. Output ranges from -1 to +1 with significantly
|
|
/// reduced lag compared to traditional RSI.
|
|
///
|
|
/// Pipeline:
|
|
/// <list type="number">
|
|
/// <item>SU = max(0, Close - Close[1]), SD = max(0, Close[1] - Close)</item>
|
|
/// <item>avgSU = SMA(SU, 4), avgSD = SMA(SD, 4)</item>
|
|
/// <item>USU = UltimateSmoother(avgSU, period), USD = UltimateSmoother(avgSD, period)</item>
|
|
/// <item>USI = (USU - USD) / (USU + USD) when denom > 0</item>
|
|
/// </list>
|
|
///
|
|
/// Reference: John F. Ehlers, "Ultimate Strength Index (USI)",
|
|
/// Technical Analysis of Stocks & Commodities, November 2024.
|
|
/// </remarks>
|
|
/// <seealso href="Usi.md">Detailed documentation</seealso>
|
|
/// <seealso href="usi.pine">Reference Pine Script implementation</seealso>
|
|
[SkipLocalsInit]
|
|
public sealed class Usi : AbstractBase
|
|
{
|
|
private const int SmaLen = 4;
|
|
private const double MinSmoothed = 0.01;
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(
|
|
double PrevClose,
|
|
// SMA(4) circular buffers
|
|
double Su0, double Su1, double Su2, double Su3,
|
|
double Sd0, double Sd1, double Sd2, double Sd3,
|
|
int BufIdx,
|
|
// UltimateSmoother IIR state for SU path
|
|
double Usu1, double Usu2,
|
|
double AvgSu1, double AvgSu2,
|
|
// UltimateSmoother IIR state for SD path
|
|
double Usd1, double Usd2,
|
|
double AvgSd1, double AvgSd2,
|
|
// Output
|
|
double Usi,
|
|
int Count, double LastValid)
|
|
{
|
|
public static State New() => new()
|
|
{
|
|
PrevClose = double.NaN,
|
|
Su0 = 0, Su1 = 0, Su2 = 0, Su3 = 0,
|
|
Sd0 = 0, Sd1 = 0, Sd2 = 0, Sd3 = 0,
|
|
BufIdx = 0,
|
|
Usu1 = 0, Usu2 = 0, AvgSu1 = 0, AvgSu2 = 0,
|
|
Usd1 = 0, Usd2 = 0, AvgSd1 = 0, AvgSd2 = 0,
|
|
Usi = 0,
|
|
Count = 0, LastValid = 0
|
|
};
|
|
}
|
|
|
|
// USF precomputed coefficients
|
|
private readonly double _k0; // (1 - c1)
|
|
private readonly double _k1; // (2*c1 - c2)
|
|
private readonly double _k2; // -(c1 + c3)
|
|
private readonly double _c2;
|
|
private readonly double _c3;
|
|
|
|
private State _s = State.New();
|
|
private State _ps = State.New();
|
|
|
|
/// <summary>
|
|
/// Creates USI with specified period.
|
|
/// </summary>
|
|
/// <param name="period">UltimateSmoother filter period (must be > 0, default 28)</param>
|
|
public Usi(int period = 28)
|
|
{
|
|
if (period <= 0)
|
|
{
|
|
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
|
}
|
|
|
|
// UltimateSmoother coefficients (same as USF)
|
|
double arg = Math.Sqrt(2) * Math.PI / period;
|
|
double expArg = Math.Exp(-arg);
|
|
_c2 = 2.0 * expArg * Math.Cos(arg);
|
|
_c3 = -(expArg * expArg);
|
|
double c1 = (1.0 + _c2 - _c3) / 4.0;
|
|
|
|
_k0 = 1.0 - c1;
|
|
_k1 = 2.0 * c1 - _c2;
|
|
_k2 = -(c1 + _c3);
|
|
|
|
Name = $"Usi({period})";
|
|
WarmupPeriod = period + SmaLen;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates USI with specified source and period.
|
|
/// </summary>
|
|
public Usi(ITValuePublisher source, int period = 28) : this(period)
|
|
{
|
|
source.Pub += Handle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates USI with a TSeries source, primes from history, then subscribes.
|
|
/// </summary>
|
|
public Usi(TSeries source, int period = 28) : this(period)
|
|
{
|
|
Prime(source.Values);
|
|
if (source.Count > 0)
|
|
{
|
|
Last = new TValue(source.LastTime, Last.Value);
|
|
}
|
|
source.Pub += Handle;
|
|
}
|
|
|
|
public override bool IsHot => _s.Count >= WarmupPeriod;
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
if (source.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_s = State.New();
|
|
_ps = State.New();
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double val = source[i];
|
|
if (double.IsFinite(val))
|
|
{
|
|
_s.LastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = _s.LastValid;
|
|
}
|
|
|
|
Step(val);
|
|
}
|
|
|
|
Last = new TValue(DateTime.MinValue, _s.Usi);
|
|
_ps = _s;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
if (isNew)
|
|
{
|
|
_ps = _s;
|
|
}
|
|
else
|
|
{
|
|
_s = _ps;
|
|
}
|
|
|
|
double val = input.Value;
|
|
if (double.IsFinite(val))
|
|
{
|
|
_s.LastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = _s.LastValid;
|
|
}
|
|
|
|
Step(val);
|
|
|
|
Last = new TValue(input.Time, _s.Usi);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
int len = source.Count;
|
|
var t = new List<long>(len);
|
|
var v = new List<double>(len);
|
|
CollectionsMarshal.SetCount(t, len);
|
|
CollectionsMarshal.SetCount(v, len);
|
|
|
|
var tSpan = CollectionsMarshal.AsSpan(t);
|
|
var vSpan = CollectionsMarshal.AsSpan(v);
|
|
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
Reset();
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double val = source.Values[i];
|
|
if (double.IsFinite(val))
|
|
{
|
|
_s.LastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = _s.LastValid;
|
|
}
|
|
|
|
Step(val);
|
|
vSpan[i] = _s.Usi;
|
|
}
|
|
|
|
_ps = _s;
|
|
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
|
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Core streaming step: SU/SD → SMA(4) → UltimateSmoother → normalize.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private void Step(double close)
|
|
{
|
|
_s.Count++;
|
|
|
|
// First bar: seed PrevClose, no SU/SD yet
|
|
if (double.IsNaN(_s.PrevClose))
|
|
{
|
|
_s.PrevClose = close;
|
|
return;
|
|
}
|
|
|
|
// --- Strength Up / Strength Down ---
|
|
double diff = close - _s.PrevClose;
|
|
double su = diff > 0 ? diff : 0.0;
|
|
double sd = diff < 0 ? -diff : 0.0;
|
|
_s.PrevClose = close;
|
|
|
|
// --- SMA(4) circular buffer for SU ---
|
|
int idx = _s.BufIdx & 3; // idx mod 4
|
|
switch (idx)
|
|
{
|
|
case 0: _s.Su0 = su; _s.Sd0 = sd; break;
|
|
case 1: _s.Su1 = su; _s.Sd1 = sd; break;
|
|
case 2: _s.Su2 = su; _s.Sd2 = sd; break;
|
|
case 3: _s.Su3 = su; _s.Sd3 = sd; break;
|
|
}
|
|
_s.BufIdx++;
|
|
|
|
double avgSu = (_s.Su0 + _s.Su1 + _s.Su2 + _s.Su3) * 0.25;
|
|
double avgSd = (_s.Sd0 + _s.Sd1 + _s.Sd2 + _s.Sd3) * 0.25;
|
|
|
|
// --- UltimateSmoother for USU and USD ---
|
|
double usu, usd;
|
|
if (_s.Count < 8)
|
|
{
|
|
// Bootstrap: pass-through before IIR has enough history
|
|
usu = avgSu;
|
|
usd = avgSd;
|
|
}
|
|
else
|
|
{
|
|
// USF IIR: k0*avg + k1*avg[1] + k2*avg[2] + c2*USF[1] + c3*USF[2]
|
|
usu = Math.FusedMultiplyAdd(_c3, _s.Usu2,
|
|
Math.FusedMultiplyAdd(_c2, _s.Usu1,
|
|
Math.FusedMultiplyAdd(_k2, _s.AvgSu2,
|
|
Math.FusedMultiplyAdd(_k1, _s.AvgSu1, _k0 * avgSu))));
|
|
|
|
usd = Math.FusedMultiplyAdd(_c3, _s.Usd2,
|
|
Math.FusedMultiplyAdd(_c2, _s.Usd1,
|
|
Math.FusedMultiplyAdd(_k2, _s.AvgSd2,
|
|
Math.FusedMultiplyAdd(_k1, _s.AvgSd1, _k0 * avgSd))));
|
|
}
|
|
|
|
// Shift IIR state
|
|
_s.Usu2 = _s.Usu1;
|
|
_s.Usu1 = usu;
|
|
_s.AvgSu2 = _s.AvgSu1;
|
|
_s.AvgSu1 = avgSu;
|
|
|
|
_s.Usd2 = _s.Usd1;
|
|
_s.Usd1 = usd;
|
|
_s.AvgSd2 = _s.AvgSd1;
|
|
_s.AvgSd1 = avgSd;
|
|
|
|
// --- Normalization ---
|
|
double denom = usu + usd;
|
|
if (denom > MinSmoothed)
|
|
{
|
|
_s.Usi = Math.Clamp((usu - usd) / denom, -1.0, 1.0);
|
|
}
|
|
// else: keep previous _s.Usi value (denominator near zero = flat market)
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch calculation returning a TSeries.
|
|
/// </summary>
|
|
public static TSeries Batch(TSeries source, int period = 28)
|
|
{
|
|
var indicator = new Usi(period);
|
|
return indicator.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch calculation writing to a pre-allocated output span. Zero-allocation hot path.
|
|
/// </summary>
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 28)
|
|
{
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
|
}
|
|
if (period <= 0)
|
|
{
|
|
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
|
}
|
|
|
|
if (source.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var indicator = new Usi(period);
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double val = source[i];
|
|
if (double.IsFinite(val))
|
|
{
|
|
indicator._s.LastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = indicator._s.LastValid;
|
|
}
|
|
|
|
indicator.Step(val);
|
|
output[i] = indicator._s.Usi;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a hot indicator from historical data, ready for streaming.
|
|
/// </summary>
|
|
public static (TSeries Results, Usi Indicator) Calculate(TSeries source, int period = 28)
|
|
{
|
|
var indicator = new Usi(period);
|
|
TSeries results = indicator.Update(source);
|
|
return (results, indicator);
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_s = State.New();
|
|
_ps = _s;
|
|
Last = default;
|
|
}
|
|
}
|