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,308 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VfIndicator();
|
||||
|
||||
Assert.Equal("VF - Volume Force", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(14, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_ShortName_ReflectsPeriod()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 20 };
|
||||
Assert.Equal("VF(20)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.MinHistoryDepths);
|
||||
Assert.Equal(10, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_Period_CanBeSet()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 30 };
|
||||
Assert.Equal(30, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_Initialize_CreatesInternalVf()
|
||||
{
|
||||
var indicator = new VfIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double close = 100 + i * 0.5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 100000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105, 100000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 105, 115, 100, 112, 80000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_PriceUp_PositiveForce()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar establishes baseline
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: close increases -> positive raw_vf
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 108, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val > 0, $"VF should be positive when price increases: {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_PriceDown_NegativeForce()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar establishes baseline
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: close decreases -> negative raw_vf
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 102, 90, 92, 10000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val < 0, $"VF should be negative when price decreases: {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_NoChange_ZeroForce()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// All bars with same close
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100, 10000);
|
||||
var args = i == 0
|
||||
? new UpdateArgs(UpdateReason.HistoricalBar)
|
||||
: new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0, val, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_LargerVolume_LargerImpact()
|
||||
{
|
||||
var indicator1 = new VfIndicator { Period = 14 };
|
||||
indicator1.Initialize();
|
||||
|
||||
var indicator2 = new VfIndicator { Period = 14 };
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same price action, different volume
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + i;
|
||||
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 1000);
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
|
||||
|
||||
var args = i == 0
|
||||
? new UpdateArgs(UpdateReason.HistoricalBar)
|
||||
: new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator1.ProcessUpdate(args);
|
||||
indicator2.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val1 = Math.Abs(indicator1.LinesSeries[0].GetValue(0));
|
||||
double val2 = Math.Abs(indicator2.LinesSeries[0].GetValue(0));
|
||||
|
||||
// Higher volume should produce larger magnitude
|
||||
Assert.True(val2 > val1, $"Higher volume should produce larger VF: {val2} > {val1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_DifferentPeriods_DifferentSmoothing()
|
||||
{
|
||||
var shortPeriod = new VfIndicator { Period = 5 };
|
||||
shortPeriod.Initialize();
|
||||
|
||||
var longPeriod = new VfIndicator { Period = 30 };
|
||||
longPeriod.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add volatile data
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double close = 100 + (i % 2 == 0 ? 5 : -3);
|
||||
shortPeriod.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
|
||||
longPeriod.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
|
||||
|
||||
var args = i == 0
|
||||
? new UpdateArgs(UpdateReason.HistoricalBar)
|
||||
: new UpdateArgs(UpdateReason.NewBar);
|
||||
shortPeriod.ProcessUpdate(args);
|
||||
longPeriod.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double shortVal = shortPeriod.LinesSeries[0].GetValue(0);
|
||||
double longVal = longPeriod.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Different periods should produce different results
|
||||
Assert.NotEqual(shortVal, longVal, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_EmaSmoothing_ReducesNoise()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Add noisy data
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
// Alternating price changes
|
||||
double close = 100 + (i % 2 == 0 ? 2 : -2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
|
||||
|
||||
var args = i == 0
|
||||
? new UpdateArgs(UpdateReason.HistoricalBar)
|
||||
: new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
|
||||
// After warmup, values should be relatively stable (EMA smoothing)
|
||||
var lastValues = values.Skip(20).ToList();
|
||||
double range = lastValues.Max() - lastValues.Min();
|
||||
|
||||
// EMA should smooth out the alternating pattern
|
||||
Assert.True(range < 100000, $"EMA should smooth values; range={range}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_WarmupCompensation_FirstValueNotZero()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar with significant price-volume action
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 95, 105, 50000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With warmup compensation, first value should not be severely damped
|
||||
double firstVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// First bar: no previous close, so raw_vf = 0, VF = 0
|
||||
// This is expected behavior for first bar
|
||||
Assert.True(double.IsFinite(firstVal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VfIndicator_OscillatesAroundZero()
|
||||
{
|
||||
var indicator = new VfIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
bool hasPositive = false;
|
||||
bool hasNegative = false;
|
||||
|
||||
// Mix of up and down days
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.5) * 10;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close - 2, close + 2, close - 3, close, 10000);
|
||||
|
||||
var args = i == 0
|
||||
? new UpdateArgs(UpdateReason.HistoricalBar)
|
||||
: new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
if (val > 0)
|
||||
{
|
||||
hasPositive = true;
|
||||
}
|
||||
if (val < 0)
|
||||
{
|
||||
hasNegative = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(hasPositive && hasNegative, "VF should oscillate around zero");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VfTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
private const int DefaultPeriod = 14;
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_SetsCorrectProperties()
|
||||
{
|
||||
var vf = new Vf();
|
||||
|
||||
Assert.Equal("Vf(14)", vf.Name);
|
||||
Assert.Equal(14, vf.WarmupPeriod);
|
||||
Assert.False(vf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_SetsCorrectProperties()
|
||||
{
|
||||
var vf = new Vf(period: 20);
|
||||
|
||||
Assert.Equal("Vf(20)", vf.Name);
|
||||
Assert.Equal(20, vf.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodLessThanOne_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vf(period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vf(period: -5));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsZero()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
var result = vf.Update(bar);
|
||||
|
||||
Assert.Equal(0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceIncrease_ReturnsPositiveValue()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var result = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000)); // +5 price change
|
||||
|
||||
Assert.True(result.Value > 0, "VF should be positive when price increases");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PriceDecrease_ReturnsNegativeValue()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var result = vf.Update(new TBar(time.AddMinutes(1), 100, 102, 90, 95, 2000)); // -5 price change
|
||||
|
||||
Assert.True(result.Value < 0, "VF should be negative when price decreases");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NoPriceChange_ReturnsZeroOrNearZero()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var result = vf.Update(new TBar(time.AddMinutes(1), 100, 105, 95, 100, 2000)); // 0 price change
|
||||
|
||||
Assert.Equal(0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsCorrectTime()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var expectedTime = DateTime.UtcNow;
|
||||
var bar = new TBar(expectedTime, 100, 105, 95, 102, 1000);
|
||||
|
||||
var result = vf.Update(bar);
|
||||
|
||||
Assert.Equal(expectedTime.Ticks, result.Time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Formula Verification Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondBar_AppliesEmaWithWarmupCompensation()
|
||||
{
|
||||
var vf = new Vf(period: 10);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
|
||||
// Second bar: price change = 110 - 100 = 10, raw_vf = 10 * 2000 = 20000
|
||||
var result = vf.Update(new TBar(time.AddMinutes(1), 108, 115, 105, 110, 2000));
|
||||
|
||||
// Expected: ~20000 (the warmup compensation should give us the raw value initially)
|
||||
Assert.True(Math.Abs(result.Value - 20000) < 1, "VF should be approximately 20000 with warmup compensation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleBarSequence_CalculatesCorrectly()
|
||||
{
|
||||
var vf = new Vf(period: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Bar 1: establishes baseline
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
|
||||
// Bar 2: price +10, volume 1000 -> raw_vf = 10000
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 115, 98, 110, 1000));
|
||||
|
||||
// Bar 3: price -5, volume 500 -> raw_vf = -2500
|
||||
vf.Update(new TBar(time.AddMinutes(2), 108, 112, 103, 105, 500));
|
||||
|
||||
// Bar 4: price +5, volume 2000 -> raw_vf = 10000
|
||||
var result = vf.Update(new TBar(time.AddMinutes(3), 105, 115, 104, 110, 2000));
|
||||
|
||||
// Result should be a smoothed positive value (EMA of 10000, -2500, 10000)
|
||||
Assert.True(result.Value > 0, "VF should be positive given more positive raw_vf values");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var vf = new Vf(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
vf.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000));
|
||||
}
|
||||
|
||||
Assert.False(vf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AtWarmup_ReturnsTrue()
|
||||
{
|
||||
var vf = new Vf(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vf.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000));
|
||||
}
|
||||
|
||||
Assert.True(vf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var vf = new Vf(period: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vf.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000));
|
||||
}
|
||||
|
||||
Assert.True(vf.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction (isNew=false) Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RollsBackState()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var valueAfterFirst = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Update same bar with different data (isNew=false)
|
||||
var valueAfterCorrection = vf.Update(new TBar(time.AddMinutes(1), 100, 108, 96, 103, 1500), isNew: false);
|
||||
|
||||
// Values should differ because the bar was corrected
|
||||
Assert.NotEqual(valueAfterFirst.Value, valueAfterCorrection.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_MaintainsConsistency()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
|
||||
// First update
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Multiple corrections
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 108, 96, 103, 1500), isNew: false);
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 112, 97, 108, 2500), isNew: false);
|
||||
var finalValue = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000), isNew: false);
|
||||
|
||||
// Final correction back to original should match
|
||||
vf.Reset();
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var expectedValue = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
Assert.Equal(expectedValue.Value, finalValue.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreOriginalState()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
var originalValue = vf.Update(new TBar(time.AddMinutes(2), 105, 115, 103, 110, 1500));
|
||||
|
||||
// Make correction
|
||||
vf.Update(new TBar(time.AddMinutes(2), 105, 120, 100, 115, 3000), isNew: false);
|
||||
|
||||
// Restore original
|
||||
var restoredValue = vf.Update(new TBar(time.AddMinutes(2), 105, 115, 103, 110, 1500), isNew: false);
|
||||
|
||||
Assert.Equal(originalValue.Value, restoredValue.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vf.Update(new TBar(time.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000));
|
||||
}
|
||||
|
||||
vf.Reset();
|
||||
|
||||
Assert.False(vf.IsHot);
|
||||
Assert.Equal(default, vf.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First use
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var firstResult = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
vf.Reset();
|
||||
|
||||
// Second use with same data
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
var secondResult = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
Assert.Equal(firstResult.Value, secondResult.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNClose_UsesLastValidValue()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
_ = vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Update with NaN close
|
||||
var nanResult = vf.Update(new TBar(time.AddMinutes(2), 105, 115, 100, double.NaN, 1500));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value), "VF should handle NaN close gracefully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNVolume_UsesLastValidValue()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Update with NaN volume
|
||||
var result = vf.Update(new TBar(time.AddMinutes(2), 105, 115, 100, 110, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "VF should handle NaN volume gracefully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Update with infinity
|
||||
var result = vf.Update(new TBar(time.AddMinutes(2), 105, 115, 100, double.PositiveInfinity, 1500));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "VF should handle infinity gracefully");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var vf = new Vf();
|
||||
TValue? receivedValue = null;
|
||||
bool? receivedIsNew = null;
|
||||
|
||||
vf.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
receivedValue = args.Value;
|
||||
receivedIsNew = args.IsNew;
|
||||
};
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
var result = vf.Update(bar);
|
||||
|
||||
Assert.NotNull(receivedValue);
|
||||
Assert.Equal(result.Value, receivedValue.Value.Value);
|
||||
Assert.True(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_PublishesEventWithIsNewFalse()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
|
||||
bool? receivedIsNew = null;
|
||||
vf.Pub += (object? sender, in TValueEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Mode Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var series = GenerateTestBarSeries(100);
|
||||
|
||||
var result = vf.Update(series);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var series = GenerateTestBarSeries(100);
|
||||
|
||||
var result = Vf.Batch(series, DefaultPeriod);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_EmptySeries_ReturnsEmpty()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
var result = Vf.Batch(series, DefaultPeriod);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span Mode Tests
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MatchesStreamingMode()
|
||||
{
|
||||
var series = GenerateTestBarSeries(50);
|
||||
var close = new double[50];
|
||||
var volume = new double[50];
|
||||
var output = new double[50];
|
||||
|
||||
// Extract values from series
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
close[i] = series[i].Close;
|
||||
volume[i] = series[i].Volume;
|
||||
}
|
||||
|
||||
// Span calculation
|
||||
Vf.Batch(close, volume, output, DefaultPeriod);
|
||||
|
||||
// Streaming calculation
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
var streamingResult = vf.Update(series);
|
||||
|
||||
// Compare last 30 values (after warmup)
|
||||
for (int i = 20; i < 50; i++)
|
||||
{
|
||||
Assert.Equal(streamingResult[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var close = new double[100];
|
||||
var volume = new double[50]; // Different length
|
||||
var output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vf.Batch(close, volume, output, DefaultPeriod));
|
||||
Assert.Equal("volume", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_OutputLengthMismatch_ThrowsArgumentException()
|
||||
{
|
||||
var close = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[50]; // Different length
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vf.Batch(close, volume, output, DefaultPeriod));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var close = new double[100];
|
||||
var volume = new double[100];
|
||||
var output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vf.Batch(close, volume, output, period: 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_EmptyInput_ReturnsWithoutError()
|
||||
{
|
||||
var close = Array.Empty<double>();
|
||||
var volume = Array.Empty<double>();
|
||||
var output = Array.Empty<double>();
|
||||
|
||||
// Should not throw
|
||||
Vf.Batch(close, volume, output, DefaultPeriod);
|
||||
Assert.True(true); // Test passes if no exception
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_FirstValueIsZero()
|
||||
{
|
||||
var close = new double[] { 100, 105, 110, 108, 112 };
|
||||
var volume = new double[] { 1000, 2000, 1500, 1800, 2200 };
|
||||
var output = new double[5];
|
||||
|
||||
Vf.Batch(close, volume, output, period: 3);
|
||||
|
||||
Assert.Equal(0, output[0]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TValue Update Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ThrowsNotSupportedException()
|
||||
{
|
||||
var vf = new Vf();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state with bars
|
||||
vf.Update(new TBar(time, 100, 105, 95, 100, 1000));
|
||||
vf.Update(new TBar(time.AddMinutes(1), 100, 110, 98, 105, 2000));
|
||||
|
||||
// Update with TValue should throw NotSupportedException (VF requires volume)
|
||||
var ex = Assert.Throws<NotSupportedException>(() => vf.Update(new TValue(time.AddMinutes(2), 110)));
|
||||
Assert.Contains("volume", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
var series = GenerateTestBarSeries(100);
|
||||
var close = new double[100];
|
||||
var volume = new double[100];
|
||||
|
||||
// Extract values from series
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
close[i] = series[i].Close;
|
||||
volume[i] = series[i].Volume;
|
||||
}
|
||||
|
||||
// Streaming mode
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
var streamingResult = vf.Update(series);
|
||||
|
||||
// Batch mode
|
||||
var batchResult = Vf.Batch(series, DefaultPeriod);
|
||||
|
||||
// Span mode
|
||||
var spanOutput = new double[100];
|
||||
Vf.Batch(close, volume, spanOutput, DefaultPeriod);
|
||||
|
||||
// Compare all modes (last 50 values to avoid warmup differences)
|
||||
for (int i = 50; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(streamingResult[i].Value, batchResult[i].Value, Tolerance);
|
||||
Assert.Equal(streamingResult[i].Value, spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static TBarSeries GenerateTestBarSeries(int count)
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var gbm = new GBM(seed: 42);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
bars.Add(gbm.Next());
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Vf: Mathematical property validation tests
|
||||
// Volume Force is a QuanTAlib-specific indicator combining price change with volume
|
||||
// and EMA smoothing. No standard external library equivalents. Validation uses
|
||||
// mathematical property testing.
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
public class VfValidationTests
|
||||
{
|
||||
private const int DefaultPeriod = 14;
|
||||
private const int TestDataLength = 500;
|
||||
|
||||
[Fact]
|
||||
public void Vf_Output_IsFiniteForGbmData()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = vf.Update(bars[i], isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Vf output must be finite at bar {i}, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_FirstBar_ReturnsZero()
|
||||
{
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 101, 99, 100, 1000);
|
||||
|
||||
var result = vf.Update(bar, isNew: true);
|
||||
|
||||
// First bar has no previous close, so raw VF = 0
|
||||
Assert.Equal(0.0, result.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_RisingPrice_PositiveForce()
|
||||
{
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
|
||||
// First bar
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vf.Update(bar1, isNew: true);
|
||||
|
||||
// Rising price: positive raw VF
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 1000);
|
||||
var result = vf.Update(bar2, isNew: true);
|
||||
|
||||
// rawVF = (105 - 100) * 1000 = 5000, EMA of that should be positive
|
||||
Assert.True(result.Value > 0,
|
||||
$"Vf should be positive for rising price, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_FallingPrice_NegativeForce()
|
||||
{
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
|
||||
// First bar
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vf.Update(bar1, isNew: true);
|
||||
|
||||
// Falling price: negative raw VF
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 95, 100, 95, 95, 1000);
|
||||
var result = vf.Update(bar2, isNew: true);
|
||||
|
||||
// rawVF = (95 - 100) * 1000 = -5000, EMA of that should be negative
|
||||
Assert.True(result.Value < 0,
|
||||
$"Vf should be negative for falling price, got {result.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_ConstantPrice_ZeroForce()
|
||||
{
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
|
||||
// Feed constant-price bars
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
100, 100, 100, 100, 1000);
|
||||
vf.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
// No price change → raw VF = 0 each bar → EMA converges to 0
|
||||
Assert.Equal(0.0, vf.Last.Value, precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_HighVolume_AmplifiesForce()
|
||||
{
|
||||
// Low volume
|
||||
var vfLow = new Vf(DefaultPeriod);
|
||||
var bar1Low = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 100);
|
||||
vfLow.Update(bar1Low, isNew: true);
|
||||
var bar2Low = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 100);
|
||||
vfLow.Update(bar2Low, isNew: true);
|
||||
|
||||
// High volume
|
||||
var vfHigh = new Vf(DefaultPeriod);
|
||||
var bar1High = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10000);
|
||||
vfHigh.Update(bar1High, isNew: true);
|
||||
var bar2High = new TBar(DateTime.UtcNow.AddMinutes(1), 105, 105, 100, 105, 10000);
|
||||
vfHigh.Update(bar2High, isNew: true);
|
||||
|
||||
// Higher volume should produce larger absolute VF
|
||||
Assert.True(Math.Abs(vfHigh.Last.Value) > Math.Abs(vfLow.Last.Value),
|
||||
$"High volume VF ({vfHigh.Last.Value}) should exceed low volume VF ({vfLow.Last.Value})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_BatchAndStreaming_ProduceSameResults()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Batch
|
||||
var batchResults = Vf.Batch(bars, DefaultPeriod);
|
||||
|
||||
// Streaming
|
||||
var streamVf = new Vf(DefaultPeriod);
|
||||
var streamResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = streamVf.Update(bars[i], isNew: true);
|
||||
streamResults[i] = result.Value;
|
||||
}
|
||||
|
||||
Assert.Equal(batchResults.Count, bars.Count);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResults.Values[i], streamResults[i], precision: 8);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_SpanAndStreaming_ProduceSameResults()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(TestDataLength, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var spanOutput = new double[bars.Count];
|
||||
|
||||
Vf.Batch(bars.Close.Values, bars.Volume.Values, spanOutput, DefaultPeriod);
|
||||
|
||||
// Streaming
|
||||
var streamVf = new Vf(DefaultPeriod);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = streamVf.Update(bars[i], isNew: true);
|
||||
Assert.Equal(spanOutput[i], result.Value, precision: 8);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_DifferentPeriods_ProduceDifferentSmoothing()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var vf3 = new Vf(period: 3);
|
||||
var vf50 = new Vf(period: 50);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vf3.Update(bars[i], isNew: true);
|
||||
vf50.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.NotEqual(vf3.Last.Value, vf50.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vf_BarCorrection_IsNewFalse_RestoresState()
|
||||
{
|
||||
var bars = new GBM(sigma: 0.5, seed: 123).Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var vf = new Vf(DefaultPeriod);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
vf.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
vf.Update(bars[30], isNew: true);
|
||||
double afterNew = vf.Last.Value;
|
||||
|
||||
vf.Update(bars[30], isNew: false);
|
||||
double afterCorrection = vf.Last.Value;
|
||||
|
||||
Assert.Equal(afterNew, afterCorrection, precision: 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user