mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +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,160 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AobvIndicatorTests
|
||||
{
|
||||
private const int SlowPeriod = 14;
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
|
||||
Assert.Equal("AOBV - Archer On-Balance Volume", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SlowPeriod, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_ShortName_IsFixed()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
Assert.Equal("AOBV", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_MinHistoryDepths_EqualsSlowPeriod()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
|
||||
Assert.Equal(SlowPeriod, indicator.MinHistoryDepths);
|
||||
Assert.Equal(SlowPeriod, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_Initialize_CreatesInternalAobv()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, two line series should exist (Fast and Slow)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
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, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Both line series should have values
|
||||
double fastVal = indicator.LinesSeries[0].GetValue(0);
|
||||
double slowVal = indicator.LinesSeries[1].GetValue(0);
|
||||
Assert.True(double.IsFinite(fastVal), "Fast EMA should be finite");
|
||||
Assert.True(double.IsFinite(slowVal), "Slow EMA should be finite");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
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, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[1].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_FastSlowRelationship_InUptrend()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Create consistent uptrend: closes always rising
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
basePrice, // Open
|
||||
basePrice + 2, // High
|
||||
basePrice - 1, // Low
|
||||
basePrice + 1, // Close (rising)
|
||||
1000000); // Volume
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// In sustained uptrend, both EMAs should be rising
|
||||
double fastVal = indicator.LinesSeries[0].GetValue(0);
|
||||
double slowVal = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
// Both should be positive (accumulating volume)
|
||||
Assert.True(fastVal > 0, $"Fast EMA should be positive in uptrend: {fastVal}");
|
||||
Assert.True(slowVal > 0, $"Slow EMA should be positive in uptrend: {slowVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_Values_AreFinite()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double open = 100 + i;
|
||||
double high = open + 10 + (i % 5);
|
||||
double low = open - 5;
|
||||
double close = (i % 2 == 0) ? high - 1 : low + 1;
|
||||
double volume = 1000 + (i * 100);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double fastVal = indicator.LinesSeries[0].GetValue(0);
|
||||
double slowVal = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(fastVal), $"Fast EMA value should be finite: {fastVal}");
|
||||
Assert.True(double.IsFinite(slowVal), $"Slow EMA value should be finite: {slowVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AobvIndicator_TwoLineSeries_Exist()
|
||||
{
|
||||
var indicator = new AobvIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Fast", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Slow", indicator.LinesSeries[1].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AobvTests
|
||||
{
|
||||
[Fact]
|
||||
public void Aobv_Constructor_SetsCorrectName()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
Assert.Equal("AOBV(4,14)", aobv.Name);
|
||||
Assert.Equal(14, aobv.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_BasicCalculation_ReturnsFiniteValues()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var bar1 = new TBar(time, 100, 105, 95, 102, 1000);
|
||||
var val1 = aobv.Update(bar1);
|
||||
|
||||
Assert.True(double.IsFinite(val1.Value));
|
||||
Assert.True(double.IsFinite(aobv.LastFast.Value));
|
||||
Assert.True(double.IsFinite(aobv.LastSlow.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_OBV_AccumulatesCorrectly()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
aobv.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
|
||||
// Second bar: Close = 105 (up), adds volume
|
||||
aobv.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000), isNew: true);
|
||||
|
||||
// Third bar: Close = 102 (down), subtracts volume
|
||||
aobv.Update(new TBar(time.AddMinutes(2), 105, 108, 100, 102, 1500), isNew: true);
|
||||
|
||||
Assert.True(double.IsFinite(aobv.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_IsNew_False_UpdatesSameBar()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var bar1 = new TBar(time, 100, 105, 95, 102, 1000);
|
||||
aobv.Update(bar1, isNew: true);
|
||||
_ = aobv.LastFast.Value;
|
||||
_ = aobv.LastSlow.Value;
|
||||
|
||||
// Update same bar with different close
|
||||
var bar1Update = new TBar(time, 100, 105, 95, 103, 1000);
|
||||
aobv.Update(bar1Update, isNew: false);
|
||||
|
||||
// Values may change due to different OBV calculation
|
||||
Assert.True(double.IsFinite(aobv.LastFast.Value));
|
||||
Assert.True(double.IsFinite(aobv.LastSlow.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
// Build up some state
|
||||
TBar tenthBar = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthBar = gbm.Next(isNew: true);
|
||||
aobv.Update(tenthBar, isNew: true);
|
||||
}
|
||||
|
||||
double stateAfterTenFast = aobv.LastFast.Value;
|
||||
double stateAfterTenSlow = aobv.LastSlow.Value;
|
||||
|
||||
// Multiple corrections
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
aobv.Update(bar, isNew: false);
|
||||
}
|
||||
|
||||
// Restore with original 10th bar
|
||||
aobv.Update(tenthBar, isNew: false);
|
||||
|
||||
Assert.Equal(stateAfterTenFast, aobv.LastFast.Value, 9);
|
||||
Assert.Equal(stateAfterTenSlow, aobv.LastSlow.Value, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Reset_ClearsState()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: OBV = 0 (no prev bar to compare)
|
||||
aobv.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
// Second bar with higher close: OBV += volume
|
||||
aobv.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// After two bars with price increase, should have non-zero value
|
||||
Assert.NotEqual(0, aobv.Last.Value);
|
||||
|
||||
aobv.Reset();
|
||||
Assert.False(aobv.IsHot);
|
||||
Assert.Equal(0, aobv.Last.Value);
|
||||
Assert.Equal(0, aobv.LastFast.Value);
|
||||
Assert.Equal(0, aobv.LastSlow.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_IsHot_FlipsAtWarmupPeriod()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
Assert.False(aobv.IsHot);
|
||||
|
||||
for (int i = 0; i < 13; i++)
|
||||
{
|
||||
aobv.Update(gbm.Next());
|
||||
Assert.False(aobv.IsHot);
|
||||
}
|
||||
|
||||
aobv.Update(gbm.Next());
|
||||
Assert.True(aobv.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
aobv.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
aobv.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// NaN close
|
||||
var result = aobv.Update(new TBar(time.AddMinutes(2), 105, 108, 100, double.NaN, 1500));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
|
||||
// NaN volume
|
||||
result = aobv.Update(new TBar(time.AddMinutes(3), 100, 108, 100, 103, double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
aobv.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
|
||||
var result = aobv.Update(new TBar(time.AddMinutes(1), 100, 110, 98, double.PositiveInfinity, 2000));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
|
||||
result = aobv.Update(new TBar(time.AddMinutes(2), 100, 110, 98, 105, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_TValueUpdate_ThrowsNotSupportedException()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
Assert.Throws<NotSupportedException>(() => aobv.Update(new TValue(DateTime.UtcNow, 100)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
bool eventFired = false;
|
||||
aobv.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
aobv.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_UpdateTBarSeries_ReturnsCorrectSeries()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
var (fast, slow) = aobv.Update(bars);
|
||||
|
||||
Assert.Equal(50, fast.Count);
|
||||
Assert.Equal(50, slow.Count);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(fast[i].Value));
|
||||
Assert.True(double.IsFinite(slow[i].Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_CalculateTBarSeries_ReturnsCorrectSeries()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
var (fast, slow) = Aobv.Calculate(bars);
|
||||
|
||||
Assert.Equal(50, fast.Count);
|
||||
Assert.Equal(50, slow.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_CalculateSpan_ReturnsCorrectValues()
|
||||
{
|
||||
double[] close = { 100, 102, 101, 103, 102 };
|
||||
double[] volume = { 1000, 1500, 1200, 1800, 1100 };
|
||||
double[] outputFast = new double[5];
|
||||
double[] outputSlow = new double[5];
|
||||
|
||||
Aobv.Batch(close, volume, outputFast, outputSlow);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(outputFast[i]));
|
||||
Assert.True(double.IsFinite(outputSlow[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_CalculateSpan_ThrowsOnMismatchedLengths()
|
||||
{
|
||||
double[] close = { 100, 102 };
|
||||
double[] volume = { 1000 }; // Short
|
||||
double[] outputFast = new double[2];
|
||||
double[] outputSlow = new double[2];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Aobv.Batch(close, volume, outputFast, outputSlow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_CalculateSpan_ThrowsOnMismatchedOutputLength()
|
||||
{
|
||||
double[] close = { 100, 102 };
|
||||
double[] volume = { 1000, 1500 };
|
||||
double[] outputFast = new double[1]; // Short
|
||||
double[] outputSlow = new double[2];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Aobv.Batch(close, volume, outputFast, outputSlow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Calculate_EmptySeries_ReturnsEmpty()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var (fast, slow) = Aobv.Calculate(bars);
|
||||
Assert.Empty(fast);
|
||||
Assert.Empty(slow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_StreamingMatchesBatch()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var aobvStreaming = new Aobv();
|
||||
var streamingFast = new List<double>();
|
||||
var streamingSlow = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
aobvStreaming.Update(bar);
|
||||
streamingFast.Add(aobvStreaming.LastFast.Value);
|
||||
streamingSlow.Add(aobvStreaming.LastSlow.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var (batchFast, batchSlow) = Aobv.Calculate(bars);
|
||||
|
||||
// Compare after warmup
|
||||
for (int i = 14; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(batchFast[i].Value, streamingFast[i], 9);
|
||||
Assert.Equal(batchSlow[i].Value, streamingSlow[i], 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_FastRespondsQuickerThanSlow()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed steady prices first
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
aobv.Update(new TBar(time.AddMinutes(i), 100, 101, 99, 100, 1000), isNew: true);
|
||||
}
|
||||
|
||||
double fastBefore = aobv.LastFast.Value;
|
||||
double slowBefore = aobv.LastSlow.Value;
|
||||
|
||||
// Sudden price spike with high volume
|
||||
aobv.Update(new TBar(time.AddMinutes(20), 100, 110, 100, 108, 5000), isNew: true);
|
||||
|
||||
double fastAfter = aobv.LastFast.Value;
|
||||
double slowAfter = aobv.LastSlow.Value;
|
||||
|
||||
// Fast should change more than slow
|
||||
double fastChange = Math.Abs(fastAfter - fastBefore);
|
||||
double slowChange = Math.Abs(slowAfter - slowBefore);
|
||||
|
||||
Assert.True(fastChange > slowChange,
|
||||
$"Fast change ({fastChange}) should be greater than slow change ({slowChange})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_WarmupCompensation_ProducesNonZeroFirstValue()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar with price increase should produce non-zero OBV
|
||||
aobv.Update(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
||||
// OBV = 0 on first bar
|
||||
|
||||
// Second bar with higher close
|
||||
aobv.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000), isNew: true);
|
||||
// OBV = 2000, EMA should be compensated
|
||||
|
||||
Assert.NotEqual(0, aobv.LastFast.Value);
|
||||
Assert.NotEqual(0, aobv.LastSlow.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for AOBV (Archer On-Balance Volume) indicator.
|
||||
/// Note: AOBV is a proprietary indicator not available in external libraries
|
||||
/// (TA-Lib, Skender, Tulip, Ooples). Validation focuses on internal consistency.
|
||||
/// </summary>
|
||||
public class AobvValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public AobvValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_NotAvailable_Skender()
|
||||
{
|
||||
// AOBV is a proprietary indicator by EverGet (Archer)
|
||||
// Not available in Skender.Stock.Indicators
|
||||
Assert.True(true, "AOBV is proprietary - not available in Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_NotAvailable_Talib()
|
||||
{
|
||||
// AOBV is a proprietary indicator
|
||||
// TA-Lib has OBV but not AOBV (smoothed OBV)
|
||||
Assert.True(true, "AOBV is proprietary - not available in TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_NotAvailable_Tulip()
|
||||
{
|
||||
// AOBV is a proprietary indicator
|
||||
// Tulip has OBV but not AOBV (smoothed OBV)
|
||||
Assert.True(true, "AOBV is proprietary - not available in Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_NotAvailable_Ooples()
|
||||
{
|
||||
// AOBV is a proprietary indicator
|
||||
// Not available in OoplesFinance.StockIndicators
|
||||
Assert.True(true, "AOBV is proprietary - not available in Ooples");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Streaming_Matches_Batch()
|
||||
{
|
||||
// Streaming
|
||||
var aobv = new Aobv();
|
||||
var streamingFast = new List<double>();
|
||||
var streamingSlow = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
streamingFast.Add(aobv.LastFast.Value);
|
||||
streamingSlow.Add(aobv.LastSlow.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var (batchFast, _) = Aobv.Calculate(_data.Bars);
|
||||
var batchFastArray = batchFast.Values.ToArray();
|
||||
|
||||
// Compare Fast EMA values (primary output)
|
||||
ValidationHelper.VerifyData(streamingFast.ToArray(), batchFastArray, 0, 100, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Span_Matches_Streaming()
|
||||
{
|
||||
// Streaming
|
||||
var aobv = new Aobv();
|
||||
var streamingFast = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
streamingFast.Add(aobv.LastFast.Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanFast = new double[close.Length];
|
||||
var spanSlow = new double[close.Length];
|
||||
|
||||
Aobv.Batch(close, volume, spanFast, spanSlow);
|
||||
|
||||
ValidationHelper.VerifyData(streamingFast.ToArray(), spanFast, 0, 100, 1e-12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Fast_Slow_Relationship()
|
||||
{
|
||||
// Fast EMA (period 4) should be more responsive than Slow EMA (period 14)
|
||||
// Calculate variance of differences from raw OBV
|
||||
var aobv = new Aobv();
|
||||
var fastDeltas = new List<double>();
|
||||
var slowDeltas = new List<double>();
|
||||
double prevFast = 0, prevSlow = 0;
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
if (aobv.IsHot)
|
||||
{
|
||||
fastDeltas.Add(Math.Abs(aobv.LastFast.Value - prevFast));
|
||||
slowDeltas.Add(Math.Abs(aobv.LastSlow.Value - prevSlow));
|
||||
}
|
||||
prevFast = aobv.LastFast.Value;
|
||||
prevSlow = aobv.LastSlow.Value;
|
||||
}
|
||||
|
||||
// Fast should have higher average delta (more responsive)
|
||||
var avgFastDelta = fastDeltas.Average();
|
||||
var avgSlowDelta = slowDeltas.Average();
|
||||
|
||||
Assert.True(avgFastDelta >= avgSlowDelta * 0.9,
|
||||
$"Fast EMA should be at least as responsive as slow. Fast avg delta: {avgFastDelta}, Slow avg delta: {avgSlowDelta}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Warmup_Convergence()
|
||||
{
|
||||
// Test that warmup compensation produces stable values
|
||||
var aobv = new Aobv();
|
||||
int warmupPeriod = aobv.WarmupPeriod;
|
||||
int count = 0;
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
count++;
|
||||
if (count >= warmupPeriod)
|
||||
{
|
||||
Assert.True(aobv.IsHot, $"Should be hot after {warmupPeriod} bars");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_Values_Are_Finite()
|
||||
{
|
||||
var aobv = new Aobv();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
Assert.True(double.IsFinite(aobv.LastFast.Value), "Fast EMA should be finite");
|
||||
Assert.True(double.IsFinite(aobv.LastSlow.Value), "Slow EMA should be finite");
|
||||
Assert.True(double.IsFinite(aobv.Last.Value), "Last value should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Aobv_CrossValidation_OBV_Trend()
|
||||
{
|
||||
// When OBV is trending up, both EMAs should eventually trend up
|
||||
// Create synthetic uptrend data
|
||||
var bars = new TBarSeries();
|
||||
double baseClose = 100.0;
|
||||
double baseVolume = 1000000.0;
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Consistently rising closes with volume
|
||||
bars.Add(new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
baseClose + i, // Open
|
||||
baseClose + i + 1, // High
|
||||
baseClose + i - 0.5, // Low
|
||||
baseClose + i + 0.5, // Close (always rising)
|
||||
baseVolume));
|
||||
}
|
||||
|
||||
var aobv = new Aobv();
|
||||
double lastFast = 0, lastSlow = 0;
|
||||
int risingFastCount = 0, risingSlowCount = 0;
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
aobv.Update(bar);
|
||||
if (aobv.IsHot)
|
||||
{
|
||||
if (aobv.LastFast.Value > lastFast)
|
||||
{
|
||||
risingFastCount++;
|
||||
}
|
||||
if (aobv.LastSlow.Value > lastSlow)
|
||||
{
|
||||
risingSlowCount++;
|
||||
}
|
||||
lastFast = aobv.LastFast.Value;
|
||||
lastSlow = aobv.LastSlow.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// In an uptrend, most values should be rising
|
||||
Assert.True(risingFastCount > 20, $"Fast EMA should trend up in uptrend, rising count: {risingFastCount}");
|
||||
Assert.True(risingSlowCount > 15, $"Slow EMA should trend up in uptrend, rising count: {risingSlowCount}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user