mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +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,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user