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:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+504
View File
@@ -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");
}
}
+389
View File
@@ -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;
}
}
+134
View File
@@ -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).