mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 21:48:03 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class CrsiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CrsiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CrsiIndicator();
|
||||
|
||||
Assert.Equal(3, indicator.RsiPeriod);
|
||||
Assert.Equal(2, indicator.StreakPeriod);
|
||||
Assert.Equal(100, indicator.RankPeriod);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CRSI - Connors RSI", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 3 };
|
||||
|
||||
Assert.Equal(0, CrsiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 5, StreakPeriod = 3, RankPeriod = 50 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("CRSI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("5", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("3", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("50", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new CrsiIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Crsi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 3, StreakPeriod = 2, RankPeriod = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 3, StreakPeriod = 2, RankPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.True(value >= 0.0 && value <= 100.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 3, StreakPeriod = 2, RankPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new CrsiIndicator();
|
||||
|
||||
indicator.RsiPeriod = 5;
|
||||
indicator.StreakPeriod = 3;
|
||||
indicator.RankPeriod = 50;
|
||||
indicator.Source = SourceType.Open;
|
||||
|
||||
Assert.Equal(5, indicator.RsiPeriod);
|
||||
Assert.Equal(3, indicator.StreakPeriod);
|
||||
Assert.Equal(50, indicator.RankPeriod);
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
Assert.Equal(0, CrsiIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrsiIndicator_DifferentSources_Work()
|
||||
{
|
||||
foreach (var source in new[] { SourceType.Close, SourceType.Open, SourceType.High, SourceType.Low })
|
||||
{
|
||||
var indicator = new CrsiIndicator { RsiPeriod = 3, StreakPeriod = 2, RankPeriod = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class CrsiTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
// ───── A) Constructor validation ─────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RsiPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(rsiPeriod: 0));
|
||||
Assert.Equal("rsiPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RsiPeriodNegative_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(rsiPeriod: -1));
|
||||
Assert.Equal("rsiPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_StreakPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(streakPeriod: 0));
|
||||
Assert.Equal("streakPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_StreakPeriodNegative_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(streakPeriod: -5));
|
||||
Assert.Equal("streakPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RankPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(rankPeriod: 0));
|
||||
Assert.Equal("rankPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RankPeriodNegative_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Crsi(rankPeriod: -10));
|
||||
Assert.Equal("rankPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidDefaults_SetsProperties()
|
||||
{
|
||||
var crsi = new Crsi();
|
||||
Assert.Equal(3, crsi.RsiPeriod);
|
||||
Assert.Equal(2, crsi.StreakPeriod);
|
||||
Assert.Equal(100, crsi.RankPeriod);
|
||||
Assert.Equal("Crsi(3,2,100)", crsi.Name);
|
||||
Assert.False(crsi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriods_SetsProperties()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 5, streakPeriod: 3, rankPeriod: 50);
|
||||
Assert.Equal(5, crsi.RsiPeriod);
|
||||
Assert.Equal(3, crsi.StreakPeriod);
|
||||
Assert.Equal(50, crsi.RankPeriod);
|
||||
Assert.Equal("Crsi(5,3,50)", crsi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_RsiPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[] { 1, 2, 3 };
|
||||
var out1 = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Crsi.Batch(src, out1, rsiPeriod: 0));
|
||||
Assert.Equal("rsiPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_StreakPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[] { 1, 2, 3 };
|
||||
var out1 = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Crsi.Batch(src, out1, streakPeriod: 0));
|
||||
Assert.Equal("streakPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_RankPeriodZero_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[] { 1, 2, 3 };
|
||||
var out1 = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Crsi.Batch(src, out1, rankPeriod: 0));
|
||||
Assert.Equal("rankPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_MismatchedLength_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[] { 1, 2, 3 };
|
||||
var out1 = new double[4];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Crsi.Batch(src, out1));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
// ───── B) Basic calculation ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var result = crsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_OutputInRange0To100()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.3, seed: 99);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars.Close)
|
||||
{
|
||||
var v = crsi.Update(bar).Value;
|
||||
Assert.True(v >= 0.0 && v <= 100.0, $"CRSI={v} out of [0,100]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NameAccessible()
|
||||
{
|
||||
var crsi = new Crsi(3, 2, 100);
|
||||
crsi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal("Crsi(3,2,100)", crsi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsHotFalseBeforeWarmup()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
crsi.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
Assert.False(crsi.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── C) State + bar correction ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var t = DateTime.UtcNow;
|
||||
crsi.Update(new TValue(t, 100.0), isNew: true);
|
||||
var v1 = crsi.Last;
|
||||
crsi.Update(new TValue(t.AddMinutes(1), 105.0), isNew: true);
|
||||
var v2 = crsi.Last;
|
||||
|
||||
// Two distinct bars — Last values can differ
|
||||
Assert.NotEqual(default, v1);
|
||||
Assert.NotEqual(default, v2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RollsBack()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
double[] prices = [100, 102, 104, 103, 105, 107];
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Correction — produce different value
|
||||
crsi.Update(new TValue(t.AddMinutes(prices.Length), 150.0), isNew: false);
|
||||
var corrected1 = crsi.Last.Value;
|
||||
|
||||
// Same correction again must produce same result (idempotent)
|
||||
crsi.Update(new TValue(t.AddMinutes(prices.Length), 150.0), isNew: false);
|
||||
var corrected2 = crsi.Last.Value;
|
||||
|
||||
Assert.Equal(corrected1, corrected2, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
double[] prices = [100, 102, 98, 105, 103, 107];
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
double baseline = crsi.Last.Value;
|
||||
|
||||
// Two bad corrections, then restore original
|
||||
crsi.Update(new TValue(t.AddMinutes(prices.Length), 999.0), isNew: false);
|
||||
crsi.Update(new TValue(t.AddMinutes(prices.Length), 888.0), isNew: false);
|
||||
crsi.Update(new TValue(t.AddMinutes(prices.Length), prices[^1]), isNew: false);
|
||||
|
||||
Assert.Equal(baseline, crsi.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(crsi.IsHot);
|
||||
crsi.Reset();
|
||||
Assert.False(crsi.IsHot);
|
||||
Assert.Equal(default, crsi.Last);
|
||||
}
|
||||
|
||||
// ───── D) Warmup / convergence ─────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterRankPeriodBars()
|
||||
{
|
||||
int rankPeriod = 5;
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: rankPeriod);
|
||||
var t = DateTime.UtcNow;
|
||||
|
||||
// rankPeriod-1 bars: still cold
|
||||
for (int i = 0; i < rankPeriod - 1; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), 100.0 + i));
|
||||
Assert.False(crsi.IsHot);
|
||||
}
|
||||
|
||||
// rankPeriod bar: hot
|
||||
crsi.Update(new TValue(t.AddMinutes(rankPeriod - 1), 100.0 + rankPeriod - 1));
|
||||
Assert.True(crsi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsAccessible()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 100);
|
||||
Assert.True(crsi.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
// ───── E) Robustness ─────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValid()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
crsi.Update(new TValue(t.AddMinutes(8), double.NaN));
|
||||
Assert.True(double.IsFinite(crsi.Last.Value));
|
||||
Assert.True(crsi.Last.Value >= 0.0 && crsi.Last.Value <= 100.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValid()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
crsi.Update(new TValue(t.AddMinutes(8), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(crsi.Last.Value));
|
||||
|
||||
crsi.Update(new TValue(t.AddMinutes(9), double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(crsi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var crsi = new Crsi(rsiPeriod: 3, streakPeriod: 2, rankPeriod: 5);
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
crsi.Update(new TValue(t.AddMinutes(i), double.NaN));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(crsi.Last.Value));
|
||||
}
|
||||
|
||||
// ───── F) Consistency (4 modes match) ─────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int rsiPeriod = 3;
|
||||
int streakPeriod = 2;
|
||||
int rankPeriod = 20;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 77);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
// 1. Streaming
|
||||
var streaming = new Crsi(rsiPeriod, streakPeriod, rankPeriod);
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// 2. Batch TSeries
|
||||
TSeries batchSeries = Crsi.Batch(source, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
// 3. Batch Span
|
||||
var spanOutput = new double[source.Count];
|
||||
Crsi.Batch(source.Values, spanOutput, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
// 4. Event-based
|
||||
var eventSource = new TSeries();
|
||||
var eventIndicator = new Crsi(eventSource, rsiPeriod, streakPeriod, rankPeriod);
|
||||
var eventResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventSource.Add(source[i]);
|
||||
eventResults[i] = eventIndicator.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchSeries.Values[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], spanOutput[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], eventResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── G) Span API tests ─────
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_EmptySource_DoesNotThrow()
|
||||
{
|
||||
var src = Array.Empty<double>();
|
||||
var out1 = Array.Empty<double>();
|
||||
// Should not throw and output remains empty
|
||||
Crsi.Batch(src, out1);
|
||||
Assert.Empty(out1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_OutputInRange0to100()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 55);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var src = bars.Close.Values;
|
||||
var out1 = new double[src.Length];
|
||||
|
||||
Crsi.Batch(src, out1, rsiPeriod: 3, streakPeriod: 2, rankPeriod: 20);
|
||||
|
||||
for (int i = 0; i < out1.Length; i++)
|
||||
{
|
||||
Assert.True(out1[i] >= 0.0 && out1[i] <= 100.0, $"Span output[{i}]={out1[i]} out of range");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_LargeData_NoStackOverflow()
|
||||
{
|
||||
int n = 10_000;
|
||||
var src = new double[n];
|
||||
var out1 = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
src[i] = 100.0 + i * 0.01;
|
||||
}
|
||||
|
||||
// rankPeriod > 256 to exercise ArrayPool path
|
||||
Crsi.Batch(src, out1, rsiPeriod: 3, streakPeriod: 2, rankPeriod: 500);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Assert.True(out1[i] >= 0.0 && out1[i] <= 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── H) Chainability ─────
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_PubFires()
|
||||
{
|
||||
int rsiPeriod = 3;
|
||||
int streakPeriod = 2;
|
||||
int rankPeriod = 5;
|
||||
var sourceTs = new TSeries();
|
||||
var crsi = new Crsi(sourceTs, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
int count = 0;
|
||||
crsi.Pub += (_, in _) => count++;
|
||||
|
||||
var t = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
sourceTs.Add(new TValue(t.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(10, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// CRSI validation:
|
||||
/// - Internal consistency (streaming/batch/span/eventing)
|
||||
/// - Native Skender GetConnorsRsi cross-validation (batch, streaming, span)
|
||||
/// - Native Ooples CalculateConnorsRelativeStrengthIndex cross-validation (batch, streaming, span)
|
||||
/// - External structural cross-validation via RSI components from Skender/TA-Lib/Tulip/Ooples
|
||||
/// </summary>
|
||||
public sealed class CrsiValidationTests(ITestOutputHelper output) : IDisposable
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_data.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch_DefaultParams()
|
||||
{
|
||||
var source = _data.Data;
|
||||
|
||||
var streaming = new Crsi(3, 2, 100);
|
||||
var streamVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamVals[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
TSeries batchTs = Crsi.Batch(source, 3, 2, 100);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], batchTs.Values[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_MatchesBatch_DefaultParams()
|
||||
{
|
||||
var source = _data.Data;
|
||||
|
||||
TSeries batchTs = Crsi.Batch(source, 3, 2, 100);
|
||||
|
||||
var spanOut = new double[source.Count];
|
||||
Crsi.Batch(source.Values, spanOut, 3, 2, 100);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs.Values[i], spanOut[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Eventing_MatchesStreaming()
|
||||
{
|
||||
var source = _data.Data;
|
||||
|
||||
var streaming = new Crsi(3, 2, 50);
|
||||
var streamVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamVals[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
var eventTs = new TSeries();
|
||||
var eventCrsi = new Crsi(eventTs, 3, 2, 50);
|
||||
var eventVals = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventTs.Add(source[i]);
|
||||
eventVals[i] = eventCrsi.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamVals[i], eventVals[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Output_AlwaysInRange0To100()
|
||||
{
|
||||
var source = _data.Data;
|
||||
var crsi = new Crsi(3, 2, 100);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
double v = crsi.Update(source[i]).Value;
|
||||
Assert.True(v >= 0.0 && v <= 100.0, $"CRSI={v} at i={i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ThenReplay_MatchesFreshRun()
|
||||
{
|
||||
var source = _data.Data;
|
||||
|
||||
var crsi1 = new Crsi(3, 2, 30);
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
crsi1.Update(source[i]);
|
||||
}
|
||||
|
||||
double finalVal1 = crsi1.Last.Value;
|
||||
|
||||
crsi1.Reset();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
crsi1.Update(source[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(finalVal1, crsi1.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDistinctResults()
|
||||
{
|
||||
var source = _data.Data;
|
||||
|
||||
TSeries r1 = Crsi.Batch(source, 3, 2, 50);
|
||||
TSeries r2 = Crsi.Batch(source, 5, 3, 50);
|
||||
|
||||
bool anyDiff = false;
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
if (Math.Abs(r1.Values[i] - r2.Values[i]) > 1e-6)
|
||||
{
|
||||
anyDiff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(anyDiff, "Different periods should produce different results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_StructuralComposite()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
double[] streak = ComputeStreak(close);
|
||||
double[] pctRank = ComputePercentRank(close, rankPeriod);
|
||||
|
||||
var closeRsi = _data.SkenderQuotes.GetRsi(rsiPeriod).Select(x => x.Rsi.HasValue ? x.Rsi.Value : double.NaN).ToArray();
|
||||
|
||||
var streakQuotes = BuildSyntheticQuotes(_data.SkenderQuotes, streak);
|
||||
var streakRsi = streakQuotes.GetRsi(streakPeriod).Select(x => x.Rsi.HasValue ? x.Rsi.Value : double.NaN).ToArray();
|
||||
|
||||
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
|
||||
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.SkenderTolerance);
|
||||
_output.WriteLine("CRSI validated against Skender structural composite (RSI + RSI(streak) + %Rank).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_StructuralComposite()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
double[] streak = ComputeStreak(close);
|
||||
double[] pctRank = ComputePercentRank(close, rankPeriod);
|
||||
|
||||
var closeRsi = ComputeTalibRsiFull(close, rsiPeriod);
|
||||
var streakRsi = ComputeTalibRsiFull(streak, streakPeriod);
|
||||
|
||||
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
|
||||
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.TalibTolerance);
|
||||
_output.WriteLine("CRSI validated against TA-Lib structural composite (RSI + RSI(streak) + %Rank).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_StructuralComposite()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
double[] streak = ComputeStreak(close);
|
||||
double[] pctRank = ComputePercentRank(close, rankPeriod);
|
||||
|
||||
var closeRsi = ComputeTulipRsiFull(close, rsiPeriod);
|
||||
var streakRsi = ComputeTulipRsiFull(streak, streakPeriod);
|
||||
|
||||
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
|
||||
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.TulipTolerance);
|
||||
_output.WriteLine("CRSI validated against Tulip structural composite (RSI + RSI(streak) + %Rank).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_StructuralComposite()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
double[] close = _data.ClosePrices.ToArray();
|
||||
double[] streak = ComputeStreak(close);
|
||||
double[] pctRank = ComputePercentRank(close, rankPeriod);
|
||||
|
||||
var closeRsi = ComputeOoplesRsiFull(BuildOoplesTickerData(close), rsiPeriod);
|
||||
var streakRsi = ComputeOoplesRsiFull(BuildOoplesTickerData(streak), streakPeriod);
|
||||
|
||||
var expected = ComposeCrsi(closeRsi, streakRsi, pctRank);
|
||||
var actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(actual, expected, x => x, skip: 200, tolerance: ValidationHelper.OoplesTolerance);
|
||||
_output.WriteLine("CRSI validated against Ooples structural composite (RSI + RSI(streak) + %Rank).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_NativeConnorsRsi_Batch()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
|
||||
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
TSeries actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
AssertOoplesNativeComparable(actual.Values.ToArray(), expected, "batch");
|
||||
|
||||
_output.WriteLine("CRSI batch structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_NativeConnorsRsi_Streaming()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
|
||||
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
var crsi = new Crsi(rsiPeriod, streakPeriod, rankPeriod);
|
||||
var streamVals = new double[_data.Data.Count];
|
||||
for (int i = 0; i < _data.Data.Count; i++)
|
||||
{
|
||||
streamVals[i] = crsi.Update(_data.Data[i]).Value;
|
||||
}
|
||||
|
||||
AssertOoplesNativeComparable(streamVals, expected, "streaming");
|
||||
_output.WriteLine("CRSI streaming structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_NativeConnorsRsi_Span()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var ooplesData = BuildOoplesTickerData(_data.ClosePrices.ToArray());
|
||||
var expected = ComputeOoplesConnorsRsiFull(ooplesData, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
var spanOut = new double[_data.Data.Count];
|
||||
Crsi.Batch(_data.Data.Values, spanOut, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
AssertOoplesNativeComparable(spanOut, expected, "span");
|
||||
_output.WriteLine("CRSI span structurally validated against Ooples native CalculateConnorsRelativeStrengthIndex.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_NativeConnorsRsi_Batch()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
|
||||
.ToList();
|
||||
|
||||
TSeries actual = Crsi.Batch(_data.Data, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(
|
||||
actual,
|
||||
skenderResults,
|
||||
x => x.ConnorsRsi,
|
||||
skip: 200,
|
||||
tolerance: ValidationHelper.SkenderTolerance);
|
||||
|
||||
_output.WriteLine("CRSI batch validated against Skender native GetConnorsRsi.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_NativeConnorsRsi_Streaming()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
|
||||
.ToList();
|
||||
|
||||
var crsi = new Crsi(rsiPeriod, streakPeriod, rankPeriod);
|
||||
var streamVals = new double[_data.Data.Count];
|
||||
for (int i = 0; i < _data.Data.Count; i++)
|
||||
{
|
||||
streamVals[i] = crsi.Update(_data.Data[i]).Value;
|
||||
}
|
||||
|
||||
int count = _data.Data.Count;
|
||||
int start = Math.Max(0, count - 200);
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double? expected = skenderResults[i].ConnorsRsi;
|
||||
if (!expected.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(streamVals[i] - expected.Value) <= ValidationHelper.SkenderTolerance,
|
||||
$"Streaming mismatch at i={i}: QuanTAlib={streamVals[i]:G17}, Skender={expected.Value:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("CRSI streaming validated against Skender native GetConnorsRsi.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_NativeConnorsRsi_Span()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
|
||||
.ToList();
|
||||
|
||||
var spanOut = new double[_data.Data.Count];
|
||||
Crsi.Batch(_data.Data.Values, spanOut, rsiPeriod, streakPeriod, rankPeriod);
|
||||
|
||||
ValidationHelper.VerifyData(
|
||||
spanOut,
|
||||
skenderResults,
|
||||
x => x.ConnorsRsi,
|
||||
skip: 200,
|
||||
tolerance: ValidationHelper.SkenderTolerance);
|
||||
|
||||
_output.WriteLine("CRSI span validated against Skender native GetConnorsRsi.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_NativeConnorsRsi_Components()
|
||||
{
|
||||
const int rsiPeriod = 3;
|
||||
const int streakPeriod = 2;
|
||||
const int rankPeriod = 100;
|
||||
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetConnorsRsi(rsiPeriod, streakPeriod, rankPeriod)
|
||||
.ToList();
|
||||
|
||||
// Verify all 3 sub-components are populated for converged bars
|
||||
int count = _data.Data.Count;
|
||||
int start = Math.Max(0, count - 100);
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
var r = skenderResults[i];
|
||||
Assert.True(r.Rsi.HasValue, $"Skender Rsi null at {i}");
|
||||
Assert.True(r.RsiStreak.HasValue, $"Skender RsiStreak null at {i}");
|
||||
Assert.True(r.PercentRank.HasValue, $"Skender PercentRank null at {i}");
|
||||
Assert.True(r.ConnorsRsi.HasValue, $"Skender ConnorsRsi null at {i}");
|
||||
Assert.InRange(r.ConnorsRsi!.Value, 0.0, 100.0);
|
||||
}
|
||||
|
||||
_output.WriteLine("Skender ConnorsRsi components all present and in [0,100] for converged bars.");
|
||||
}
|
||||
|
||||
private static double[] ComputeStreak(ReadOnlySpan<double> close)
|
||||
{
|
||||
int n = close.Length;
|
||||
var streak = new double[n];
|
||||
|
||||
int s = 0;
|
||||
streak[0] = 0.0;
|
||||
for (int i = 1; i < n; i++)
|
||||
{
|
||||
if (close[i] > close[i - 1])
|
||||
{
|
||||
s = s >= 0 ? s + 1 : 1;
|
||||
}
|
||||
else if (close[i] < close[i - 1])
|
||||
{
|
||||
s = s <= 0 ? s - 1 : -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = 0;
|
||||
}
|
||||
|
||||
streak[i] = s;
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
private static double[] ComputePercentRank(ReadOnlySpan<double> close, int rankPeriod)
|
||||
{
|
||||
int n = close.Length;
|
||||
var pct = new double[n];
|
||||
|
||||
var rocBuf = new double[rankPeriod];
|
||||
int head = 0;
|
||||
int count = 0;
|
||||
double prev = double.NaN;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double roc = 0.0;
|
||||
if (!double.IsNaN(prev) && prev != 0.0)
|
||||
{
|
||||
roc = (close[i] - prev) / prev * 100.0;
|
||||
}
|
||||
|
||||
prev = close[i];
|
||||
|
||||
// Scan BEFORE writing current roc (compare against historical values only)
|
||||
int lessCount = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
if (rocBuf[j] < roc)
|
||||
{
|
||||
lessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
pct[i] = count > 0 ? (double)lessCount / count * 100.0 : 50.0;
|
||||
|
||||
// Store current ROC after rank scan
|
||||
rocBuf[head] = roc;
|
||||
head = (head + 1) % rankPeriod;
|
||||
if (count < rankPeriod)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return pct;
|
||||
}
|
||||
|
||||
private static double[] ComposeCrsi(double[] priceRsi, double[] streakRsi, double[] pctRank)
|
||||
{
|
||||
int n = priceRsi.Length;
|
||||
var result = new double[n];
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double a = priceRsi[i];
|
||||
double b = streakRsi[i];
|
||||
double c = pctRank[i];
|
||||
|
||||
if (!double.IsFinite(a) || !double.IsFinite(b) || !double.IsFinite(c))
|
||||
{
|
||||
result[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
|
||||
double v = (a + b + c) / 3.0;
|
||||
result[i] = Math.Clamp(v, 0.0, 100.0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void AssertOoplesNativeComparable(double[] actual, double[] expected, string mode)
|
||||
{
|
||||
int count = Math.Min(actual.Length, expected.Length);
|
||||
int start = Math.Max(0, count - 300);
|
||||
|
||||
var a = new List<double>(300);
|
||||
var b = new List<double>(300);
|
||||
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double x = actual[i];
|
||||
double y = expected[i];
|
||||
|
||||
if (double.IsFinite(x) && double.IsFinite(y))
|
||||
{
|
||||
Assert.InRange(x, 0.0, 100.0);
|
||||
Assert.InRange(y, 0.0, 100.0);
|
||||
a.Add(x);
|
||||
b.Add(y);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(a.Count >= 150, $"Insufficient overlapping finite values for Ooples {mode} validation.");
|
||||
|
||||
double mae = 0.0;
|
||||
for (int i = 0; i < a.Count; i++)
|
||||
{
|
||||
mae += Math.Abs(a[i] - b[i]);
|
||||
}
|
||||
|
||||
mae /= a.Count;
|
||||
|
||||
Assert.True(
|
||||
mae <= 20.0,
|
||||
$"Ooples {mode} MAE too large for structural agreement: {mae:G17}");
|
||||
_output.WriteLine($"CRSI {mode} vs Ooples native: finite={a.Count}, MAE={mae:G6}");
|
||||
}
|
||||
|
||||
private static Quote[] BuildSyntheticQuotes(IReadOnlyList<Quote> baseQuotes, double[] values)
|
||||
{
|
||||
var quotes = new Quote[values.Length];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
decimal v = (decimal)values[i];
|
||||
quotes[i] = new Quote
|
||||
{
|
||||
Date = baseQuotes[i].Date,
|
||||
Open = v,
|
||||
High = v,
|
||||
Low = v,
|
||||
Close = v,
|
||||
Volume = baseQuotes[i].Volume
|
||||
};
|
||||
}
|
||||
|
||||
return quotes;
|
||||
}
|
||||
|
||||
private static double[] ComputeTalibRsiFull(double[] input, int period)
|
||||
{
|
||||
var output = new double[input.Length];
|
||||
var ret = TALib.Functions.Rsi<double>(input, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, ret);
|
||||
|
||||
var full = Enumerable.Repeat(double.NaN, input.Length).ToArray();
|
||||
var (offset, length) = outRange.GetOffsetAndLength(output.Length);
|
||||
for (int i = 0; i < length && (offset + i) < full.Length; i++)
|
||||
{
|
||||
full[offset + i] = output[i];
|
||||
}
|
||||
|
||||
return full;
|
||||
}
|
||||
|
||||
private static double[] ComputeTulipRsiFull(double[] input, int period)
|
||||
{
|
||||
var indicator = Tulip.Indicators.rsi;
|
||||
double[][] inputs = { input };
|
||||
double[] options = { period };
|
||||
|
||||
int lookback = indicator.Start(options);
|
||||
double[][] outputs = { new double[input.Length - lookback] };
|
||||
indicator.Run(inputs, options, outputs);
|
||||
|
||||
var full = Enumerable.Repeat(double.NaN, input.Length).ToArray();
|
||||
var rsi = outputs[0];
|
||||
for (int i = 0; i < rsi.Length; i++)
|
||||
{
|
||||
full[i + lookback] = rsi[i];
|
||||
}
|
||||
|
||||
return full;
|
||||
}
|
||||
|
||||
private static double[] ComputeOoplesConnorsRsiFull(List<TickerData> data, int rsiPeriod, int streakPeriod, int rankPeriod)
|
||||
{
|
||||
var stockData = new StockData(data);
|
||||
|
||||
// Ooples uses extension methods declared on static Calculations class.
|
||||
var method = typeof(Calculations).GetMethods()
|
||||
.FirstOrDefault(m =>
|
||||
string.Equals(m.Name, "CalculateConnorsRelativeStrengthIndex", StringComparison.Ordinal) &&
|
||||
m.GetParameters().Length > 0 &&
|
||||
m.GetParameters()[0].ParameterType == typeof(StockData));
|
||||
|
||||
Assert.NotNull(method);
|
||||
|
||||
var parameters = method!.GetParameters();
|
||||
var args = new object?[parameters.Length];
|
||||
args[0] = stockData; // extension target
|
||||
|
||||
int idx = 0;
|
||||
int[] periods = [rsiPeriod, streakPeriod, rankPeriod];
|
||||
|
||||
for (int i = 1; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
|
||||
if ((p.ParameterType == typeof(int) || p.ParameterType == typeof(int?)) && idx < periods.Length)
|
||||
{
|
||||
args[i] = periods[idx++];
|
||||
}
|
||||
else if (p.HasDefaultValue)
|
||||
{
|
||||
args[i] = p.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
args[i] = Type.Missing;
|
||||
}
|
||||
}
|
||||
|
||||
var result = method.Invoke(null, args) as StockData;
|
||||
Assert.NotNull(result);
|
||||
|
||||
var outputValues = result!.OutputValues as System.Collections.IDictionary;
|
||||
Assert.NotNull(outputValues);
|
||||
Assert.NotEmpty(outputValues!.Keys);
|
||||
|
||||
object? firstSeries = outputValues.Values.Cast<object?>().FirstOrDefault(v => v is IEnumerable<double>);
|
||||
Assert.NotNull(firstSeries);
|
||||
|
||||
return ((IEnumerable<double>)firstSeries!).ToArray();
|
||||
}
|
||||
|
||||
private static List<TickerData> BuildOoplesTickerData(double[] values)
|
||||
{
|
||||
var list = new List<TickerData>(values.Length);
|
||||
var start = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
double v = values[i];
|
||||
list.Add(new TickerData
|
||||
{
|
||||
Date = start.AddMinutes(i),
|
||||
Open = v,
|
||||
High = v,
|
||||
Low = v,
|
||||
Close = v,
|
||||
Volume = 1.0
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static double[] ComputeOoplesRsiFull(List<TickerData> data, int period)
|
||||
{
|
||||
var stockData = new StockData(data);
|
||||
var result = stockData.CalculateRelativeStrengthIndex(length: period);
|
||||
return result.OutputValues.Values.First().ToArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user