mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +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,278 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TviIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TviIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TviIndicator();
|
||||
|
||||
Assert.Equal("TVI - Trade Volume Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(0.125, indicator.MinTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_ShortName_IsConstant()
|
||||
{
|
||||
var indicator = new TviIndicator();
|
||||
Assert.Equal("TVI", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new TviIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_MinTick_CanBeSet()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.5 };
|
||||
Assert.Equal(0.5, indicator.MinTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_Initialize_CreatesInternalTvi()
|
||||
{
|
||||
var indicator = new TviIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Varying close prices to trigger TVI direction changes
|
||||
double close = 100 + (i % 2 == 0 ? i * 0.5 : -i * 0.25);
|
||||
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 TviIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
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 significant price change
|
||||
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 TviIndicator_PriceAboveMinTick_DirectionUp_AddsVolume()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Second bar with price increase > minTick - direction up, adds volume
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 100.5, 20000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(secondVal > firstVal, $"TVI should increase when price rises above minTick: {secondVal} vs {firstVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_PriceBelowNegMinTick_DirectionDown_SubtractsVolume()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Second bar with price decrease > minTick - direction down, subtracts volume
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 102, 90, 99.5, 20000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(secondVal < firstVal, $"TVI should decrease when price falls below -minTick: {secondVal} vs {firstVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_PriceWithinMinTick_DirectionSticky()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 1.0 }; // Large minTick for testing
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar with large price increase - direction up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 105, 20000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double upVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Third bar with small price change within minTick - direction stays up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 105, 106, 104, 105.2, 15000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double stickyVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Direction stayed up, so volume added
|
||||
Assert.True(stickyVal > upVal, $"TVI direction should be sticky: {stickyVal} vs {upVal}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_Cumulative_CorrectAccumulation()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Bar 1: close=100 -> TVI=0 (first bar, direction=1 by default)
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Bar 2: close=101 (up > minTick), volume=20000 -> TVI=+20000
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 98, 101, 20000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double afterUp = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(20000, afterUp, 1);
|
||||
|
||||
// Bar 3: close=99.5 (down > minTick), volume=15000 -> TVI=20000-15000=5000
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 101, 102, 99, 99.5, 15000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double afterDown = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(5000, afterDown, 1);
|
||||
|
||||
// Bar 4: close=100 (up > minTick), volume=10000 -> TVI=5000+10000=15000
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 99.5, 101, 99, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double finalVal = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(15000, finalVal, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_LargeVolume_HandlesCorrectly()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
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 TviIndicator_StartsAtZero()
|
||||
{
|
||||
var indicator = new TviIndicator { MinTick = 0.125 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar - TVI 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TviIndicator_DifferentMinTick_AffectsBehavior()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Indicator with small minTick
|
||||
var smallTick = new TviIndicator { MinTick = 0.01 };
|
||||
smallTick.Initialize();
|
||||
|
||||
// Indicator with large minTick
|
||||
var largeTick = new TviIndicator { MinTick = 5.0 };
|
||||
largeTick.Initialize();
|
||||
|
||||
// First bar
|
||||
smallTick.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
smallTick.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
largeTick.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
largeTick.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar with price change of 0.5
|
||||
smallTick.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 100.5, 20000);
|
||||
smallTick.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
largeTick.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 100.5, 20000);
|
||||
largeTick.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double smallVal = smallTick.LinesSeries[0].GetValue(0);
|
||||
double largeVal = largeTick.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Small tick: 0.5 > 0.01, direction changes -> adds volume
|
||||
// Large tick: 0.5 < 5.0, direction stays same (up) -> adds volume
|
||||
// Both add volume but direction logic differs
|
||||
Assert.True(double.IsFinite(smallVal));
|
||||
Assert.True(double.IsFinite(largeVal));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TviTests
|
||||
{
|
||||
private const double DefaultMinTick = 0.125;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_CreatesValidIndicator()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
Assert.Equal($"Tvi({DefaultMinTick})", tvi.Name);
|
||||
Assert.Equal(2, tvi.WarmupPeriod);
|
||||
Assert.False(tvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomMinTick_SetsParameter()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
Assert.Equal("Tvi(0.5)", tvi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroMinTick_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Tvi(minTick: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeMinTick_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Tvi(minTick: -0.1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTBar_ReturnsValidValue()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
var result = tvi.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 tvi = new Tvi();
|
||||
var value = new TValue(DateTime.UtcNow, 100);
|
||||
var result = tvi.Update(value);
|
||||
// TVI without volume data returns current TVI value (zero initially)
|
||||
Assert.Equal(0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceIncreasesAboveMinTick_DirectionUp_AddsVolume()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar - establishes baseline
|
||||
tvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
|
||||
// Second bar with price increase > minTick - direction becomes up, add volume
|
||||
var result = tvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 101, 80000)); // +1 > 0.5
|
||||
|
||||
Assert.Equal(80000, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceDecreasesAboveMinTick_DirectionDown_SubtractsVolume()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar - establishes baseline
|
||||
tvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
|
||||
// Second bar with price decrease > minTick - direction becomes down, subtract volume
|
||||
var result = tvi.Update(new TBar(time.AddMinutes(1), 100, 102, 90, 99, 80000)); // -1 < -0.5
|
||||
|
||||
Assert.Equal(-80000, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceChangeWithinMinTick_DirectionSticky()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar - establishes baseline
|
||||
tvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
|
||||
// Second bar - big move up, direction = 1
|
||||
tvi.Update(new TBar(time.AddMinutes(1), 100, 108, 98, 102, 80000)); // +2 > 0.5, direction = 1
|
||||
Assert.Equal(80000, tvi.Last.Value);
|
||||
|
||||
// Third bar - small move (within minTick), direction stays 1
|
||||
var result = tvi.Update(new TBar(time.AddMinutes(2), 102, 103, 101, 102.2, 50000)); // +0.2 < 0.5, sticky
|
||||
|
||||
Assert.Equal(80000 + 50000, result.Value); // Still adds because direction is still 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_DirectionStickyWhenPriceFlat()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
tvi.Update(new TBar(time, 100, 105, 95, 100, 100000));
|
||||
|
||||
// Second bar - move down, direction = -1
|
||||
tvi.Update(new TBar(time.AddMinutes(1), 100, 102, 90, 99, 80000)); // -1 < -0.5
|
||||
Assert.Equal(-80000, tvi.Last.Value);
|
||||
|
||||
// Third bar - flat price, direction stays -1
|
||||
var result = tvi.Update(new TBar(time.AddMinutes(2), 99, 100, 98, 99, 50000)); // 0 within ±0.5
|
||||
|
||||
Assert.Equal(-80000 - 50000, result.Value); // Subtracts because direction is still -1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConsistentUpDays_TviIncreases()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
double price = 100;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, 10000));
|
||||
price += 1; // Price increasing each day by more than minTick
|
||||
}
|
||||
|
||||
Assert.True(tvi.Last.Value > 0, $"TVI should be positive after consistent up days, was {tvi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConsistentDownDays_TviDecreases()
|
||||
{
|
||||
var tvi = new Tvi(minTick: 0.1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
double price = 100;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tvi.Update(new TBar(time.AddMinutes(i), price, price + 2, price - 1, price, 10000));
|
||||
price -= 1; // Price decreasing each day by more than minTick
|
||||
}
|
||||
|
||||
Assert.True(tvi.Last.Value < 0, $"TVI should be negative after consistent down days, was {tvi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
var result1 = tvi.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 110, 800000);
|
||||
var result2 = tvi.Update(bar2, isNew: true);
|
||||
|
||||
Assert.NotEqual(result1.Time, result2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tvi.Update(gbm.Next(), isNew: true);
|
||||
}
|
||||
|
||||
// Get a new bar
|
||||
var bar1 = gbm.Next();
|
||||
var result1 = tvi.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 = tvi.Update(bar2, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Time, result2.Time);
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoresState()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var gbm = new GBM(seed: 123);
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tvi.Update(gbm.Next(), isNew: true);
|
||||
}
|
||||
|
||||
_ = tvi.Last.Value;
|
||||
|
||||
// New bar
|
||||
var originalBar = gbm.Next();
|
||||
tvi.Update(originalBar, isNew: true);
|
||||
|
||||
// Correction with same values should restore similar state
|
||||
var correctionBar = originalBar;
|
||||
var correctedResult = tvi.Update(correctionBar, isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(correctedResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WarmupPeriod_IsHotBecomesTrueAfterWarmup()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
Assert.False(tvi.IsHot);
|
||||
|
||||
tvi.Update(new TBar(time, 100, 110, 90, 105, 100000), isNew: true);
|
||||
Assert.False(tvi.IsHot);
|
||||
|
||||
tvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 80000), isNew: true);
|
||||
Assert.True(tvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNaN_UsesLastValidValue()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Process some valid bars first
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tvi.Update(new TBar(time.AddMinutes(i), 100, 105, 95, 102 + i, 100000));
|
||||
}
|
||||
|
||||
_ = tvi.Last.Value;
|
||||
|
||||
// Process bar with NaN volume
|
||||
var nanBar = new TBar(time.AddMinutes(10), 105, 110, 100, 115, double.NaN);
|
||||
var result = tvi.Update(nanBar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ZeroVolume_HandlesGracefully()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
tvi.Update(new TBar(time, 100, 110, 90, 105, 100000));
|
||||
var result = tvi.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 0));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tvi.Update(new TBar(time.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 100000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(tvi.IsHot);
|
||||
Assert.True(double.IsFinite(tvi.Last.Value));
|
||||
|
||||
tvi.Reset();
|
||||
|
||||
Assert.False(tvi.IsHot);
|
||||
Assert.Equal(default, tvi.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 tvi = new Tvi();
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingValues.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Tvi.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 tvi = new Tvi();
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingValues.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var price = bars.Close.Values.ToArray();
|
||||
var volume = bars.Volume.Values.ToArray();
|
||||
var output = new double[bars.Count];
|
||||
|
||||
Tvi.Batch(price, volume, output);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingValues[i], output[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_InvalidLengths_ThrowsArgumentException()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[99]; // Different length
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Tvi.Batch(price, volume, output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_InvalidMinTick_ThrowsArgumentException()
|
||||
{
|
||||
var price = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Tvi.Batch(price, volume, output, minTick: 0));
|
||||
Assert.Throws<ArgumentException>(() => Tvi.Batch(price, volume, output, minTick: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_EmptyInput_HandlesGracefully()
|
||||
{
|
||||
var price = Array.Empty<double>();
|
||||
var volume = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
Tvi.Batch(price, volume, output);
|
||||
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Event_PubFiresOnUpdate()
|
||||
{
|
||||
var tvi = new Tvi();
|
||||
TValue? receivedValue = null;
|
||||
bool receivedIsNew = false;
|
||||
|
||||
tvi.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
receivedValue = args.Value;
|
||||
receivedIsNew = args.IsNew;
|
||||
};
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000000);
|
||||
tvi.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 tvi = new Tvi();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = tvi.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(tvi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormulaVerification_ManualCalculation()
|
||||
{
|
||||
// Manual verification of TVI formula with known values
|
||||
var tvi = new Tvi(minTick: 0.5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Bar 1: baseline (close = 100, volume = 10000)
|
||||
tvi.Update(new TBar(time, 100, 105, 95, 100, 10000));
|
||||
Assert.Equal(0, tvi.Last.Value); // First bar, TVI starts at 0
|
||||
|
||||
// Bar 2: price up by 2 (>0.5), direction = 1, add volume
|
||||
// Expected: TVI = 0 + 15000 = 15000
|
||||
tvi.Update(new TBar(time.AddMinutes(1), 100, 110, 95, 102, 15000));
|
||||
Assert.Equal(15000, tvi.Last.Value);
|
||||
|
||||
// Bar 3: price down by 3 (<-0.5), direction = -1, subtract volume
|
||||
// Expected: TVI = 15000 - 12000 = 3000
|
||||
tvi.Update(new TBar(time.AddMinutes(2), 102, 103, 98, 99, 12000));
|
||||
Assert.Equal(3000, tvi.Last.Value);
|
||||
|
||||
// Bar 4: price up by 0.2 (within ±0.5), direction stays -1, subtract volume
|
||||
// Expected: TVI = 3000 - 20000 = -17000
|
||||
tvi.Update(new TBar(time.AddMinutes(3), 99, 100, 98, 99.2, 20000));
|
||||
Assert.Equal(-17000, tvi.Last.Value);
|
||||
|
||||
// Bar 5: price up by 3 (>0.5), direction = 1, add volume
|
||||
// Expected: TVI = -17000 + 8000 = -9000
|
||||
tvi.Update(new TBar(time.AddMinutes(4), 99.2, 105, 99, 102.2, 8000));
|
||||
Assert.Equal(-9000, tvi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentMinTicks_ProduceDifferentResults()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
var bars = new List<TBar>
|
||||
{
|
||||
new(time, 100, 105, 95, 100, 10000),
|
||||
new(time.AddMinutes(1), 100, 101, 99, 100.3, 15000), // +0.3
|
||||
new(time.AddMinutes(2), 100.3, 101, 99, 100.1, 12000), // -0.2
|
||||
new(time.AddMinutes(3), 100.1, 102, 99, 101, 8000), // +0.9
|
||||
};
|
||||
|
||||
// With minTick = 0.1: all moves register
|
||||
var tvi01 = new Tvi(minTick: 0.1);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tvi01.Update(bar);
|
||||
}
|
||||
|
||||
// With minTick = 0.5: only large moves register
|
||||
var tvi05 = new Tvi(minTick: 0.5);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tvi05.Update(bar);
|
||||
}
|
||||
|
||||
// Results should differ due to sticky direction behavior
|
||||
Assert.NotEqual(tvi01.Last.Value, tvi05.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TviValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public TviValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
// Note: TVI (Trade Volume Index) is not available in TA-Lib, Skender, Tulip, or Ooples.
|
||||
// Validation tests focus on internal consistency between streaming, batch, and span modes.
|
||||
|
||||
[Fact]
|
||||
public void Tvi_Streaming_Matches_Batch()
|
||||
{
|
||||
const double minTick = 0.125;
|
||||
|
||||
// Streaming
|
||||
var tvi = new Tvi(minTick);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Tvi.Batch(_data.Bars, minTick);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_Span_Matches_Streaming()
|
||||
{
|
||||
const double minTick = 0.125;
|
||||
|
||||
// Streaming
|
||||
var tvi = new Tvi(minTick);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanOutput = new double[close.Length];
|
||||
|
||||
Tvi.Batch(close, volume, spanOutput, minTick);
|
||||
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_Different_MinTicks_Produce_Different_Results()
|
||||
{
|
||||
const double minTick1 = 0.1;
|
||||
const double minTick2 = 0.5;
|
||||
|
||||
var tvi1 = new Tvi(minTick1);
|
||||
var tvi2 = new Tvi(minTick2);
|
||||
|
||||
var values1 = new List<double>();
|
||||
var values2 = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
values1.Add(tvi1.Update(bar).Value);
|
||||
values2.Add(tvi2.Update(bar).Value);
|
||||
}
|
||||
|
||||
// With different minTick values, we expect different direction changes
|
||||
// leading to different cumulative values
|
||||
bool foundDifference = false;
|
||||
for (int i = 10; i < values1.Count; i++)
|
||||
{
|
||||
if (Math.Abs(values1[i] - values2[i]) > 1e-9)
|
||||
{
|
||||
foundDifference = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(foundDifference, "Different minTick values should produce different results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_With_Tiny_MinTick_Behaves_Like_OBV()
|
||||
{
|
||||
// With very small minTick, TVI should behave similarly to OBV
|
||||
// (direction changes on virtually any price change)
|
||||
const double minTick = 1e-12;
|
||||
|
||||
var tvi = new Tvi(minTick);
|
||||
var obv = new Obv();
|
||||
|
||||
var tviValues = new List<double>();
|
||||
var obvValues = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
tviValues.Add(tvi.Update(bar).Value);
|
||||
obvValues.Add(obv.Update(bar).Value);
|
||||
}
|
||||
|
||||
// With tiny minTick, TVI direction changes on any price move (like OBV)
|
||||
// Note: TVI direction is sticky when price unchanged, OBV adds 0 when unchanged
|
||||
// So they should match closely but may differ on exactly unchanged prices
|
||||
// At minimum, verify finite values and similar magnitude
|
||||
Assert.True(tviValues.All(v => double.IsFinite(v)), "TVI should produce finite values");
|
||||
Assert.True(obvValues.All(v => double.IsFinite(v)), "OBV should produce finite values");
|
||||
|
||||
// Both should have same sign (both accumulating in same direction)
|
||||
double lastTvi = tviValues[tviValues.Count - 1];
|
||||
double lastObv = obvValues[obvValues.Count - 1];
|
||||
if (lastTvi != 0 && lastObv != 0)
|
||||
{
|
||||
Assert.Equal(Math.Sign(lastTvi), Math.Sign(lastObv));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_AllModes_Match_With_Different_MinTicks()
|
||||
{
|
||||
double[] minTickValues = { 0.01, 0.05, 0.1, 0.25, 0.5, 1.0 };
|
||||
|
||||
foreach (var minTick in minTickValues)
|
||||
{
|
||||
// Streaming
|
||||
var tvi = new Tvi(minTick);
|
||||
var streamingValues = new List<double>();
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
streamingValues.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Tvi.Batch(_data.Bars, minTick);
|
||||
var batchValues = batchResult.Values.ToArray();
|
||||
|
||||
// Span
|
||||
var close = _data.Bars.Close.Values.ToArray();
|
||||
var volume = _data.Bars.Volume.Values.ToArray();
|
||||
var spanOutput = new double[close.Length];
|
||||
Tvi.Batch(close, volume, spanOutput, minTick);
|
||||
|
||||
// Verify all modes match
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), batchValues, 0, 100, 1e-9);
|
||||
ValidationHelper.VerifyData(streamingValues.ToArray(), spanOutput, 0, 100, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_Cumulative_Values_Are_Finite()
|
||||
{
|
||||
const double minTick = 0.125;
|
||||
|
||||
var tvi = new Tvi(minTick);
|
||||
var values = new List<double>();
|
||||
|
||||
foreach (var bar in _data.Bars)
|
||||
{
|
||||
values.Add(tvi.Update(bar).Value);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
Assert.True(values.All(v => double.IsFinite(v)), "All TVI values should be finite");
|
||||
|
||||
// Values should be non-zero after warmup
|
||||
Assert.True(values.Skip(10).Any(v => v != 0), "TVI should have non-zero values after warmup");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tvi_MatchesOoples_Structural()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var ooplesData = bars.Select(b => new TickerData
|
||||
{
|
||||
Date = new DateTime(b.Time, DateTimeKind.Utc),
|
||||
Open = b.Open, High = b.High, Low = b.Low,
|
||||
Close = b.Close, Volume = b.Volume
|
||||
}).ToList();
|
||||
var result = new StockData(ooplesData).CalculateTradeVolumeIndex();
|
||||
var values = result.CustomValuesList;
|
||||
int finiteCount = values.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user