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
+224
View File
@@ -0,0 +1,224 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class ObvIndicatorTests
{
[Fact]
public void ObvIndicator_Constructor_SetsDefaults()
{
var indicator = new ObvIndicator();
Assert.Equal("OBV - On Balance Volume", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(2, indicator.MinHistoryDepths);
}
[Fact]
public void ObvIndicator_ShortName_IsConstant()
{
var indicator = new ObvIndicator();
Assert.Equal("OBV", indicator.ShortName);
}
[Fact]
public void ObvIndicator_MinHistoryDepths_EqualsTwo()
{
var indicator = new ObvIndicator();
Assert.Equal(2, indicator.MinHistoryDepths);
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void ObvIndicator_Initialize_CreatesInternalObv()
{
var indicator = new ObvIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void ObvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new ObvIndicator();
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
// Varying close prices to trigger OBV changes
double close = 100 + (i % 2 == 0 ? i : -i / 2);
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, close, 100000);
// 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 val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void ObvIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new ObvIndicator();
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 with higher close to increase OBV
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 ObvIndicator_UpClose_IncreasesObv()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// First bar
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstVal = indicator.LinesSeries[0].GetValue(0);
// Second bar with higher close - OBV should increase by volume
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 108, 50000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(secondVal > firstVal, $"OBV should increase when close rises: {secondVal} vs {firstVal}");
Assert.Equal(50000, secondVal - firstVal, 1); // Volume added
}
[Fact]
public void ObvIndicator_DownClose_DecreasesObv()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// First bar
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstVal = indicator.LinesSeries[0].GetValue(0);
// Second bar with lower close - OBV should decrease by volume
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 102, 90, 92, 50000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
Assert.True(secondVal < firstVal, $"OBV should decrease when close falls: {secondVal} vs {firstVal}");
Assert.Equal(-50000, secondVal - firstVal, 1); // Volume subtracted
}
[Fact]
public void ObvIndicator_EqualClose_ObvUnchanged()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// First bar
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstVal = indicator.LinesSeries[0].GetValue(0);
// Second bar with same close - OBV should not change
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 90, 100, 200000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double secondVal = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(firstVal, secondVal);
}
[Fact]
public void ObvIndicator_Cumulative_CorrectAccumulation()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Bar 1: close=100, volume=10000 -> OBV=0 (first bar)
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Bar 2: close=110 (up), volume=20000 -> OBV=+20000
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 115, 98, 110, 20000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
// Bar 3: close=105 (down), volume=15000 -> OBV=+20000-15000=5000
indicator.HistoricalData.AddBar(now.AddMinutes(2), 110, 112, 100, 105, 15000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
// Bar 4: close=108 (up), volume=10000 -> OBV=5000+10000=15000
indicator.HistoricalData.AddBar(now.AddMinutes(3), 105, 110, 104, 108, 10000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double finalVal = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(15000, finalVal, 1);
}
[Fact]
public void ObvIndicator_LargeVolume_HandlesCorrectly()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Test with large volume values
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 1_000_000_000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 108, 2_000_000_000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(2_000_000_000, val, 1);
}
[Fact]
public void ObvIndicator_StartsAtZero()
{
var indicator = new ObvIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// First bar - OBV should be 0
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 100000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstVal = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(0, firstVal);
}
}
+403
View File
@@ -0,0 +1,403 @@
using Xunit;
namespace QuanTAlib.Tests;
public class ObvTests
{
[Fact]
public void Constructor_DefaultParameters_CreatesValidIndicator()
{
var obv = new Obv();
Assert.Equal("Obv", obv.Name);
Assert.Equal(2, obv.WarmupPeriod);
Assert.False(obv.IsHot);
}
[Fact]
public void Update_WithTBar_ReturnsValidValue()
{
var obv = new Obv();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result = obv.Update(bar);
Assert.True(double.IsFinite(result.Value));
Assert.Equal(0, result.Value); // First bar stays at zero (no comparison)
}
[Fact]
public void Update_WithTValue_ReturnsCurrentValue()
{
var obv = new Obv();
var value = new TValue(DateTime.UtcNow, 100);
var result = obv.Update(value);
// OBV without volume data returns current OBV value (zero initially)
Assert.Equal(0, result.Value);
}
[Fact]
public void Update_PriceIncreases_AddsVolume()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// First bar - establishes baseline
obv.Update(new TBar(time, 100, 105, 95, 100, 100000));
// Second bar with higher close - OBV should add volume
var result = obv.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 105, 80000));
Assert.Equal(80000, result.Value);
}
[Fact]
public void Update_PriceDecreases_SubtractsVolume()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// First bar - establishes baseline
obv.Update(new TBar(time, 100, 105, 95, 100, 100000));
// Second bar with lower close - OBV should subtract volume
var result = obv.Update(new TBar(time.AddMinutes(1), 100, 102, 90, 95, 80000));
Assert.Equal(-80000, result.Value);
}
[Fact]
public void Update_PriceUnchanged_ObvUnchanged()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// First bar
obv.Update(new TBar(time, 100, 105, 95, 100, 100000));
var firstObv = obv.Last.Value;
// Second bar with same close - OBV should stay the same
var result = obv.Update(new TBar(time.AddMinutes(1), 100, 108, 92, 100, 150000));
Assert.Equal(firstObv, result.Value);
}
[Fact]
public void Update_ConsistentUpDays_ObvIncreases()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// Build up with consistently rising prices
double price = 100;
for (int i = 0; i < 20; i++)
{
obv.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, 10000));
price += 1; // Price increasing each day
}
Assert.True(obv.Last.Value > 0, $"OBV should be positive after consistent up days, was {obv.Last.Value}");
}
[Fact]
public void Update_ConsistentDownDays_ObvDecreases()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// Build up with consistently falling prices
double price = 100;
for (int i = 0; i < 20; i++)
{
obv.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, 10000));
price -= 1; // Price decreasing each day
}
Assert.True(obv.Last.Value < 0, $"OBV should be negative after consistent down days, was {obv.Last.Value}");
}
[Fact]
public void Update_IsNewTrue_AdvancesState()
{
var obv = new Obv();
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
var result1 = obv.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 800000);
var result2 = obv.Update(bar2, isNew: true);
Assert.NotEqual(result1.Time, result2.Time);
}
[Fact]
public void Update_IsNewFalse_UpdatesCurrentBar()
{
var obv = new Obv();
var gbm = new GBM(seed: 42);
// Build up history
for (int i = 0; i < 20; i++)
{
obv.Update(gbm.Next(), isNew: true);
}
// Get a new bar
var bar1 = gbm.Next();
var result1 = obv.Update(bar1, isNew: true);
// Create a correction with different close
var bar2 = new TBar(bar1.Time, bar1.Open, bar1.High, bar1.Low, bar1.Close * 1.1, bar1.Volume);
var result2 = obv.Update(bar2, isNew: false);
Assert.Equal(result1.Time, result2.Time);
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void Update_IterativeCorrections_RestoresState()
{
var obv = new Obv();
var gbm = new GBM(seed: 123);
// Build up history
for (int i = 0; i < 20; i++)
{
obv.Update(gbm.Next(), isNew: true);
}
_ = obv.Last.Value; // Capture state before new bar
// New bar
var originalBar = gbm.Next();
obv.Update(originalBar, isNew: true);
// Correction with same values should restore similar state
var correctionBar = originalBar;
var correctedResult = obv.Update(correctionBar, isNew: false);
Assert.True(double.IsFinite(correctedResult.Value));
}
[Fact]
public void Update_WarmupPeriod_IsHotBecomesTrueAfterWarmup()
{
var obv = new Obv();
var time = DateTime.UtcNow;
Assert.False(obv.IsHot);
obv.Update(new TBar(time, 100, 110, 90, 105, 100000), isNew: true);
Assert.False(obv.IsHot);
obv.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 80000), isNew: true);
Assert.True(obv.IsHot);
}
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var obv = new Obv();
var time = DateTime.UtcNow;
// Process some valid bars first
for (int i = 0; i < 10; i++)
{
obv.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102 + i, 100000));
}
_ = obv.Last.Value;
// Process bar with NaN volume
var nanBar = new TBar(time.AddMinutes(10), 105, 110, 100, 115, double.NaN);
var result = obv.Update(nanBar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_ZeroVolume_HandlesGracefully()
{
var obv = new Obv();
var time = DateTime.UtcNow;
obv.Update(new TBar(time, 100, 110, 90, 105, 100000));
var result = obv.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 0));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Reset_ClearsState()
{
var obv = new Obv();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
obv.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000), isNew: true);
}
Assert.True(obv.IsHot);
Assert.True(double.IsFinite(obv.Last.Value));
obv.Reset();
Assert.False(obv.IsHot);
Assert.Equal(default, obv.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 obv = new Obv();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(obv.Update(bar).Value);
}
// Batch
var batchResult = Obv.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 bars = new TBarSeries();
var gbm = new GBM(seed: 42);
for (int i = 0; i < 100; i++)
{
bars.Add(gbm.Next());
}
// Streaming
var obv = new Obv();
var streamingValues = new List<double>();
foreach (var bar in bars)
{
streamingValues.Add(obv.Update(bar).Value);
}
// Span
var close = bars.Close.Values.ToArray();
var volume = bars.Volume.Values.ToArray();
var output = new double[bars.Count];
Obv.Batch(close, volume, output);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingValues[i], output[i], 10);
}
}
[Fact]
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
{
var close = new double[100];
var volume = new double[99]; // Different length
var output = new double[100];
Assert.Throws<ArgumentException>(() => Obv.Batch(close, volume, output));
}
[Fact]
public void SpanCalculate_EmptyInput_HandlesGracefully()
{
var close = Array.Empty<double>();
var volume = Array.Empty<double>();
var output = Array.Empty<double>();
Obv.Batch(close, volume, output);
Assert.Empty(output);
}
[Fact]
public void Event_PubFiresOnUpdate()
{
var obv = new Obv();
TValue? receivedValue = null;
bool receivedIsNew = false;
obv.Pub += (object? sender, in TValueEventArgs args) =>
{
receivedValue = args.Value;
receivedIsNew = args.IsNew;
};
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
obv.Update(bar, 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 obv = new Obv();
foreach (var bar in bars)
{
var result = obv.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
Assert.True(obv.IsHot);
}
[Fact]
public void FormulaVerification_ManualCalculation()
{
// Manual verification of OBV formula with known values
var obv = new Obv();
var time = DateTime.UtcNow;
// Bar 1: baseline (close = 100, volume = 10000)
obv.Update(new TBar(time, 100, 105, 95, 100, 10000));
Assert.Equal(0, obv.Last.Value); // First bar, OBV starts at 0
// Bar 2: price up (105 > 100), add volume
// Expected: OBV = 0 + 15000 = 15000
obv.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 105, 15000));
Assert.Equal(15000, obv.Last.Value);
// Bar 3: price down (102 < 105), subtract volume
// Expected: OBV = 15000 - 12000 = 3000
obv.Update(new TBar(time.AddMinutes(2), 105, 108, 100, 102, 12000));
Assert.Equal(3000, obv.Last.Value);
// Bar 4: price unchanged (102 == 102), OBV unchanged
// Expected: OBV = 3000
obv.Update(new TBar(time.AddMinutes(3), 102, 106, 100, 102, 20000));
Assert.Equal(3000, obv.Last.Value);
// Bar 5: price up (110 > 102), add volume
// Expected: OBV = 3000 + 8000 = 11000
obv.Update(new TBar(time.AddMinutes(4), 102, 112, 100, 110, 8000));
Assert.Equal(11000, obv.Last.Value);
}
}
@@ -0,0 +1,162 @@
using Skender.Stock.Indicators;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public class ObvValidationTests
{
private readonly ValidationTestData _data;
public ObvValidationTests()
{
_data = new ValidationTestData();
}
[Fact]
public void Obv_Matches_Skender()
{
// Skender
var skenderResults = _data.SkenderQuotes.GetObv();
var skenderValues = skenderResults.Select(x => x.Obv).ToArray();
// QuanTAlib
var obv = new Obv();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(obv.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), skenderValues, 0, 100, ValidationHelper.SkenderTolerance);
}
[Fact]
public void Obv_Matches_Talib()
{
// TA-Lib OBV may have different handling for cumulative calculation
// QuanTAlib matches Skender and Tulip implementations
// Known discrepancy: TA-Lib may use different starting value or NaN handling
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var talibValues = new double[close.Length];
var retCode = TALib.Functions.Obv(close, volume, 0..^0, talibValues, out _);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
// QuanTAlib
var obv = new Obv();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(obv.Update(bar).Value);
}
// Verify both produce finite values (implementation may differ in cumulative handling)
Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib OBV should produce finite values");
Assert.True(talibValues.All(v => double.IsFinite(v)), "TA-Lib OBV should produce finite values");
// Note: TA-Lib and QuanTAlib may diverge over long series due to different
// cumulative calculation approaches. QuanTAlib matches Skender and Tulip.
}
[Fact]
public void Obv_Matches_Tulip()
{
// Tulip
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var tulipIndicator = Tulip.Indicators.obv;
double[][] inputs = { close, volume };
double[] options = Array.Empty<double>();
double[][] outputs = { new double[close.Length] };
tulipIndicator.Run(inputs, options, outputs);
var tulipValues = outputs[0];
// QuanTAlib
var obv = new Obv();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(obv.Update(bar).Value);
}
ValidationHelper.VerifyData(quantalibValues.ToArray(), tulipValues, 0, 100, ValidationHelper.TulipTolerance);
}
[Fact]
public void Obv_Matches_Ooples()
{
// Ooples OBV may have different handling for cumulative calculation
// QuanTAlib matches Skender and Tulip implementations
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 stockData = new StockData(ooplesData);
var oResult = stockData.CalculateOnBalanceVolume();
var oValues = oResult.OutputValues["Obv"];
// QuanTAlib
var obv = new Obv();
var quantalibValues = new List<double>();
foreach (var bar in _data.Bars)
{
quantalibValues.Add(obv.Update(bar).Value);
}
// Verify both produce finite values (implementation may differ in cumulative handling)
Assert.True(quantalibValues.All(v => double.IsFinite(v)), "QuanTAlib OBV should produce finite values");
Assert.True(oValues.All(v => double.IsFinite(v)), "Ooples OBV should produce finite values");
// Note: Ooples and QuanTAlib may diverge over long series due to different
// cumulative calculation approaches. QuanTAlib matches Skender and Tulip.
}
[Fact]
public void Obv_Streaming_Matches_Batch()
{
// Streaming
var obv = new Obv();
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(obv.Update(bar).Value);
}
// Batch
var batchResult = Obv.Batch(_data.Bars);
var batchValues = batchResult.Values.ToArray();
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
}
[Fact]
public void Obv_Span_Matches_Streaming()
{
// Streaming
var obv = new Obv();
var streamingValues = new List<double>();
foreach (var bar in _data.Bars)
{
streamingValues.Add(obv.Update(bar).Value);
}
// Span
var close = _data.Bars.Close.Values.ToArray();
var volume = _data.Bars.Volume.Values.ToArray();
var spanOutput = new double[close.Length];
Obv.Batch(close, volume, spanOutput);
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
}
}