Files

443 lines
13 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ULTOSC: Ultimate Oscillator
/// </summary>
/// <remarks>
/// The Ultimate Oscillator, developed by Larry Williams in 1976, is a momentum oscillator
/// that uses weighted averages of three different time periods to reduce volatility and
/// false signals inherent in single-period oscillators.
///
/// Calculation:
/// 1. Buying Pressure (BP) = Close - True Low
/// True Low = Min(Low, Previous Close)
/// 2. True Range (TR) = True High - True Low
/// True High = Max(High, Previous Close)
/// 3. Average for each period = Sum(BP) / Sum(TR)
/// 4. Ultimate Oscillator = 100 * (4*Avg7 + 2*Avg14 + Avg28) / (4 + 2 + 1)
///
/// Key Features:
/// - Three time frames reduce false signals
/// - Buying pressure concept measures demand
/// - Weighted average gives priority to shorter-term movements
///
/// Sources:
/// - Larry Williams, "The Ultimate Oscillator" (1985 Stocks & Commodities)
/// - https://www.investopedia.com/terms/u/ultimateoscillator.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ultosc : AbstractBase
{
private readonly int _period1;
private readonly int _period2;
private readonly int _period3;
private readonly RingBuffer _bp1;
private readonly RingBuffer _bp2;
private readonly RingBuffer _bp3;
private readonly RingBuffer _tr1;
private readonly RingBuffer _tr2;
private readonly RingBuffer _tr3;
private double _prevClose;
private double _p_prevClose;
private int _index;
private int _p_index;
private readonly TBarSeries? _source;
private readonly TBarPublishedHandler? _handler;
private bool _disposed;
// Weights: 4:2:1
private const double Weight1 = 4.0;
private const double Weight2 = 2.0;
private const double Weight3 = 1.0;
private const double WeightSum = Weight1 + Weight2 + Weight3; // 7.0
public override bool IsHot => _index >= _period3;
/// <summary>
/// Creates Ultimate Oscillator with specified periods.
/// </summary>
/// <param name="period1">Short period (default: 7)</param>
/// <param name="period2">Intermediate period (default: 14)</param>
/// <param name="period3">Long period (default: 28)</param>
public Ultosc(int period1 = 7, int period2 = 14, int period3 = 28)
{
if (period1 <= 0)
{
throw new ArgumentException("Period1 must be greater than 0", nameof(period1));
}
if (period2 <= 0)
{
throw new ArgumentException("Period2 must be greater than 0", nameof(period2));
}
if (period3 <= 0)
{
throw new ArgumentException("Period3 must be greater than 0", nameof(period3));
}
if (period1 >= period2)
{
throw new ArgumentException("Period1 must be less than Period2", nameof(period1));
}
if (period2 >= period3)
{
throw new ArgumentException("Period2 must be less than Period3", nameof(period2));
}
_period1 = period1;
_period2 = period2;
_period3 = period3;
_bp1 = new RingBuffer(period1);
_bp2 = new RingBuffer(period2);
_bp3 = new RingBuffer(period3);
_tr1 = new RingBuffer(period1);
_tr2 = new RingBuffer(period2);
_tr3 = new RingBuffer(period3);
_prevClose = double.NaN;
_p_prevClose = double.NaN;
_index = 0;
_p_index = 0;
Name = $"Ultosc({period1},{period2},{period3})";
WarmupPeriod = period3;
}
/// <summary>
/// Creates Ultimate Oscillator with source subscription and specified periods.
/// </summary>
public Ultosc(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28) : this(period1, period2, period3)
{
_source = source;
_handler = Handle;
source.Pub += _handler;
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && _source != null && _handler != null)
{
_source.Pub -= _handler;
}
_disposed = true;
}
base.Dispose(disposing);
}
private void Handle(object? sender, in TBarEventArgs args)
{
Update(args.Value, args.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_prevClose = _prevClose;
_p_index = _index;
}
else
{
_prevClose = _p_prevClose;
_index = _p_index;
}
double high = input.High;
double low = input.Low;
double close = input.Close;
// Handle invalid inputs
if (!double.IsFinite(high) || !double.IsFinite(low) || !double.IsFinite(close))
{
Last = new TValue(input.Time, Last.Value);
PubEvent(Last, isNew);
return Last;
}
double bp, tr;
if (double.IsNaN(_prevClose))
{
// First bar: True Range = High - Low, BP = Close - Low
bp = close - low;
tr = high - low;
}
else
{
// True Low = Min(Low, Previous Close)
double trueLow = Math.Min(low, _prevClose);
// True High = Max(High, Previous Close)
double trueHigh = Math.Max(high, _prevClose);
// Buying Pressure = Close - True Low
bp = close - trueLow;
// True Range = True High - True Low
tr = trueHigh - trueLow;
}
// Add to all three period buffers
_bp1.Add(bp, isNew);
_bp2.Add(bp, isNew);
_bp3.Add(bp, isNew);
_tr1.Add(tr, isNew);
_tr2.Add(tr, isNew);
_tr3.Add(tr, isNew);
if (isNew)
{
_prevClose = close;
_index++;
}
// Calculate sums
double bpSum1 = _bp1.Sum;
double bpSum2 = _bp2.Sum;
double bpSum3 = _bp3.Sum;
double trSum1 = _tr1.Sum;
double trSum2 = _tr2.Sum;
double trSum3 = _tr3.Sum;
// Calculate averages (handle division by zero)
const double epsilon = 1e-10;
double avg1 = trSum1 > epsilon ? bpSum1 / trSum1 : 0.5;
double avg2 = trSum2 > epsilon ? bpSum2 / trSum2 : 0.5;
double avg3 = trSum3 > epsilon ? bpSum3 / trSum3 : 0.5;
// Ultimate Oscillator = 100 * (4*Avg1 + 2*Avg2 + Avg3) / 7
double ultosc = 100.0 * Math.FusedMultiplyAdd(Weight1, avg1, Math.FusedMultiplyAdd(Weight2, avg2, Weight3 * avg3)) / WeightSum;
Last = new TValue(input.Time, ultosc);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Update for TValue input - not recommended for Ultimate Oscillator as it needs OHLC.
/// This method will return 50 (neutral) since proper calculation requires OHLC data.
/// </summary>
public override TValue Update(TValue input, bool isNew = true)
{
// Ultimate Oscillator requires OHLC data
// Return neutral value if called with TValue
Last = new TValue(input.Time, 50.0);
PubEvent(Last, isNew);
return Last;
}
public TSeries Update(TBarSeries 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);
// Calculate using span method
Batch(source.High.Values, source.Low.Values, source.Close.Values,
vSpan, _period1, _period2, _period3);
source.Times.CopyTo(tSpan);
// Restore state for streaming
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i]);
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public override TSeries Update(TSeries source)
{
// Cannot properly calculate Ultimate Oscillator from single-value series
// Return series of neutral values
if (source.Count == 0)
{
return [];
}
var t = new List<long>(source.Count);
var v = new List<double>(source.Count);
for (int i = 0; i < source.Count; i++)
{
t.Add(source.Times[i]);
v.Add(50.0);
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
// Cannot properly prime Ultimate Oscillator from single-value array
// This method is a no-op for OHLC indicators
}
public static TSeries Batch(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28)
{
var ultosc = new Ultosc(period1, period2, period3);
return ultosc.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> output,
int period1 = 7,
int period2 = 14,
int period3 = 28)
{
int len = high.Length;
if (len != low.Length || len != close.Length || len != output.Length)
{
throw new ArgumentException("All arrays must have the same length", nameof(output));
}
if (period1 <= 0)
{
throw new ArgumentException("Period1 must be greater than 0", nameof(period1));
}
if (period2 <= 0)
{
throw new ArgumentException("Period2 must be greater than 0", nameof(period2));
}
if (period3 <= 0)
{
throw new ArgumentException("Period3 must be greater than 0", nameof(period3));
}
if (period1 >= period2)
{
throw new ArgumentException("Period1 must be less than Period2", nameof(period1));
}
if (period2 >= period3)
{
throw new ArgumentException("Period2 must be less than Period3", nameof(period2));
}
if (len == 0)
{
return;
}
// Allocate buffers for BP and TR
double[] bpArray = System.Buffers.ArrayPool<double>.Shared.Rent(len);
double[] trArray = System.Buffers.ArrayPool<double>.Shared.Rent(len);
try
{
Span<double> bp = bpArray.AsSpan(0, len);
Span<double> tr = trArray.AsSpan(0, len);
// First bar
bp[0] = close[0] - low[0];
tr[0] = high[0] - low[0];
// Calculate BP and TR for remaining bars
for (int i = 1; i < len; i++)
{
double h = high[i];
double l = low[i];
double c = close[i];
double prevC = close[i - 1];
double trueLow = Math.Min(l, prevC);
double trueHigh = Math.Max(h, prevC);
bp[i] = c - trueLow;
tr[i] = trueHigh - trueLow;
}
// Calculate running sums and output
double bpSum1 = 0, bpSum2 = 0, bpSum3 = 0;
double trSum1 = 0, trSum2 = 0, trSum3 = 0;
const double epsilon = 1e-10;
for (int i = 0; i < len; i++)
{
// Add current values
bpSum1 += bp[i];
bpSum2 += bp[i];
bpSum3 += bp[i];
trSum1 += tr[i];
trSum2 += tr[i];
trSum3 += tr[i];
// Remove old values for each period window
if (i >= period1)
{
bpSum1 -= bp[i - period1];
trSum1 -= tr[i - period1];
}
if (i >= period2)
{
bpSum2 -= bp[i - period2];
trSum2 -= tr[i - period2];
}
if (i >= period3)
{
bpSum3 -= bp[i - period3];
trSum3 -= tr[i - period3];
}
// Calculate averages
double avg1 = trSum1 > epsilon ? bpSum1 / trSum1 : 0.5;
double avg2 = trSum2 > epsilon ? bpSum2 / trSum2 : 0.5;
double avg3 = trSum3 > epsilon ? bpSum3 / trSum3 : 0.5;
// Ultimate Oscillator
output[i] = 100.0 * Math.FusedMultiplyAdd(Weight1, avg1, Math.FusedMultiplyAdd(Weight2, avg2, Weight3 * avg3)) / WeightSum;
}
}
finally
{
System.Buffers.ArrayPool<double>.Shared.Return(bpArray);
System.Buffers.ArrayPool<double>.Shared.Return(trArray);
}
}
public static (TSeries Results, Ultosc Indicator) Calculate(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28)
{
var indicator = new Ultosc(period1, period2, period3);
TSeries results = indicator.Update(source);
return (results, indicator);
}
public override void Reset()
{
_bp1.Clear();
_bp2.Clear();
_bp3.Clear();
_tr1.Clear();
_tr2.Clear();
_tr3.Clear();
_prevClose = double.NaN;
_p_prevClose = double.NaN;
_index = 0;
_p_index = 0;
Last = default;
}
}