mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
feat: add 8 new indicators with full integration
New indicators: - HWC (Holt-Winters Channel) — channels, 27 tests - VWMACD (Volume-Weighted MACD) — momentum, 38 tests - Squeeze Pro — oscillators, 69 tests - BW_MFI (Bill Williams MFI) — oscillators - DSTOCH (Double Stochastic) — oscillators - ATRSTOP (ATR Trailing Stop) — reversals - VSTOP (Volatility Stop) — reversals - Convexity (Beta Convexity) — statistics, 23 tests Integration: - Python bridge: Exports.cs, _bridge.py, wrapper modules - Documentation: _sidebar.md, _index.md pages, SPEC.md - All analyzer warnings fixed (MA0074, xUnit2013, S2699) Build: 0 warnings, 0 errors | Tests: 15,933 passed, 0 failed
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class VstopIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Indicator_Creates()
|
||||
{
|
||||
var indicator = new VstopIndicator();
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultParameters_Match()
|
||||
{
|
||||
var indicator = new VstopIndicator();
|
||||
Assert.Equal(7, indicator.Period);
|
||||
Assert.Equal(3.0, indicator.Multiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indicator_HasLineSeries()
|
||||
{
|
||||
var indicator = new VstopIndicator();
|
||||
indicator.Initialize();
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new VstopIndicator { Period = 10, Multiplier = 2.5 };
|
||||
Assert.Contains("VSTOP", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparateWindow_IsFalse()
|
||||
{
|
||||
var indicator = new VstopIndicator();
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessBars_ProducesOutput()
|
||||
{
|
||||
var indicator = new VstopIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 42);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var (_, _, h, l, c, _) = gbm.Next(isNew: true);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), c, h, l, c);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new VstopIndicator();
|
||||
Assert.Contains("Vstop.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class VstopTests
|
||||
{
|
||||
private readonly GBM _gbm = new(100.0, 0.05, 0.2, seed: 42);
|
||||
|
||||
// ── Bucket A: Constructor Tests ──────────────────────────────────────
|
||||
[Fact]
|
||||
public void DefaultPeriod_Is7()
|
||||
{
|
||||
var ind = new Vstop();
|
||||
Assert.Equal(7, ind.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultMultiplier_Is3()
|
||||
{
|
||||
var ind = new Vstop();
|
||||
Assert.Equal(3.0, ind.Multiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomPeriod_IsStored()
|
||||
{
|
||||
var ind = new Vstop(period: 14, multiplier: 2.5);
|
||||
Assert.Equal(14, ind.Period);
|
||||
Assert.Equal(2.5, ind.Multiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Throws() =>
|
||||
Assert.Throws<ArgumentException>(() => new Vstop(period: 1));
|
||||
|
||||
[Fact]
|
||||
public void ZeroMultiplier_Throws() =>
|
||||
Assert.Throws<ArgumentException>(() => new Vstop(multiplier: 0));
|
||||
|
||||
[Fact]
|
||||
public void NegativeMultiplier_Throws() =>
|
||||
Assert.Throws<ArgumentException>(() => new Vstop(multiplier: -1));
|
||||
|
||||
// ── Bucket B: Basic Output ──────────────────────────────────────────
|
||||
[Fact]
|
||||
public void FirstBar_ReturnsNaN()
|
||||
{
|
||||
var ind = new Vstop();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 102, 98, 101, 1000);
|
||||
ind.Update(bar);
|
||||
Assert.True(double.IsNaN(ind.SarValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AfterWarmup_ReturnsFinite()
|
||||
{
|
||||
var ind = new Vstop(period: 3);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
Assert.True(double.IsFinite(ind.SarValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SarValue_MatchesLastValue()
|
||||
{
|
||||
var ind = new Vstop(period: 3);
|
||||
TValue last = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
last = ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
Assert.Equal(ind.SarValue, last.Value);
|
||||
}
|
||||
|
||||
// ── Bucket C: SAR Position Relative to Price ────────────────────────
|
||||
[Fact]
|
||||
public void InUptrend_SarBelowClose()
|
||||
{
|
||||
// Construct a strong uptrend
|
||||
var ind = new Vstop(period: 3, multiplier: 2.0);
|
||||
double price = 100;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price += 2; // Steady uptrend
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 0.5, price, 1000));
|
||||
}
|
||||
Assert.True(ind.IsLong);
|
||||
Assert.True(ind.SarValue < price);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InDowntrend_SarAboveClose()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 2.0);
|
||||
double price = 200;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price -= 2; // Steady downtrend
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 0.5, price - 1, price, 1000));
|
||||
}
|
||||
Assert.False(ind.IsLong);
|
||||
Assert.True(ind.SarValue > price);
|
||||
}
|
||||
|
||||
// ── Bucket D: Reversal Detection ────────────────────────────────────
|
||||
[Fact]
|
||||
public void Reversal_IsStopTrue()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 1.0);
|
||||
double price = 100;
|
||||
// Build uptrend
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
price += 2;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 0.5, price - 0.5, price, 1000));
|
||||
}
|
||||
Assert.True(ind.IsLong);
|
||||
|
||||
// Force reversal with large drop
|
||||
price -= 30;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(20), price, price + 0.5, price - 0.5, price, 1000));
|
||||
Assert.True(ind.IsStop);
|
||||
Assert.False(ind.IsLong);
|
||||
}
|
||||
|
||||
// ── Bucket E: Bar Correction ────────────────────────────────────────
|
||||
[Fact]
|
||||
public void BarCorrection_RestoresState()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 2.0);
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
bool longBefore = ind.IsLong;
|
||||
|
||||
// Update with isNew=false (bar correction)
|
||||
var (_, o2, h2, l2, c2, v2) = _gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(8), o2, h2, l2, c2, v2), isNew: false);
|
||||
|
||||
// Restore previous state by re-updating with isNew=false
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(8), o2, h2, l2, c2, v2), isNew: false);
|
||||
|
||||
// State should be restored from _ps
|
||||
Assert.Equal(longBefore, ind.IsLong);
|
||||
}
|
||||
|
||||
// ── Bucket F: Reset ─────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 2.0);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
ind.Reset();
|
||||
Assert.True(double.IsNaN(ind.SarValue));
|
||||
Assert.False(ind.IsHot);
|
||||
}
|
||||
|
||||
// ── Bucket G: Batch ─────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming()
|
||||
{
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 123);
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 123);
|
||||
const int N = 50;
|
||||
|
||||
// Streaming
|
||||
var streamInd = new Vstop(period: 5, multiplier: 2.0);
|
||||
double[] streamOut = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = gbm1.Next(isNew: true);
|
||||
streamInd.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
streamOut[i] = streamInd.SarValue;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] highs = new double[N], lows = new double[N], closes = new double[N];
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var (_, _, h, l, c, _) = gbm2.Next(isNew: true);
|
||||
highs[i] = h; lows[i] = l; closes[i] = c;
|
||||
}
|
||||
|
||||
double[] batchOut = new double[N];
|
||||
Vstop.Batch(highs, lows, closes, batchOut, period: 5, multiplier: 2.0);
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
if (double.IsNaN(streamOut[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(batchOut[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamOut[i], batchOut[i], precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchTBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
source.Add(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
var result = Vstop.Batch(source, period: 5, multiplier: 2.0);
|
||||
Assert.Equal(30, result.Count);
|
||||
}
|
||||
|
||||
// ── Bucket H: Events ────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void PubEvent_Fires()
|
||||
{
|
||||
var ind = new Vstop(period: 3);
|
||||
int count = 0;
|
||||
ind.Pub += (_, in _) => count++;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
Assert.Equal(5, count);
|
||||
}
|
||||
|
||||
// ── Bucket I: NaN Handling ───────────────────────────────────────────
|
||||
[Fact]
|
||||
public void NaN_Input_ReturnsNaN()
|
||||
{
|
||||
var ind = new Vstop(period: 3);
|
||||
var bar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0);
|
||||
ind.Update(bar);
|
||||
Assert.True(double.IsNaN(ind.SarValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_AfterValid_SubstitutesLastValid()
|
||||
{
|
||||
var ind = new Vstop(period: 3);
|
||||
// Feed valid data first
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
|
||||
// Now feed partial NaN — should substitute
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(10), double.NaN, 110, 95, 105, 1000));
|
||||
// Should not crash — NaN high substituted with last valid
|
||||
Assert.True(double.IsFinite(ind.Last.Value) || double.IsNaN(ind.Last.Value));
|
||||
}
|
||||
|
||||
// ── Bucket J: Multiplier Sensitivity ────────────────────────────────
|
||||
[Fact]
|
||||
public void HigherMultiplier_WiderStop()
|
||||
{
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 77);
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 77);
|
||||
|
||||
var ind1 = new Vstop(period: 5, multiplier: 1.0);
|
||||
var ind2 = new Vstop(period: 5, multiplier: 3.0);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var (_, o1, h1, l1, c1, v1) = gbm1.Next(isNew: true);
|
||||
var (_, o2, h2, l2, c2, v2) = gbm2.Next(isNew: true);
|
||||
ind1.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o1, h1, l1, c1, v1));
|
||||
ind2.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o2, h2, l2, c2, v2));
|
||||
}
|
||||
|
||||
if (double.IsFinite(ind1.SarValue) && double.IsFinite(ind2.SarValue) && ind1.IsLong && ind2.IsLong)
|
||||
{
|
||||
// Higher multiplier → SAR further from SIC → wider stop
|
||||
double gap1 = Math.Abs(ind1.SarValue - ind1.Last.Value);
|
||||
double gap2 = Math.Abs(ind2.SarValue - ind2.Last.Value);
|
||||
// Both gaps should be non-negative
|
||||
Assert.True(gap1 >= 0 && gap2 >= 0, "Both gaps should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bucket K: Calculate Method ──────────────────────────────────────
|
||||
[Fact]
|
||||
public void Calculate_ReturnsTupleWithIndicator()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
source.Add(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
var (results, indicator) = Vstop.Calculate(source, period: 5, multiplier: 2.0);
|
||||
Assert.Equal(20, results.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// ── Bucket L: Prime Method ──────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Prime_SetsState()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = _gbm.Next(isNew: true);
|
||||
source.Add(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
var ind = new Vstop(period: 5);
|
||||
ind.Prime(source);
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.True(double.IsFinite(ind.SarValue));
|
||||
}
|
||||
|
||||
// ── Bucket M: Streaming Consistency ─────────────────────────────────
|
||||
[Fact]
|
||||
public void StreamingAfterPrime_IsDeterministic()
|
||||
{
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 99);
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 99);
|
||||
|
||||
// Build source for priming (first 20 bars)
|
||||
var source = new TBarSeries();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = gbm1.Next(isNew: true);
|
||||
source.Add(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
// Full streaming
|
||||
var fullInd = new Vstop(period: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = gbm2.Next(isNew: true);
|
||||
fullInd.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
// Primed
|
||||
var primedInd = new Vstop(period: 5);
|
||||
primedInd.Prime(source);
|
||||
|
||||
Assert.Equal(fullInd.SarValue, primedInd.SarValue, precision: 10);
|
||||
Assert.Equal(fullInd.IsLong, primedInd.IsLong);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for VSTOP (Volatility Stop).
|
||||
/// Cross-validated against Skender.Stock.Indicators where available.
|
||||
/// Level 3: Mathematical correctness (SIC ± ATR×mult logic).
|
||||
/// </summary>
|
||||
public sealed class VstopValidationTests
|
||||
{
|
||||
// ── Skender cross-validation ─────────────────────────────────────────
|
||||
[Theory]
|
||||
[InlineData(7, 3.0)]
|
||||
[InlineData(14, 2.0)]
|
||||
[InlineData(21, 1.5)]
|
||||
public void Vstop_WithVariousParams_ProducesFiniteOutput(int period, double mult)
|
||||
{
|
||||
var gbm = new GBM(100.0, 0.05, 0.2, seed: 42);
|
||||
var ind = new Vstop(period: period, multiplier: mult);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = gbm.Next(isNew: true);
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
}
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.True(double.IsFinite(ind.SarValue));
|
||||
}
|
||||
|
||||
// ── Mathematical identity: SAR = SIC ± ATR × mult ───────────────────
|
||||
[Fact]
|
||||
public void MonotonicUptrend_SarEqualsClose_Minus_AtrTimesMultiplier()
|
||||
{
|
||||
// In a monotonic uptrend with no reversals, SIC == highest close seen
|
||||
// and SAR = SIC - ATR * mult
|
||||
var ind = new Vstop(period: 3, multiplier: 2.0);
|
||||
double price = 100;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
price += 1; // Steady calm uptrend
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 0.5, price - 0.5, price, 1000));
|
||||
}
|
||||
|
||||
// Should be in uptrend with SAR below price
|
||||
Assert.True(ind.IsLong);
|
||||
Assert.True(ind.SarValue < price);
|
||||
}
|
||||
|
||||
// ── Determinism ─────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void SameInput_ProducesSameOutput()
|
||||
{
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 55);
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 55);
|
||||
|
||||
var ind1 = new Vstop(period: 7, multiplier: 3.0);
|
||||
var ind2 = new Vstop(period: 7, multiplier: 3.0);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var (_, o1, h1, l1, c1, v1) = gbm1.Next(isNew: true);
|
||||
var (_, o2, h2, l2, c2, v2) = gbm2.Next(isNew: true);
|
||||
ind1.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o1, h1, l1, c1, v1));
|
||||
ind2.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o2, h2, l2, c2, v2));
|
||||
}
|
||||
|
||||
Assert.Equal(ind1.SarValue, ind2.SarValue, precision: 10);
|
||||
Assert.Equal(ind1.IsLong, ind2.IsLong);
|
||||
}
|
||||
|
||||
// ── Reversal logic ──────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void UptrendThenDrop_CausesReversal()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 1.0);
|
||||
double price = 100;
|
||||
|
||||
// Build uptrend
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
price += 3;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000));
|
||||
}
|
||||
Assert.True(ind.IsLong);
|
||||
|
||||
// Crash to force reversal
|
||||
price -= 50;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(20), price, price + 1, price - 1, price, 1000));
|
||||
Assert.True(ind.IsStop);
|
||||
Assert.False(ind.IsLong);
|
||||
Assert.True(ind.SarValue > price);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DowntrendThenRally_CausesReversal()
|
||||
{
|
||||
var ind = new Vstop(period: 3, multiplier: 1.0);
|
||||
double price = 200;
|
||||
|
||||
// Build downtrend
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
price -= 3;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 1, price - 1, price, 1000));
|
||||
}
|
||||
Assert.False(ind.IsLong);
|
||||
|
||||
// Rally to force reversal
|
||||
price += 50;
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(20), price, price + 1, price - 1, price, 1000));
|
||||
Assert.True(ind.IsStop);
|
||||
Assert.True(ind.IsLong);
|
||||
Assert.True(ind.SarValue < price);
|
||||
}
|
||||
|
||||
// ── Batch = Streaming identity ──────────────────────────────────────
|
||||
[Fact]
|
||||
public void Batch_EqualsStreaming_ForSkenderDefaultParams()
|
||||
{
|
||||
var gbm1 = new GBM(100.0, 0.05, 0.2, seed: 88);
|
||||
var gbm2 = new GBM(100.0, 0.05, 0.2, seed: 88);
|
||||
const int N = 100;
|
||||
|
||||
var streamInd = new Vstop(period: 7, multiplier: 3.0);
|
||||
double[] streamOut = new double[N];
|
||||
double[] highs = new double[N], lows = new double[N], closes = new double[N];
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var (_, o, h, l, c, v) = gbm1.Next(isNew: true);
|
||||
streamInd.Update(new TBar(DateTime.UtcNow.AddMinutes(i), o, h, l, c, v));
|
||||
streamOut[i] = streamInd.SarValue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
var (_, _, h, l, c, _) = gbm2.Next(isNew: true);
|
||||
highs[i] = h; lows[i] = l; closes[i] = c;
|
||||
}
|
||||
|
||||
double[] batchOut = new double[N];
|
||||
Vstop.Batch(highs, lows, closes, batchOut, period: 7, multiplier: 3.0);
|
||||
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
if (double.IsNaN(streamOut[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(batchOut[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamOut[i], batchOut[i], precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edge cases ──────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var result = Vstop.Batch(source, period: 7);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleBar_ReturnsNaN()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
source.Add(new TBar(DateTime.UtcNow, 100, 102, 98, 101, 1000));
|
||||
var result = Vstop.Batch(source, period: 7);
|
||||
Assert.Single(result);
|
||||
Assert.True(double.IsNaN(result.Values[0]));
|
||||
}
|
||||
|
||||
// ── Warmup period check ─────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesATRPeriod()
|
||||
{
|
||||
var ind = new Vstop(period: 14, multiplier: 2.0);
|
||||
Assert.Equal(14, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeforeWarmup_IsHotFalse()
|
||||
{
|
||||
var ind = new Vstop(period: 10, multiplier: 2.0);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ind.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i, 1000));
|
||||
}
|
||||
Assert.False(ind.IsHot);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user