mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,504 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class UltoscTests
|
||||
{
|
||||
// ============== Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod1_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(0, 14, 28));
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(-1, 14, 28));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod2_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, 0, 28));
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, -1, 28));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod3_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, 14, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, 14, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1NotLessThanPeriod2_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(14, 14, 28));
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(15, 14, 28));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period2NotLessThanPeriod3_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, 28, 28));
|
||||
Assert.Throws<ArgumentException>(() => new Ultosc(7, 29, 28));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidParameters_Succeeds()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
Assert.NotNull(ultosc);
|
||||
|
||||
var ultosc2 = new Ultosc(5, 10, 20);
|
||||
Assert.NotNull(ultosc2);
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ultosc.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
Assert.Equal(0, ultosc.Last.Value);
|
||||
|
||||
TValue result = ultosc.Update(bar);
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, ultosc.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsValidOscillator()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
// First bar: BP = Close - Low = 105 - 90 = 15
|
||||
// TR = High - Low = 110 - 90 = 20
|
||||
// Avg = BP/TR = 15/20 = 0.75 for all periods
|
||||
// UO = 100 * (4*0.75 + 2*0.75 + 0.75) / 7 = 100 * 5.25/7 = 75
|
||||
|
||||
TValue result = ultosc.Update(bar);
|
||||
|
||||
Assert.Equal(75.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
|
||||
Assert.Equal(0, ultosc.Last.Value);
|
||||
Assert.False(ultosc.IsHot);
|
||||
Assert.Contains("Ultosc", ultosc.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(28, ultosc.WarmupPeriod);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
ultosc.Update(bar);
|
||||
|
||||
Assert.NotEqual(0, ultosc.Last.Value);
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
ultosc.Update(bar1, isNew: true);
|
||||
double value1 = ultosc.Last.Value;
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
|
||||
ultosc.Update(bar2, isNew: true);
|
||||
double value2 = ultosc.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
ultosc.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
|
||||
ultosc.Update(bar2, isNew: true);
|
||||
double beforeUpdate = ultosc.Last.Value;
|
||||
|
||||
var bar2Modified = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 120, 90, 108, 1000);
|
||||
ultosc.Update(bar2Modified, isNew: false);
|
||||
double afterUpdate = ultosc.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
ultosc.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
ultosc.Update(bars[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume);
|
||||
double val2 = ultosc.Update(modifiedBar, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var ultosc2 = new Ultosc(7, 14, 28);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
ultosc2.Update(bars[i]);
|
||||
}
|
||||
double val3 = ultosc2.Update(modifiedBar, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed 10 new values
|
||||
TBar tenthBar = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthBar = bars[i];
|
||||
ultosc.Update(tenthBar, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = ultosc.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
ultosc.Update(bars[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th bar again with isNew=false
|
||||
TValue finalResult = ultosc.Update(tenthBar, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars) ultosc.Update(bar);
|
||||
|
||||
double lastVal = ultosc.Last.Value;
|
||||
Assert.NotEqual(0, lastVal);
|
||||
|
||||
ultosc.Reset();
|
||||
Assert.Equal(0, ultosc.Last.Value);
|
||||
Assert.False(ultosc.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
ultosc.Update(bars[0]);
|
||||
Assert.NotEqual(0, ultosc.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
Assert.False(ultosc.IsHot);
|
||||
|
||||
int steps = 0;
|
||||
var baseTime = DateTime.UtcNow;
|
||||
while (!ultosc.IsHot && steps < 100)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(steps), 100, 110, 90, 100, 1000);
|
||||
ultosc.Update(bar);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(ultosc.IsHot);
|
||||
Assert.True(steps > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
Assert.True(ultosc.WarmupPeriod > 0);
|
||||
Assert.Equal(28, ultosc.WarmupPeriod);
|
||||
|
||||
var ultosc2 = new Ultosc(5, 10, 20);
|
||||
Assert.Equal(20, ultosc2.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
ultosc.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
ultosc.Update(bar2);
|
||||
|
||||
// Feed bar with NaN values
|
||||
var barWithNaN = new TBar(DateTime.UtcNow.AddMinutes(2), double.NaN, 115, 100, 112, 1000);
|
||||
var resultAfterNaN = ultosc.Update(barWithNaN);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
ultosc.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
ultosc.Update(bar2);
|
||||
|
||||
// Feed bar with Infinity
|
||||
var barWithInf = new TBar(DateTime.UtcNow.AddMinutes(2), 108, double.PositiveInfinity, 100, 112, 1000);
|
||||
var resultAfterInf = ultosc.Update(barWithInf);
|
||||
|
||||
// Result should be finite or infinity (depending on implementation)
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value) || double.IsPositiveInfinity(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var ultoscIterative = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
iterativeResults.Add(ultoscIterative.Update(bar));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Ultosc.Batch(bars, 7, 14, 28);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var ultosc1 = new Ultosc(7, 14, 28);
|
||||
var ultosc2 = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ultosc1.Update(bar);
|
||||
}
|
||||
|
||||
// Batch
|
||||
ultosc2.Update(bars);
|
||||
|
||||
Assert.Equal(ultosc1.Last.Value, ultosc2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var result = ultosc.Update(bars);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(ultosc.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Oscillator Range Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Oscillator_ReturnsValueBetween0And100()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = ultosc.Update(bar);
|
||||
Assert.InRange(result.Value, 0.0, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrongUptrend_ReturnsHighValues()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Create strong uptrend bars where Close is always at High
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + (i * 5); // Rising prices
|
||||
var bar = new TBar(baseTime.AddMinutes(i), basePrice, basePrice + 10, basePrice - 2, basePrice + 10, 1000);
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
// In strong uptrend with Close at High, BP/TR should be high
|
||||
Assert.True(ultosc.Last.Value > 50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StrongDowntrend_ReturnsLowValues()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Create strong downtrend bars where Close is always at Low
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 200 - (i * 5); // Falling prices
|
||||
var bar = new TBar(baseTime.AddMinutes(i), basePrice, basePrice + 2, basePrice - 10, basePrice - 10, 1000);
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
// In strong downtrend with Close at Low, BP/TR should be low
|
||||
Assert.True(ultosc.Last.Value < 50);
|
||||
}
|
||||
|
||||
// ============== Static Batch Method ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var results = Ultosc.Batch(bars, 7, 14, 28);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleBar_ReturnsValidResult()
|
||||
{
|
||||
var ultosc = new Ultosc(7, 14, 28);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
|
||||
var result = ultosc.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
// BP = Close - Low = 105 - 90 = 15
|
||||
// TR = High - Low = 110 - 90 = 20
|
||||
// Avg = 15/20 = 0.75
|
||||
// UO = 100 * (4*0.75 + 2*0.75 + 0.75) / 7 = 75
|
||||
Assert.Equal(75.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatBars_ReturnsFifty()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
// All bars have same OHLC values (flat market)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
// For flat bars: BP = 0, TR = 0, so BP/TR = 0/0 handled as 0.5
|
||||
// UO = 100 * 0.5 * 7 / 7 = 50
|
||||
Assert.Equal(50.0, ultosc.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseAtHigh_ReturnsHundred()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
// All bars have Close at High
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 110, 1000);
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
// BP = Close - TrueLow = 110 - 90 = 20
|
||||
// TR = TrueHigh - TrueLow = 110 - 90 = 20
|
||||
// Avg = 20/20 = 1.0
|
||||
// UO = 100 * (4*1 + 2*1 + 1) / 7 = 100
|
||||
Assert.Equal(100.0, ultosc.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseAtLow_ReturnsZero()
|
||||
{
|
||||
var ultosc = new Ultosc(3, 5, 7);
|
||||
|
||||
// All bars have Close at Low
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 90, 1000);
|
||||
ultosc.Update(bar);
|
||||
}
|
||||
|
||||
// BP = Close - TrueLow = 90 - 90 = 0
|
||||
// TR = TrueHigh - TrueLow = 110 - 90 = 20
|
||||
// Avg = 0/20 = 0.0
|
||||
// UO = 100 * (4*0 + 2*0 + 0) / 7 = 0
|
||||
Assert.Equal(0.0, ultosc.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class UltoscValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public UltoscValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (batch TBarSeries)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResult = ultosc.Update(_testData.Bars);
|
||||
|
||||
// Calculate Skender Ultimate Oscillator
|
||||
var sResult = _testData.SkenderQuotes.GetUltimate(p1, p2, p3).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Ultimate, tolerance: ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (streaming)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(ultosc.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Skender Ultimate Oscillator
|
||||
var sResult = _testData.SkenderQuotes.GetUltimate(p1, p2, p3).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Ultimate, tolerance: ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Streaming validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Batch()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] output = new double[hData.Length];
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (batch TBarSeries)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResult = ultosc.Update(_testData.Bars);
|
||||
|
||||
// Calculate TA-Lib UltOsc
|
||||
var retCode = TALib.Functions.UltOsc(hData, lData, cData, 0..^0, output, out var outRange, p1, p2, p3);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.UltOscLookback(p1, p2, p3);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, output, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Streaming()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] output = new double[hData.Length];
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (streaming)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(ultosc.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate TA-Lib UltOsc
|
||||
var retCode = TALib.Functions.UltOsc(hData, lData, cData, 0..^0, output, out var outRange, p1, p2, p3);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.UltOscLookback(p1, p2, p3);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, output, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Streaming validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Batch()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (batch TBarSeries)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResult = ultosc.Update(_testData.Bars);
|
||||
|
||||
// Calculate Tulip UltOsc
|
||||
var ultoscIndicator = Tulip.Indicators.ultosc;
|
||||
double[][] inputs = { hData, lData, cData };
|
||||
double[] options = { p1, p2, p3 };
|
||||
|
||||
// Tulip UltOsc lookback
|
||||
int lookback = ultoscIndicator.Start(options);
|
||||
double[][] outputs = { new double[hData.Length - lookback] };
|
||||
|
||||
ultoscIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, tResult, lookback, tolerance: ValidationHelper.TulipTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Streaming()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (streaming)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(ultosc.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Tulip UltOsc
|
||||
var ultoscIndicator = Tulip.Indicators.ultosc;
|
||||
double[][] inputs = { hData, lData, cData };
|
||||
double[] options = { p1, p2, p3 };
|
||||
|
||||
// Tulip UltOsc lookback
|
||||
int lookback = ultoscIndicator.Start(options);
|
||||
double[][] outputs = { new double[hData.Length - lookback] };
|
||||
|
||||
ultoscIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, tResult, lookback, tolerance: ValidationHelper.TulipTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Streaming validated successfully against Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_Batch()
|
||||
{
|
||||
int[][] periodSets = { [7, 14, 28] };
|
||||
|
||||
// Prepare data for Ooples (List<TickerData>)
|
||||
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Close = (double)q.Close,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Open = (double)q.Open,
|
||||
Volume = (double)q.Volume
|
||||
}).ToList();
|
||||
|
||||
foreach (var periods in periodSets)
|
||||
{
|
||||
int p1 = periods[0];
|
||||
int p2 = periods[1];
|
||||
int p3 = periods[2];
|
||||
|
||||
// Calculate QuanTAlib Ultosc (batch TBarSeries)
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var qResult = ultosc.Update(_testData.Bars);
|
||||
|
||||
// Calculate Ooples Ultimate Oscillator
|
||||
var stockData = new StockData(ooplesData);
|
||||
var sResult = stockData.CalculateUltimateOscillator(p1, p2, p3).OutputValues.Values.First();
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, sResult, (s) => s, 100, ValidationHelper.OoplesTolerance);
|
||||
}
|
||||
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Ooples");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Span_MatchesTBarSeries()
|
||||
{
|
||||
const int p1 = 7;
|
||||
int p2 = 14;
|
||||
int p3 = 28;
|
||||
|
||||
// Prepare data
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] spanOutput = new double[hData.Length];
|
||||
|
||||
// Calculate using span method
|
||||
Ultosc.Calculate(hData, lData, cData, spanOutput, p1, p2, p3);
|
||||
|
||||
// Calculate using TBarSeries batch
|
||||
var ultosc = new Ultosc(p1, p2, p3);
|
||||
var tbarResult = ultosc.Update(_testData.Bars);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < tbarResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(tbarResult[i].Value, spanOutput[i], 1e-10);
|
||||
}
|
||||
_output.WriteLine("Ultosc Span calculation matches TBarSeries batch calculation");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
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;
|
||||
|
||||
// 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 (disposing && _source != null && _handler != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
}
|
||||
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
|
||||
Calculate(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 Calculate(
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# UltOsc: Ultimate Oscillator
|
||||
|
||||
> "Why use one timeframe when three can save you from yourself?"
|
||||
|
||||
The Ultimate Oscillator is Larry Williams' answer to the fundamental flaw of single-period momentum oscillators: they whipsaw. By combining buying pressure across three distinct timeframes with a weighted average, UltOsc filters out the noise that traps traders who rely on RSI or Stochastics alone.
|
||||
|
||||
The indicator oscillates between 0 and 100. Readings above 70 suggest overbought conditions; readings below 30 suggest oversold. But the real power lies in **divergence detection**: when price makes a new high but UltOsc does not, the trend is exhausted.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Larry Williams introduced the Ultimate Oscillator in his 1985 article for *Technical Analysis of Stocks & Commodities* magazine. Williams, a legendary trader who famously turned \$10,000 into over \$1 million in a single year of trading, designed UltOsc to solve a specific problem.
|
||||
|
||||
Single-period oscillators like RSI suffer from two fatal flaws:
|
||||
|
||||
1. **False signals during trends**: In a strong uptrend, RSI can stay overbought for weeks, generating endless "sell" signals.
|
||||
2. **Period sensitivity**: A 7-period RSI behaves differently from a 14-period RSI. Which one is "right"?
|
||||
|
||||
Williams' solution was elegant: use three periods (7, 14, 28) and weight them so the shortest period has the most influence (4:2:1). This gives responsiveness to recent price action while still respecting the broader context.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
UltOsc is built on two core concepts: **Buying Pressure (BP)** and **True Range (TR)**.
|
||||
|
||||
### Buying Pressure
|
||||
|
||||
Buying Pressure measures how much of today's price movement was "bought." It is the distance from the True Low (the lower of today's Low or yesterday's Close) to today's Close.
|
||||
|
||||
$$
|
||||
BP = Close - TrueLow
|
||||
$$
|
||||
|
||||
If the close is at the high of the day, BP is maximized. If the close is at the low, BP is zero.
|
||||
|
||||
### True Range
|
||||
|
||||
True Range captures the full volatility of the day, including overnight gaps.
|
||||
|
||||
$$
|
||||
TR = TrueHigh - TrueLow
|
||||
$$
|
||||
|
||||
Where:
|
||||
|
||||
- $TrueHigh = \max(High, Close_{t-1})$
|
||||
- $TrueLow = \min(Low, Close_{t-1})$
|
||||
|
||||
### The Multi-Timeframe Fusion
|
||||
|
||||
For each of the three periods, UltOsc calculates the ratio of accumulated Buying Pressure to accumulated True Range:
|
||||
|
||||
$$
|
||||
Avg_n = \frac{\sum_{i=1}^{n} BP_i}{\sum_{i=1}^{n} TR_i}
|
||||
$$
|
||||
|
||||
This ratio represents the "efficiency" of buying over that period. A value of 1.0 means all volatility was captured by buyers; 0.0 means sellers dominated.
|
||||
|
||||
The final oscillator applies a 4:2:1 weighting:
|
||||
|
||||
$$
|
||||
UltOsc = 100 \times \frac{4 \times Avg_7 + 2 \times Avg_{14} + 1 \times Avg_{28}}{4 + 2 + 1}
|
||||
$$
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. True Low and True High
|
||||
|
||||
$$
|
||||
TrueLow_t = \min(Low_t, Close_{t-1})
|
||||
$$
|
||||
|
||||
$$
|
||||
TrueHigh_t = \max(High_t, Close_{t-1})
|
||||
$$
|
||||
|
||||
### 2. Buying Pressure and True Range
|
||||
|
||||
$$
|
||||
BP_t = Close_t - TrueLow_t
|
||||
$$
|
||||
|
||||
$$
|
||||
TR_t = TrueHigh_t - TrueLow_t
|
||||
$$
|
||||
|
||||
### 3. Period Averages
|
||||
|
||||
For periods $n_1 = 7$, $n_2 = 14$, $n_3 = 28$:
|
||||
|
||||
$$
|
||||
Avg_n = \frac{\sum_{i=t-n+1}^{t} BP_i}{\sum_{i=t-n+1}^{t} TR_i}
|
||||
$$
|
||||
|
||||
### 4. Ultimate Oscillator
|
||||
|
||||
$$
|
||||
UltOsc = 100 \times \frac{4 \cdot Avg_7 + 2 \cdot Avg_{14} + 1 \cdot Avg_{28}}{7}
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 8 | Moderate; requires six running sums (BP and TR for each period). |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths using ring buffers. |
|
||||
| **Complexity** | O(1) | Constant time via running sums. |
|
||||
| **Accuracy** | 10 | Matches TA-Lib and Skender exactly. |
|
||||
| **Timeliness** | 6 | Balanced; short-period weighting provides responsiveness. |
|
||||
| **Overshoot** | 2 | Bounded to [0, 100]; minimal overshoot by design. |
|
||||
| **Smoothness** | 7 | Multi-period averaging provides inherent smoothing. |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated. |
|
||||
| **TA-Lib** | ✅ | Matches `TA_ULTOSC` exactly. |
|
||||
| **Skender** | ✅ | Matches `GetUltimate` exactly. |
|
||||
| **Tulip** | ✅ | Matches `ultosc` exactly. |
|
||||
| **Ooples** | ⚠️ | Minor deviations in warmup period handling. |
|
||||
|
||||
### Trading Signals
|
||||
|
||||
Williams outlined specific rules for trading UltOsc:
|
||||
|
||||
1. **Bullish Divergence**: Price makes a lower low, UltOsc makes a higher low (UltOsc < 30).
|
||||
2. **Breakout Confirmation**: After divergence, UltOsc breaks above the divergence high.
|
||||
3. **Exit**: UltOsc reaches 70, or price hits target.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- **Ignoring Divergence**: UltOsc is designed for divergence trading. Using it as a simple overbought/oversold indicator misses the point.
|
||||
- **Wrong Timeframes**: The default 7/14/28 works for daily charts. For intraday, consider scaling down proportionally.
|
||||
- **Trending Markets**: Like all oscillators, UltOsc struggles in strong trends. Use trend filters (ADX, moving averages) to avoid fighting the tide.
|
||||
- **Division by Zero**: If True Range is zero (flat line), the ratio is undefined. QuanTAlib handles this by returning 0.5 (neutral).
|
||||
Reference in New Issue
Block a user