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
+230
View File
@@ -0,0 +1,230 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class VaIndicatorTests
{
[Fact]
public void VaIndicator_Constructor_SetsDefaults()
{
var indicator = new VaIndicator();
Assert.Equal("VA - Volume Accumulation", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(1, indicator.MinHistoryDepths);
}
[Fact]
public void VaIndicator_ShortName_IsConstant()
{
var indicator = new VaIndicator();
Assert.Equal("VA", indicator.ShortName);
}
[Fact]
public void VaIndicator_MinHistoryDepths_EqualsOne()
{
var indicator = new VaIndicator();
Assert.Equal(1, indicator.MinHistoryDepths);
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void VaIndicator_Initialize_CreatesInternalVa()
{
var indicator = new VaIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void VaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double close = 100 + i * 0.5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 100000);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void VaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105, 100000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(30), 105, 115, 100, 112, 80000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void VaIndicator_CloseAboveMidpoint_PositiveAccumulation()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar: H=110, L=90, C=105, V=1000
// midpoint = (110 + 90) / 2 = 100
// va_period = 1000 * (105 - 100) = 5000
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(5000, val, 1);
}
[Fact]
public void VaIndicator_CloseBelowMidpoint_NegativeAccumulation()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar: H=110, L=90, C=95, V=1000
// midpoint = (110 + 90) / 2 = 100
// va_period = 1000 * (95 - 100) = -5000
indicator.HistoricalData.AddBar(now, 100, 110, 90, 95, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(-5000, val, 1);
}
[Fact]
public void VaIndicator_CloseAtMidpoint_ZeroAccumulation()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar: H=110, L=90, C=100, V=1000
// midpoint = (110 + 90) / 2 = 100
// va_period = 1000 * (100 - 100) = 0
indicator.HistoricalData.AddBar(now, 100, 110, 90, 100, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, val, 1);
}
[Fact]
public void VaIndicator_MultipleBarAccumulation_CorrectSum()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar 1: midpoint=100, close=105, vol=1000 -> va=5000
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Bar 2: midpoint=100, close=95, vol=500 -> va_period=-2500, total=2500
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 90, 95, 500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(2500, val, 1);
}
[Fact]
public void VaIndicator_LargeVolume_LargerImpact()
{
var indicator1 = new VaIndicator();
indicator1.Initialize();
var indicator2 = new VaIndicator();
indicator2.Initialize();
var now = DateTime.UtcNow;
// Same price action, different volume
indicator1.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator2.HistoricalData.AddBar(now, 100, 110, 90, 105, 10000);
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val1 = indicator1.LinesSeries[0].GetValue(0);
double val2 = indicator2.LinesSeries[0].GetValue(0);
// 10x volume should produce 10x VA
Assert.Equal(val1 * 10, val2, 1);
}
[Fact]
public void VaIndicator_CumulativeNature_AlwaysAccumulates()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
double lastVa = 0;
// Add multiple positive bars - VA should keep increasing
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 108, 1000);
var args = i == 0
? new UpdateArgs(UpdateReason.HistoricalBar)
: new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
double currentVa = indicator.LinesSeries[0].GetValue(0);
Assert.True(currentVa > lastVa, $"VA should increase: {currentVa} > {lastVa}");
lastVa = currentVa;
}
}
[Fact]
public void VaIndicator_MixedPressure_CorrectNetEffect()
{
var indicator = new VaIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Equal positive and negative with same volume should net to zero
// Bar 1: +5000 (close above midpoint)
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Bar 2: -5000 (close below midpoint by same amount)
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 90, 95, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, val, 1);
}
}
+346
View File
@@ -0,0 +1,346 @@
using Xunit;
namespace QuanTAlib.Tests;
public class VaTests
{
[Fact]
public void Constructor_CreatesValidIndicator()
{
var va = new Va();
Assert.Equal("Va", va.Name);
Assert.Equal(1, Va.WarmupPeriod);
Assert.False(va.IsHot);
}
[Fact]
public void Update_WithTBar_ReturnsValidValue()
{
var va = new Va();
// Bar: H=110, L=90, C=105, V=1000
// midpoint = (110 + 90) / 2 = 100
// va_period = 1000 * (105 - 100) = 5000
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result = va.Update(bar);
Assert.Equal(5000, result.Value, 10);
}
[Fact]
public void Update_CloseAboveMidpoint_PositiveValue()
{
var va = new Va();
// Close above midpoint = buying pressure = positive
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 108, 1000);
// midpoint = 100, va = 1000 * (108 - 100) = 8000
var result = va.Update(bar);
Assert.True(result.Value > 0);
Assert.Equal(8000, result.Value, 10);
}
[Fact]
public void Update_CloseBelowMidpoint_NegativeValue()
{
var va = new Va();
// Close below midpoint = selling pressure = negative
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 92, 1000);
// midpoint = 100, va = 1000 * (92 - 100) = -8000
var result = va.Update(bar);
Assert.True(result.Value < 0);
Assert.Equal(-8000, result.Value, 10);
}
[Fact]
public void Update_CloseAtMidpoint_ZeroValue()
{
var va = new Va();
// Close at midpoint = neutral
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
// midpoint = 100, va = 1000 * (100 - 100) = 0
var result = va.Update(bar);
Assert.Equal(0, result.Value, 10);
}
[Fact]
public void Update_MultipleValues_Accumulates()
{
var va = new Va();
var time = DateTime.UtcNow;
// Bar 1: midpoint=100, close=105, vol=1000 -> va=5000
va.Update(new TBar(time, 100, 110, 90, 105, 1000));
Assert.Equal(5000, va.Last.Value, 10);
// Bar 2: midpoint=100, close=95, vol=500 -> va_period=-2500, total=2500
va.Update(new TBar(time.AddMinutes(1), 100, 110, 90, 95, 500));
Assert.Equal(2500, va.Last.Value, 10);
// Bar 3: midpoint=100, close=100, vol=2000 -> va_period=0, total=2500
va.Update(new TBar(time.AddMinutes(2), 100, 110, 90, 100, 2000));
Assert.Equal(2500, va.Last.Value, 10);
}
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var va = new Va();
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result1 = va.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 800);
var result2 = va.Update(bar2, isNew: true);
Assert.NotEqual(result1.Time, result2.Time);
Assert.NotEqual(result1.Value, result2.Value);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var va = new Va();
var gbm = new GBM(seed: 42);
// Build up history
for (int i = 0; i < 20; i++)
{
va.Update(gbm.Next(), isNew: true);
}
// New bar
var bar1 = gbm.Next();
va.Update(bar1, isNew: true);
// Correction - restore previous state
va.Update(bar1, isNew: false);
// Value should change based on bar correction
Assert.True(double.IsFinite(va.Last.Value));
}
[Fact]
public void Update_IterativeCorrections_RestoresState()
{
var va = new Va();
var gbm = new GBM(seed: 123);
// Build up history
for (int i = 0; i < 20; i++)
{
va.Update(gbm.Next(), isNew: true);
}
// New bar
var originalBar = gbm.Next();
va.Update(originalBar, isNew: true);
// Correction with same values using isNew=false should restore
va.Update(originalBar, isNew: false);
Assert.True(double.IsFinite(va.Last.Value));
}
[Fact]
public void Update_WarmupPeriod_IsHotAfterFirstBar()
{
var va = new Va();
Assert.False(va.IsHot);
va.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000), isNew: true);
Assert.True(va.IsHot);
}
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var va = new Va();
var time = DateTime.UtcNow;
// Process valid bar first
va.Update(new TBar(time, 100, 110, 90, 105, 1000));
// Process bar with NaN close
var nanBar = new TBar(time.AddMinutes(1), 100, 110, 90, double.NaN, 500);
var result = va.Update(nanBar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Reset_ClearsState()
{
var va = new Va();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 10; i++)
{
va.Update(gbm.Next(), isNew: true);
}
Assert.True(va.IsHot);
Assert.NotEqual(0, va.Last.Value);
va.Reset();
Assert.False(va.IsHot);
Assert.Equal(default, va.Last);
}
[Fact]
public void BatchCalculate_MatchesStreaming()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
bars.Add(gbm.Next());
}
// Streaming
var va = new Va();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(va.Update(bar).Value);
}
// Batch
var batchResult = Va.Batch(bars);
Assert.Equal(bars.Count, batchResult.Count);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingValues[i], batchResult[i].Value, 10);
}
}
[Fact]
public void SpanCalculate_MatchesStreaming()
{
var gbm = new GBM(seed: 42);
int count = 100;
var high = new double[count];
var low = new double[count];
var close = new double[count];
var volume = new double[count];
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
high[i] = bar.High;
low[i] = bar.Low;
close[i] = bar.Close;
volume[i] = bar.Volume;
}
// Streaming
var va = new Va();
var streamingValues = new List<double>();
var time = DateTime.UtcNow;
for (int i = 0; i < count; i++)
{
streamingValues.Add(va.Update(new TBar(time.AddMinutes(i), 0, high[i], low[i], close[i], volume[i])).Value);
}
// Span
var output = new double[count];
Va.Batch(high, low, close, volume, output);
for (int i = 0; i < count; i++)
{
Assert.Equal(streamingValues[i], output[i], 10);
}
}
[Fact]
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
{
var high = new double[100];
var low = new double[100];
var close = new double[100];
var volume = new double[99]; // Different length
var output = new double[100];
Assert.Throws<ArgumentException>(() => Va.Batch(high, low, close, volume, output));
}
[Fact]
public void SpanCalculate_EmptyInput_HandlesGracefully()
{
var high = Array.Empty<double>();
var low = Array.Empty<double>();
var close = Array.Empty<double>();
var volume = Array.Empty<double>();
var output = Array.Empty<double>();
Va.Batch(high, low, close, volume, output);
Assert.Empty(output);
}
[Fact]
public void Event_PubFiresOnUpdate()
{
var va = new Va();
TValue? receivedValue = null;
bool receivedIsNew = false;
va.Pub += (object? sender, in TValueEventArgs args) =>
{
receivedValue = args.Value;
receivedIsNew = args.IsNew;
};
va.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000), isNew: true);
Assert.NotNull(receivedValue);
Assert.True(receivedIsNew);
}
[Fact]
public void LargeDataset_HandlesWithoutError()
{
var bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 10000; i++)
{
bars.Add(gbm.Next());
}
var va = new Va();
foreach (var bar in bars)
{
var result = va.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
Assert.True(va.IsHot);
}
[Fact]
public void FormulaVerification_ManualCalculation()
{
var va = new Va();
var time = DateTime.UtcNow;
// Bar 1: H=110, L=90, C=105, V=1000
// midpoint = (110+90)/2 = 100
// va_period = 1000 * (105 - 100) = 5000
va.Update(new TBar(time, 100, 110, 90, 105, 1000));
Assert.Equal(5000, va.Last.Value, 10);
// Bar 2: H=120, L=100, C=115, V=2000
// midpoint = (120+100)/2 = 110
// va_period = 2000 * (115 - 110) = 10000
// total = 5000 + 10000 = 15000
va.Update(new TBar(time.AddMinutes(1), 100, 120, 100, 115, 2000));
Assert.Equal(15000, va.Last.Value, 10);
// Bar 3: H=115, L=95, C=98, V=1500
// midpoint = (115+95)/2 = 105
// va_period = 1500 * (98 - 105) = -10500
// total = 15000 - 10500 = 4500
va.Update(new TBar(time.AddMinutes(2), 100, 115, 95, 98, 1500));
Assert.Equal(4500, va.Last.Value, 10);
}
}
+176
View File
@@ -0,0 +1,176 @@
// Va: Mathematical property validation tests
// Volume Accumulation is a cumulative indicator. No standard external library equivalents
// with matching implementation. Validation uses mathematical property testing.
namespace QuanTAlib.Tests;
using Xunit;
public class VaValidationTests
{
private const int TestDataLength = 500;
[Fact]
public void Va_Output_IsFiniteForGbmData()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var va = new Va();
for (int i = 0; i < bars.Count; i++)
{
var result = va.Update(bars[i], isNew: true);
Assert.True(double.IsFinite(result.Value),
$"Va output must be finite at bar {i}, got {result.Value}");
}
}
[Fact]
public void Va_CloseAboveMidpoint_PositiveAccumulation()
{
var va = new Va();
// Close is above midpoint: (H+L)/2 = 100, Close = 102
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 1000);
var result = va.Update(bar, isNew: true);
// VA_period = volume * (close - midpoint) = 1000 * (102 - 100) = 2000
Assert.True(result.Value > 0,
$"VA should be positive when close > midpoint, got {result.Value}");
}
[Fact]
public void Va_CloseBelowMidpoint_NegativeAccumulation()
{
var va = new Va();
// Close is below midpoint: (H+L)/2 = 100, Close = 98
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 98, 1000);
var result = va.Update(bar, isNew: true);
// VA_period = volume * (close - midpoint) = 1000 * (98 - 100) = -2000
Assert.True(result.Value < 0,
$"VA should be negative when close < midpoint, got {result.Value}");
}
[Fact]
public void Va_CloseAtMidpoint_ZeroAccumulation()
{
var va = new Va();
// Close is exactly at midpoint
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 100, 1000);
var result = va.Update(bar, isNew: true);
Assert.Equal(0.0, result.Value, precision: 10);
}
[Fact]
public void Va_ZeroVolume_ZeroAccumulation()
{
var va = new Va();
// Even with close above midpoint, zero volume = zero VA contribution
var bar = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 0);
var result = va.Update(bar, isNew: true);
Assert.Equal(0.0, result.Value, precision: 10);
}
[Fact]
public void Va_IsCumulative_AccumulatesOverBars()
{
var va = new Va();
// Bar 1: close above midpoint
var bar1 = new TBar(DateTime.UtcNow, 101, 101, 99, 102, 1000);
var r1 = va.Update(bar1, isNew: true);
double expectedVa1 = 1000 * (102 - 100.0); // 2000
// Bar 2: close below midpoint
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 101, 101, 99, 98, 500);
var r2 = va.Update(bar2, isNew: true);
double expectedVa2 = expectedVa1 + 500 * (98 - 100.0); // 2000 + (-1000) = 1000
Assert.Equal(expectedVa1, r1.Value, precision: 10);
Assert.Equal(expectedVa2, r2.Value, precision: 10);
}
[Fact]
public void Va_KnownCalculation_MatchesManual()
{
var va = new Va();
// Manually verified calculation
// Bar: O=100, H=105, L=95, C=103, V=2000
// Midpoint = (105 + 95) / 2 = 100
// VA_period = 2000 * (103 - 100) = 6000
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 103, 2000);
var result = va.Update(bar, isNew: true);
Assert.Equal(6000.0, result.Value, precision: 10);
}
[Fact]
public void Va_BatchAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch
var batchResults = Va.Batch(bars);
// Streaming
var streamVa = new Va();
var streamResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
var result = streamVa.Update(bars[i], isNew: true);
streamResults[i] = result.Value;
}
Assert.Equal(batchResults.Count, bars.Count);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(batchResults.Values[i], streamResults[i], precision: 8);
}
}
[Fact]
public void Va_SpanAndStreaming_ProduceSameResults()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var spanOutput = new double[bars.Count];
Va.Batch(
bars.High.Values, bars.Low.Values,
bars.Close.Values, bars.Volume.Values,
spanOutput);
// Streaming
var streamVa = new Va();
for (int i = 0; i < bars.Count; i++)
{
var result = streamVa.Update(bars[i], isNew: true);
Assert.Equal(spanOutput[i], result.Value, precision: 8);
}
}
[Fact]
public void Va_BarCorrection_IsNewFalse_RestoresState()
{
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var va = new Va();
for (int i = 0; i < 30; i++)
{
va.Update(bars[i], isNew: true);
}
va.Update(bars[30], isNew: true);
double afterNew = va.Last.Value;
va.Update(bars[30], isNew: false);
double afterCorrection = va.Last.Value;
Assert.Equal(afterNew, afterCorrection, precision: 10);
}
}