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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,86 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class VortexIndicatorTests
{
[Fact]
public void VortexIndicator_Constructor_SetsDefaults()
{
var indicator = new VortexIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("Vortex", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void VortexIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new VortexIndicator { Period = 20 };
Assert.Equal(0, VortexIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void VortexIndicator_ShortName_IncludesParameters()
{
var indicator = new VortexIndicator { Period = 20 };
indicator.Initialize();
Assert.Contains("Vortex", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void VortexIndicator_SourceCodeLink_IsValid()
{
var indicator = new VortexIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Vortex.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void VortexIndicator_Initialize_CreatesInternalVortex()
{
var indicator = new VortexIndicator { Period = 14 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (VI+, VI-)
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void VortexIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new VortexIndicator { Period = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// Need enough bars for Period
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
double viPlus = indicator.LinesSeries[0].GetValue(0);
double viMinus = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(viPlus));
Assert.True(double.IsFinite(viMinus));
}
}
+390
View File
@@ -0,0 +1,390 @@
namespace QuanTAlib;
public class VortexTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
}
Assert.True(double.IsFinite(vortex.Last.Value));
Assert.True(double.IsFinite(vortex.ViPlus.Value));
Assert.True(double.IsFinite(vortex.ViMinus.Value));
}
[Fact]
public void IsNew_Consistency()
{
var vortex = new Vortex(14);
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++)
{
vortex.Update(bars[i]);
}
// Update with 100th point (isNew=true)
vortex.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);
var val2 = vortex.Update(modifiedBar, false);
var viPlus2 = vortex.ViPlus.Value;
var viMinus2 = vortex.ViMinus.Value;
// Create new instance and feed up to modified
var vortex2 = new Vortex(14);
for (int i = 0; i < 99; i++)
{
vortex2.Update(bars[i]);
}
var val3 = vortex2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
Assert.Equal(vortex2.ViPlus.Value, viPlus2, 1e-9);
Assert.Equal(vortex2.ViMinus.Value, viMinus2, 1e-9);
}
[Fact]
public void Reset_Works()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
}
vortex.Reset();
Assert.Equal(0, vortex.Last.Value);
Assert.Equal(0, vortex.ViPlus.Value);
Assert.Equal(0, vortex.ViMinus.Value);
Assert.False(vortex.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
}
Assert.True(double.IsFinite(vortex.Last.Value));
Assert.True(double.IsFinite(vortex.ViPlus.Value));
Assert.True(double.IsFinite(vortex.ViMinus.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingViPlus = new List<double>();
var streamingViMinus = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
streamingViPlus.Add(vortex.ViPlus.Value);
streamingViMinus.Add(vortex.ViMinus.Value);
}
var vortex2 = new Vortex(14);
var seriesResults = vortex2.Update(bars);
Assert.Equal(streamingViPlus.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingViPlus[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticBatch_Works()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var staticResults = Vortex.Batch(bars, 14);
Assert.Equal(bars.Count, staticResults.Count);
// Verify that after warmup, values are finite and reasonable
for (int i = 14; i < staticResults.Count; i++)
{
Assert.True(double.IsFinite(staticResults.Values[i]));
}
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Vortex(0));
Assert.Throws<ArgumentException>(() => new Vortex(1));
Assert.Throws<ArgumentException>(() => new Vortex(-1));
}
[Fact]
public void ManualCalculation_Verify()
{
// Simple manual test with period = 2
// Create bars manually
var vortex = new Vortex(2);
// Bar 0: O=100, H=105, L=95, C=102, V=1000
var bar0 = new TBar(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), 100, 105, 95, 102, 1000);
vortex.Update(bar0);
// First bar: no previous bar, VI+ = VI- = 0
Assert.Equal(0, vortex.ViPlus.Value);
Assert.Equal(0, vortex.ViMinus.Value);
// Bar 1: O=102, H=110, L=98, C=108
var bar1 = new TBar(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 1000, 102, 110, 98, 108, 1000);
vortex.Update(bar1);
// VM+ = |H1 - L0| = |110 - 95| = 15
// VM- = |L1 - H0| = |98 - 105| = 7
// TR = max(H1-L1, |H1-C0|, |L1-C0|) = max(12, |110-102|, |98-102|) = max(12, 8, 4) = 12
// Only 1 sample in buffer (not full yet with period=2)
Assert.Equal(0, vortex.ViPlus.Value); // Not IsHot yet
Assert.Equal(0, vortex.ViMinus.Value);
// Bar 2: O=108, H=115, L=100, C=112
var bar2 = new TBar(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 2000, 108, 115, 100, 112, 1000);
vortex.Update(bar2);
// VM+ = |H2 - L1| = |115 - 98| = 17
// VM- = |L2 - H1| = |100 - 110| = 10
// TR = max(H2-L2, |H2-C1|, |L2-C1|) = max(15, |115-108|, |100-108|) = max(15, 7, 8) = 15
// Sum(VM+) = 15 + 17 = 32
// Sum(VM-) = 7 + 10 = 17
// Sum(TR) = 12 + 15 = 27
// VI+ = 32 / 27 ≈ 1.185
// VI- = 17 / 27 ≈ 0.630
Assert.True(vortex.IsHot);
Assert.True(Math.Abs(vortex.ViPlus.Value - 32.0 / 27.0) < 0.001);
Assert.True(Math.Abs(vortex.ViMinus.Value - 17.0 / 27.0) < 0.001);
}
[Fact]
public void OutputsValuesAroundOne()
{
// Vortex indicator typically oscillates around 1.0
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
}
// VI+ and VI- typically range between 0.5 and 1.5
Assert.True(vortex.ViPlus.Value > 0 && vortex.ViPlus.Value < 3);
Assert.True(vortex.ViMinus.Value > 0 && vortex.ViMinus.Value < 3);
}
[Fact]
public void Uptrend_ViPlusGreaterThanViMinus()
{
var vortex = new Vortex(14);
// Create strong uptrend data
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
double basePrice = 100;
for (int i = 0; i < 50; i++)
{
double price = basePrice + i * 2; // Strong uptrend
var bar = new TBar(baseTime + i * 60000, price, price + 1, price - 0.5, price + 0.5, 1000);
vortex.Update(bar);
}
// In a strong uptrend, VI+ should be greater than VI-
Assert.True(vortex.ViPlus.Value > vortex.ViMinus.Value);
}
[Fact]
public void Downtrend_ViMinusGreaterThanViPlus()
{
var vortex = new Vortex(14);
// Create strong downtrend data
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
double basePrice = 200;
for (int i = 0; i < 50; i++)
{
double price = basePrice - i * 2; // Strong downtrend
var bar = new TBar(baseTime + i * 60000, price, price + 0.5, price - 1, price - 0.5, 1000);
vortex.Update(bar);
}
// In a strong downtrend, VI- should be greater than VI+
Assert.True(vortex.ViMinus.Value > vortex.ViPlus.Value);
}
[Fact]
public void WarmupPeriod_Correct()
{
var vortex = new Vortex(14);
Assert.Equal(14, vortex.WarmupPeriod);
Assert.Equal(14, vortex.Period);
}
[Fact]
public void Name_ReflectsParameters()
{
var vortex = new Vortex(14);
Assert.Equal("Vortex(14)", vortex.Name);
var vortex2 = new Vortex(21);
Assert.Equal("Vortex(21)", vortex2.Name);
}
[Fact]
public void NaN_Input_Handles_Gracefully()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
vortex.Update(bars[i]);
}
// Inject NaN
var nanBar = new TBar(bars[30].Time, double.NaN, double.NaN, double.NaN, double.NaN, 0);
vortex.Update(nanBar);
Assert.True(double.IsFinite(vortex.ViPlus.Value));
Assert.True(double.IsFinite(vortex.ViMinus.Value));
}
[Fact]
public void Infinity_Input_Handles_Gracefully()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
vortex.Update(bars[i]);
}
// Inject Infinity
var infBar = new TBar(bars[30].Time, double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0);
vortex.Update(infBar);
Assert.True(double.IsFinite(vortex.ViPlus.Value));
Assert.True(double.IsFinite(vortex.ViMinus.Value));
}
[Fact]
public void Event_Publishes_Correctly()
{
var vortex = new Vortex(14);
var eventCount = 0;
TValue lastValue = default;
bool wasPublished = false;
vortex.Pub += (object? sender, in TValueEventArgs args) =>
{
eventCount++;
lastValue = args.Value;
wasPublished = true;
};
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
}
Assert.Equal(20, eventCount);
Assert.True(wasPublished);
}
[Fact]
public void TValue_Update_Works()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var values = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
for (int i = 0; i < values.Count; i++)
{
vortex.Update(values[i]);
}
Assert.True(double.IsFinite(vortex.ViPlus.Value));
Assert.True(double.IsFinite(vortex.ViMinus.Value));
}
[Fact]
public void Last_EqualsViPlus()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex.Update(bars[i]);
Assert.Equal(vortex.ViPlus.Value, vortex.Last.Value);
}
}
[Fact]
public void IsHot_BecomesTrue_AfterWarmup()
{
var vortex = new Vortex(14);
var gbm = new GBM();
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 14; i++)
{
vortex.Update(bars[i]);
// First bar initializes, then period-1 more to fill buffer
if (i < 14)
{
Assert.False(vortex.IsHot);
}
}
vortex.Update(bars[14]);
Assert.True(vortex.IsHot);
}
[Fact]
public void DifferentPeriods_ProduceDifferentResults()
{
var vortex14 = new Vortex(14);
var vortex21 = new Vortex(21);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
vortex14.Update(bars[i]);
vortex21.Update(bars[i]);
}
Assert.NotEqual(vortex14.ViPlus.Value, vortex21.ViPlus.Value);
Assert.NotEqual(vortex14.ViMinus.Value, vortex21.ViMinus.Value);
}
}
@@ -0,0 +1,187 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using QuanTAlib.Tests;
namespace QuanTAlib;
public sealed class VortexValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public VortexValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
_data.Dispose();
}
[Fact]
public void MatchesSkender()
{
var vortex = new Vortex(14);
var viPlusResults = new List<double>();
var viMinusResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex.Update(_data.Bars[i]);
viPlusResults.Add(vortex.ViPlus.Value);
viMinusResults.Add(vortex.ViMinus.Value);
}
var skenderResults = _data.SkenderQuotes.GetVortex(14).ToList();
// Verify VI+
ValidationHelper.VerifyData(viPlusResults, skenderResults, x => x.Pvi);
// Verify VI-
ValidationHelper.VerifyData(viMinusResults, skenderResults, x => x.Nvi);
}
[Fact]
public void MatchesSkender_Period21()
{
var vortex = new Vortex(21);
var viPlusResults = new List<double>();
var viMinusResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex.Update(_data.Bars[i]);
viPlusResults.Add(vortex.ViPlus.Value);
viMinusResults.Add(vortex.ViMinus.Value);
}
var skenderResults = _data.SkenderQuotes.GetVortex(21).ToList();
// Verify VI+
ValidationHelper.VerifyData(viPlusResults, skenderResults, x => x.Pvi);
// Verify VI-
ValidationHelper.VerifyData(viMinusResults, skenderResults, x => x.Nvi);
}
[Fact]
public void MatchesSkender_ShortPeriod()
{
var vortex = new Vortex(7);
var viPlusResults = new List<double>();
var viMinusResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex.Update(_data.Bars[i]);
viPlusResults.Add(vortex.ViPlus.Value);
viMinusResults.Add(vortex.ViMinus.Value);
}
var skenderResults = _data.SkenderQuotes.GetVortex(7).ToList();
// Verify VI+
ValidationHelper.VerifyData(viPlusResults, skenderResults, x => x.Pvi);
// Verify VI-
ValidationHelper.VerifyData(viMinusResults, skenderResults, x => x.Nvi);
}
[Fact]
public void MatchesSkender_LongPeriod()
{
var vortex = new Vortex(28);
var viPlusResults = new List<double>();
var viMinusResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex.Update(_data.Bars[i]);
viPlusResults.Add(vortex.ViPlus.Value);
viMinusResults.Add(vortex.ViMinus.Value);
}
var skenderResults = _data.SkenderQuotes.GetVortex(28).ToList();
// Verify VI+
ValidationHelper.VerifyData(viPlusResults, skenderResults, x => x.Pvi);
// Verify VI-
ValidationHelper.VerifyData(viMinusResults, skenderResults, x => x.Nvi);
}
[Fact]
public void BatchMatchesSkender()
{
// Batch returns VI+ as the primary output (TSeries)
var batchViPlus = Vortex.Batch(_data.Bars, 14);
var skenderResults = _data.SkenderQuotes.GetVortex(14).ToList();
// Verify batch VI+ matches Skender VI+
var viPlusResults = batchViPlus.Select(x => x.Value).ToList();
ValidationHelper.VerifyData(viPlusResults, skenderResults, x => x.Pvi);
// For VI-, use streaming since Batch only returns VI+
var vortex = new Vortex(14);
var viMinusResults = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex.Update(_data.Bars[i]);
viMinusResults.Add(vortex.ViMinus.Value);
}
// Verify VI- from streaming matches Skender
ValidationHelper.VerifyData(viMinusResults, skenderResults, x => x.Nvi);
}
[Fact]
public void ConsistentAcrossMultipleRuns()
{
var vortex1 = new Vortex(14);
var vortex2 = new Vortex(14);
var results1Plus = new List<double>();
var results1Minus = new List<double>();
var results2Plus = new List<double>();
var results2Minus = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex1.Update(_data.Bars[i]);
results1Plus.Add(vortex1.ViPlus.Value);
results1Minus.Add(vortex1.ViMinus.Value);
}
for (int i = 0; i < _data.Bars.Count; i++)
{
vortex2.Update(_data.Bars[i]);
results2Plus.Add(vortex2.ViPlus.Value);
results2Minus.Add(vortex2.ViMinus.Value);
}
for (int i = 0; i < _data.Bars.Count; i++)
{
Assert.Equal(results1Plus[i], results2Plus[i], 1e-10);
Assert.Equal(results1Minus[i], results2Minus[i], 1e-10);
}
}
[Fact]
public void Vortex_MatchesOoples_Structural()
{
// CalculateVortexIndicator — structural test; outputs stored in OutputValues (ViPlus/ViMinus)
var ooplesData = _data.SkenderQuotes
.Select(q => new TickerData { Date = q.Date, Open = (double)q.Open, High = (double)q.High, Low = (double)q.Low, Close = (double)q.Close, Volume = (double)q.Volume })
.ToList();
var result = new StockData(ooplesData).CalculateVortexIndicator();
// Ooples multi-output indicators store results in OutputValues, not CustomValuesList
var allValues = result.OutputValues.Values.SelectMany(v => v).ToList();
int finiteCount = allValues.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples Vortex values, got {finiteCount}");
}
}