Add TRAMA implementation and comprehensive tests

- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic.
- Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks.
- Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations).
- Enhanced documentation for TRAMA, including performance profiles and quality metrics.
- Updated workspace configuration by removing unnecessary folder references.
This commit is contained in:
Miha Kralj
2026-02-21 20:45:38 -08:00
parent 90d5638008
commit 7253f61299
199 changed files with 29577 additions and 234 deletions
@@ -0,0 +1,216 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class AdxvmaIndicatorTests
{
[Fact]
public void AdxvmaIndicator_Constructor_SetsDefaults()
{
var indicator = new AdxvmaIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("ADXVMA - ADX Variable Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void AdxvmaIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new AdxvmaIndicator { Period = 14 };
Assert.Equal(0, AdxvmaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void AdxvmaIndicator_ShortName_IncludesPeriod()
{
var indicator = new AdxvmaIndicator { Period = 20 };
Assert.Contains("ADXVMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void AdxvmaIndicator_Initialize_CreatesInternalAdxvma()
{
var indicator = new AdxvmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void AdxvmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AdxvmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void AdxvmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AdxvmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 110, 98, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void AdxvmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new AdxvmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void AdxvmaIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new AdxvmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
(double o, double h, double l, double c)[] bars =
{
(100, 102, 98, 101),
(101, 103, 99, 102),
(102, 104, 100, 103),
(103, 108, 97, 105),
(105, 112, 100, 110),
(110, 115, 105, 108),
(108, 110, 106, 109),
(109, 111, 107, 110),
(110, 112, 108, 111),
(111, 113, 109, 112)
};
foreach (var (o, h, l, c) in bars)
{
indicator.HistoricalData.AddBar(now, o, h, l, c);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < bars.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(bars.Length - 1 - i)));
}
// ADXVMA should be smoothing the values
double lastAdxvma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastAdxvma >= 95 && lastAdxvma <= 120);
}
[Fact]
public void AdxvmaIndicator_Parameters_CanBeChanged()
{
var indicator = new AdxvmaIndicator { Period = 10 };
Assert.Equal(10, indicator.Period);
indicator.Period = 30;
Assert.Equal(30, indicator.Period);
}
[Fact]
public void AdxvmaIndicator_LongPeriod_Works()
{
var indicator = new AdxvmaIndicator { Period = 28 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 200; i++)
{
double price = 100 + (i * 0.1) + Math.Sin(i * 0.1) * 2;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(lastValue));
Assert.True(lastValue > 100 && lastValue < 130);
}
[Fact]
public void AdxvmaIndicator_ShortPeriod_Works()
{
var indicator = new AdxvmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
(double o, double h, double l, double c)[] bars =
{
(100, 103, 97, 102),
(102, 106, 100, 105),
(105, 108, 102, 104),
(104, 107, 101, 106),
(106, 110, 104, 108),
(108, 112, 105, 110)
};
foreach (var (o, h, l, c) in bars)
{
indicator.HistoricalData.AddBar(now, o, h, l, c);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
for (int i = 0; i < bars.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(bars.Length - 1 - i)));
}
}
[Fact]
public void AdxvmaIndicator_UsesOhlcForTrueRange()
{
var indicator = new AdxvmaIndicator { Period = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add bars where High-Low range differs significantly from Close-to-Close
indicator.HistoricalData.AddBar(now, 100, 110, 90, 100);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(1)));
}
}
+61
View File
@@ -0,0 +1,61 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// Quantower adapter for ADXVMA (ADX Variable Moving Average).
/// ADXVMA requires OHLC data for TR/DM/ADX calculation.
/// </summary>
[SkipLocalsInit]
public class AdxvmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 500, 1, 0)]
public int Period { get; set; } = 14;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Adxvma ma = null!;
protected LineSeries Series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"ADXVMA {Period}";
public AdxvmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "ADXVMA - ADX Variable Moving Average";
Description = "Adaptive IIR filter that uses ADX as its smoothing constant";
Series = new LineSeries(name: $"ADXVMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Adxvma(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
// ADXVMA uses OHLC for True Range and Directional Movement calculation
var bar = new TBar(
item.TimeLeft.Ticks,
item[PriceType.Open],
item[PriceType.High],
item[PriceType.Low],
item[PriceType.Close],
item[PriceType.Volume]);
TValue result = ma.Update(bar, isNew: args.IsNewBar());
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
}
}
+672
View File
@@ -0,0 +1,672 @@
namespace QuanTAlib.Tests;
public class AdxvmaTests
{
// ==================== A) Constructor Validation ====================
[Fact]
public void Adxvma_Constructor_ThrowsOnZeroPeriod()
{
Assert.Throws<ArgumentException>(() => new Adxvma(period: 0));
}
[Fact]
public void Adxvma_Constructor_ThrowsOnNegativePeriod()
{
Assert.Throws<ArgumentException>(() => new Adxvma(period: -1));
}
[Fact]
public void Adxvma_Constructor_AcceptsValidPeriod()
{
var adxvma = new Adxvma(period: 14);
Assert.NotNull(adxvma);
Assert.Equal("Adxvma(14)", adxvma.Name);
}
[Fact]
public void Adxvma_Constructor_PeriodOneIsValid()
{
var adxvma = new Adxvma(period: 1);
Assert.NotNull(adxvma);
Assert.Equal("Adxvma(1)", adxvma.Name);
}
[Fact]
public void Adxvma_Constructor_DefaultPeriodIs14()
{
var adxvma = new Adxvma();
Assert.Equal("Adxvma(14)", adxvma.Name);
}
// ==================== B) Basic Calculation ====================
[Fact]
public void Adxvma_Calc_ReturnsValue()
{
var adxvma = new Adxvma();
Assert.Equal(0, adxvma.Last.Value);
TValue result = adxvma.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, adxvma.Last.Value);
}
[Fact]
public void Adxvma_TBar_ReturnsValue()
{
var adxvma = new Adxvma();
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
TValue result = adxvma.Update(bar, isNew: true);
Assert.True(double.IsFinite(result.Value));
Assert.Equal(result.Value, adxvma.Last.Value);
}
[Fact]
public void Adxvma_TBar_UsesOHLC_ForTrueRange()
{
var adxvma = new Adxvma();
var time = DateTime.UtcNow;
// Feed bars with varying volatility
for (int i = 0; i < 100; i++)
{
var bar = new TBar(time.AddMinutes(i), 100, 105, 95, 100, 1000);
adxvma.Update(bar, isNew: true);
}
Assert.True(double.IsFinite(adxvma.Last.Value));
Assert.True(adxvma.IsHot, "Expected IsHot=true after 100 bars");
}
[Fact]
public void Adxvma_Properties_Accessible()
{
var adxvma = new Adxvma();
Assert.Equal(0, adxvma.Last.Value);
Assert.False(adxvma.IsHot);
adxvma.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, adxvma.Last.Value);
}
// ==================== C) State + Bar Correction ====================
[Fact]
public void Adxvma_IsNew_True_AdvancesState()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = adxvma.Last.Value;
adxvma.Update(new TValue(DateTime.UtcNow, 105), isNew: true);
double value2 = adxvma.Last.Value;
Assert.NotEqual(value1, value2);
}
[Fact]
public void Adxvma_IsNew_False_UpdatesValue()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = adxvma.Last.Value;
adxvma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = adxvma.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Adxvma_IterativeCorrections_RestoreToOriginalState()
{
var adxvma = new Adxvma();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
adxvma.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double afterTen = adxvma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
adxvma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue restored = adxvma.Update(tenthInput, isNew: false);
// Should match the original state after 10 values
Assert.Equal(afterTen, restored.Value, 1e-10);
}
[Fact]
public void Adxvma_TBar_BarCorrection_Works()
{
var adxvma = new Adxvma();
var time = DateTime.UtcNow;
// Feed some history
for (int i = 0; i < 5; i++)
{
var bar = new TBar(time.AddMinutes(i), 100, 105, 95, 102, 1000);
adxvma.Update(bar, isNew: true);
}
// New bar
var newBar = new TBar(time.AddMinutes(5), 102, 108, 99, 106, 1200);
adxvma.Update(newBar, isNew: true);
double afterNewBar = adxvma.Last.Value;
// Correction with different bar
var corrBar = new TBar(time.AddMinutes(5), 103, 107, 100, 104, 1100);
adxvma.Update(corrBar, isNew: false);
double afterCorrection = adxvma.Last.Value;
// Different bar data should produce different result
Assert.NotEqual(afterNewBar, afterCorrection);
// Correction with original bar should restore state
adxvma.Update(newBar, isNew: false);
Assert.Equal(afterNewBar, adxvma.Last.Value, 1e-10);
}
[Fact]
public void Adxvma_Reset_ClearsState()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = adxvma.Last.Value;
adxvma.Reset();
Assert.Equal(0, adxvma.Last.Value);
// After reset, should accept new values
adxvma.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, adxvma.Last.Value);
Assert.NotEqual(valueBefore, adxvma.Last.Value);
}
// ==================== D) Warmup / Convergence ====================
[Fact]
public void Adxvma_IsHot_BecomesTrueAfterWarmup()
{
var adxvma = new Adxvma(period: 14);
// IsHot requires barCount >= period * 2 = 28
Assert.False(adxvma.IsHot);
int steps = 0;
while (!adxvma.IsHot && steps < 1000)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(steps), 100, 105, 95, 100, 1000);
adxvma.Update(bar, isNew: true);
steps++;
}
Assert.True(adxvma.IsHot);
Assert.True(steps <= 28, $"Expected IsHot within 28 bars but took {steps}");
}
[Fact]
public void Adxvma_WarmupPeriod_EqualsDoubleThePeriod()
{
var adxvma = new Adxvma(period: 10);
Assert.Equal(20, adxvma.WarmupPeriod);
var adxvma2 = new Adxvma(period: 14);
Assert.Equal(28, adxvma2.WarmupPeriod);
}
[Fact]
public void Adxvma_ConstantOHLC_ConvergesToClose()
{
var adxvma = new Adxvma();
var time = DateTime.UtcNow;
// Feed constant OHLC bars
for (int i = 0; i < 200; i++)
{
var bar = new TBar(time.AddMinutes(i), 100, 100, 100, 100, 1000);
adxvma.Update(bar, isNew: true);
}
// With constant input, should converge to close
Assert.Equal(100.0, adxvma.Last.Value, 1e-9);
}
[Fact]
public void Adxvma_ConstantTValue_ConvergesToInput()
{
var adxvma = new Adxvma();
// Feed constant values via TValue (synthetic bar: O=H=L=C, TR=0)
for (int i = 0; i < 200; i++)
{
adxvma.Update(new TValue(DateTime.UtcNow, 42.5));
}
// With constant input, ADXVMA should converge to input
Assert.Equal(42.5, adxvma.Last.Value, 1e-9);
}
// ==================== E) Robustness ====================
[Fact]
public void Adxvma_NaN_Input_UsesLastValidValue()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, 110));
var resultAfterNaN = adxvma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Adxvma_Infinity_Input_UsesLastValidValue()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, 110));
var resultAfterPosInf = adxvma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
var resultAfterNegInf = adxvma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
}
[Fact]
public void Adxvma_MultipleNaN_ContinuesWithLastValid()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, 110));
adxvma.Update(new TValue(DateTime.UtcNow, 120));
var r1 = adxvma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = adxvma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = adxvma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
}
[Fact]
public void Adxvma_BatchCalc_HandlesNaN()
{
var adxvma = new Adxvma();
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 100);
series.Add(DateTime.UtcNow.Ticks + 1, 110);
series.Add(DateTime.UtcNow.Ticks + 2, double.NaN);
series.Add(DateTime.UtcNow.Ticks + 3, 120);
series.Add(DateTime.UtcNow.Ticks + 4, double.PositiveInfinity);
series.Add(DateTime.UtcNow.Ticks + 5, 130);
var results = adxvma.Update(series);
foreach (var result in results)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Adxvma_Reset_ClearsLastValidValue()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Update(new TValue(DateTime.UtcNow, double.NaN));
adxvma.Reset();
// After reset, first valid value should establish new baseline
var result = adxvma.Update(new TValue(DateTime.UtcNow, 50));
Assert.Equal(50.0, result.Value, 1e-10);
}
// ==================== F) Consistency (All Modes Match) ====================
[Fact]
public void Adxvma_BatchCalc_MatchesIterativeCalc()
{
var adxvmaIterative = new Adxvma();
var adxvmaBatch = new Adxvma();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
Assert.True(series.Count > 0);
// Calculate iteratively
var iterativeResults = new TSeries();
foreach (var item in series)
{
iterativeResults.Add(adxvmaIterative.Update(item));
}
// Calculate batch
var batchResults = adxvmaBatch.Update(series);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
}
}
[Fact]
public void Adxvma_TBarSeries_MatchesIterativeTBar()
{
var adxvmaIterative = new Adxvma();
var adxvmaBatch = new Adxvma();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Calculate iteratively with TBar
var iterativeResults = new TSeries();
foreach (var bar in bars)
{
iterativeResults.Add(adxvmaIterative.Update(bar, isNew: true));
}
// Calculate batch with TBarSeries
var batchResults = adxvmaBatch.Update(bars);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
}
}
[Fact]
public void Adxvma_AllModes_ProduceSameResult()
{
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Adxvma.Batch(series);
double expected = batchSeries.Last.Value;
// 2. Streaming Mode
var streamingInd = new Adxvma();
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 3. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Adxvma(pubSource);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, eventingResult, precision: 9);
}
// ==================== G) TBar-specific Tests ====================
[Fact]
public void Adxvma_TBarSeries_BatchWorks()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Adxvma.Batch(bars);
Assert.Equal(200, result.Count);
Assert.All(result, tv => Assert.True(double.IsFinite(tv.Value)));
}
[Fact]
public void Adxvma_TValue_SyntheticBar_ProducesValidOutput()
{
// TValue creates synthetic bar: O=H=L=C → TR=0 → ADX→0 → sc→0 → flat line
var adxvma = new Adxvma();
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
var result = adxvma.Update(new TValue(bar.Time, bar.Close), isNew: true);
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Adxvma_StrongTrend_HighADX_TracksPrice()
{
var adxvma = new Adxvma(period: 14);
var time = DateTime.UtcNow;
// Feed strong uptrend bars (large +DM consistently)
for (int i = 0; i < 50; i++)
{
double price = 100 + i * 2;
var bar = new TBar(time.AddMinutes(i), price, price + 1, price - 0.5, price + 0.5, 1000);
adxvma.Update(bar, isNew: true);
}
// In a strong trend, ADX is high so sc ≈ 1, ADXVMA should track price closely
double adxvmaValue = adxvma.Last.Value;
// Should be within reasonable proximity of recent prices
Assert.True(adxvmaValue > 100, $"ADXVMA ({adxvmaValue}) should be well above 100 in a strong uptrend");
}
[Fact]
public void Adxvma_Calculate_TBarSeries_ReturnsIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Adxvma.Calculate(bars);
Assert.Equal(200, results.Count);
Assert.NotNull(indicator);
Assert.True(indicator.IsHot);
Assert.Equal(results.Last.Value, indicator.Last.Value, 1e-10);
}
[Fact]
public void Adxvma_Calculate_TSeries_ReturnsIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var (results, indicator) = Adxvma.Calculate(series);
Assert.Equal(200, results.Count);
Assert.NotNull(indicator);
Assert.True(indicator.IsHot);
Assert.Equal(results.Last.Value, indicator.Last.Value, 1e-10);
}
// ==================== H) Chainability ====================
[Fact]
public void Adxvma_Chainability_Works()
{
var source = new TSeries();
var adxvma = new Adxvma(source);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, adxvma.Last.Value, 1e-10);
}
// ==================== Prime Tests ====================
[Fact]
public void Adxvma_Prime_SetsStateCorrectly()
{
var adxvma = new Adxvma();
double[] history = [10, 20, 30, 40, 50];
adxvma.Prime(history);
var verifyAdxvma = new Adxvma();
foreach (var val in history)
{
verifyAdxvma.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyAdxvma.Last.Value, adxvma.Last.Value, 1e-10);
// Verify it continues correctly
adxvma.Update(new TValue(DateTime.UtcNow, 60));
verifyAdxvma.Update(new TValue(DateTime.UtcNow, 60));
Assert.Equal(verifyAdxvma.Last.Value, adxvma.Last.Value, 1e-10);
}
[Fact]
public void Adxvma_Prime_HandlesNaN_InHistory()
{
var adxvma = new Adxvma();
double[] history = [10, 20, double.NaN, 40, 50];
adxvma.Prime(history);
var verifyAdxvma = new Adxvma();
foreach (var val in history)
{
verifyAdxvma.Update(new TValue(DateTime.UtcNow, val));
}
Assert.Equal(verifyAdxvma.Last.Value, adxvma.Last.Value, 1e-10);
}
[Fact]
public void Adxvma_Prime_ThenUpdate_StateWorksCorrectly()
{
var adxvma = new Adxvma();
double[] history = [10, 20, 30, 40, 50];
adxvma.Prime(history);
double afterPrime = adxvma.Last.Value;
// After Prime, isNew=true should advance the state
adxvma.Update(new TValue(DateTime.UtcNow, 60), isNew: true);
double afterNewBar = adxvma.Last.Value;
Assert.NotEqual(afterPrime, afterNewBar);
// isNew=false with different value should recalculate
adxvma.Update(new TValue(DateTime.UtcNow, 70), isNew: false);
double afterCorrection = adxvma.Last.Value;
Assert.NotEqual(afterNewBar, afterCorrection);
// isNew=false with original value should restore
adxvma.Update(new TValue(DateTime.UtcNow, 60), isNew: false);
Assert.Equal(afterNewBar, adxvma.Last.Value, 1e-10);
}
// ==================== Dispose Test ====================
[Fact]
public void Adxvma_Dispose_DoesNotThrow()
{
var adxvma = new Adxvma();
adxvma.Update(new TValue(DateTime.UtcNow, 100));
adxvma.Dispose();
// Should be able to create a new one after dispose
var adxvma2 = new Adxvma();
Assert.NotNull(adxvma2);
}
// ==================== Parameter Variation Tests ====================
[Fact]
public void Adxvma_ParameterVariations_ProduceValidResults()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var adxvma1 = new Adxvma(7);
var adxvma2 = new Adxvma(14);
var adxvma3 = new Adxvma(28);
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
var tv = new TValue(bar.Time, bar.Close);
adxvma1.Update(tv, isNew: true);
adxvma2.Update(tv, isNew: true);
adxvma3.Update(tv, isNew: true);
}
Assert.True(double.IsFinite(adxvma1.Last.Value));
Assert.True(double.IsFinite(adxvma2.Last.Value));
Assert.True(double.IsFinite(adxvma3.Last.Value));
}
[Fact]
public void Adxvma_DifferentPeriods_DifferentSmoothness()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var results7 = Adxvma.Batch(bars, period: 7);
var results28 = Adxvma.Batch(bars, period: 28);
// Both should produce valid values, but longer period should be smoother
Assert.All(results7, tv => Assert.True(double.IsFinite(tv.Value)));
Assert.All(results28, tv => Assert.True(double.IsFinite(tv.Value)));
// Different periods should produce different results
Assert.NotEqual(results7.Last.Value, results28.Last.Value);
}
}
@@ -0,0 +1,301 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for ADXVMA (ADX Variable Moving Average).
/// ADXVMA is a unique adaptive IIR filter using ADX as the smoothing constant.
/// No standard external library implements this exact algorithm, so we validate
/// mathematical properties and internal consistency.
/// </summary>
public class AdxvmaValidationTests
{
private const double Tolerance = 1e-10;
// ==================== Property Validation ====================
/// <summary>
/// When input is constant, ADXVMA output should equal the input value.
/// With constant bars (O=H=L=C), TR=0, DM=0, ADX→0, sc→0.
/// Result should converge to the constant close.
/// </summary>
[Fact]
public void Adxvma_ConstantInput_OutputEqualsInput()
{
var adxvma = new Adxvma();
const double constantValue = 42.5;
for (int i = 0; i < 200; i++)
{
adxvma.Update(new TValue(DateTime.UtcNow, constantValue), isNew: true);
}
Assert.Equal(constantValue, adxvma.Last.Value, Tolerance);
}
/// <summary>
/// With constant OHLC bars, ADXVMA should converge to the close price.
/// </summary>
[Fact]
public void Adxvma_ConstantOHLC_OutputEqualsClose()
{
var adxvma = new Adxvma();
var time = DateTime.UtcNow;
for (int i = 0; i < 200; i++)
{
var bar = new TBar(time.AddMinutes(i), 100, 100, 100, 100, 1000);
adxvma.Update(bar, isNew: true);
}
Assert.Equal(100.0, adxvma.Last.Value, Tolerance);
}
/// <summary>
/// ADXVMA output should always be within the range of input values (no overshoot).
/// </summary>
[Fact]
public void Adxvma_OutputWithinInputRange()
{
var adxvma = new Adxvma();
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 123);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double minInput = double.MaxValue;
double maxInput = double.MinValue;
var outputs = new List<double>();
foreach (var bar in bars)
{
minInput = Math.Min(minInput, bar.Close);
maxInput = Math.Max(maxInput, bar.Close);
var result = adxvma.Update(bar, isNew: true);
outputs.Add(result.Value);
}
// Skip warmup period
var hotOutputs = outputs.Skip(28).ToList();
foreach (var output in hotOutputs)
{
Assert.True(output >= minInput - 1 && output <= maxInput + 1,
$"Output {output} should be within input range [{minInput}, {maxInput}]");
}
}
/// <summary>
/// ADXVMA should be continuous - no sudden jumps in output.
/// </summary>
[Fact]
public void Adxvma_OutputIsContinuous()
{
var adxvma = new Adxvma();
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 456);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var outputs = new List<double>();
foreach (var bar in bars)
{
var result = adxvma.Update(bar, isNew: true);
outputs.Add(result.Value);
}
// After warmup, consecutive outputs should not jump more than input range
for (int i = 29; i < outputs.Count; i++)
{
double delta = Math.Abs(outputs[i] - outputs[i - 1]);
Assert.True(delta < 50,
$"Jump of {delta} at index {i} is too large for a smoothed indicator");
}
}
// ==================== Streaming/Batch Equivalence ====================
/// <summary>
/// Batch and streaming calculations should produce identical results for TBarSeries.
/// </summary>
[Fact]
public void Adxvma_BatchAndStreaming_TBarSeries_Match()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Batch
var batchResults = Adxvma.Batch(bars, period: 14);
// Streaming
var streaming = new Adxvma(period: 14);
var streamResults = new List<double>();
foreach (var bar in bars)
{
streamResults.Add(streaming.Update(bar, isNew: true).Value);
}
Assert.Equal(batchResults.Count, streamResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(batchResults[i].Value, streamResults[i], Tolerance);
}
}
/// <summary>
/// Batch and streaming calculations should produce identical results for TSeries.
/// </summary>
[Fact]
public void Adxvma_BatchAndStreaming_TSeries_Match()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Batch
var batchResults = Adxvma.Batch(series, period: 14);
// Streaming
var streaming = new Adxvma(period: 14);
var streamResults = new List<double>();
foreach (var tv in series)
{
streamResults.Add(streaming.Update(tv, isNew: true).Value);
}
Assert.Equal(batchResults.Count, streamResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(batchResults[i].Value, streamResults[i], Tolerance);
}
}
// ==================== ADX-Specific Behavior ====================
/// <summary>
/// In a strong consistent trend, ADX rises, sc approaches 1, ADXVMA tracks price.
/// </summary>
[Fact]
public void Adxvma_StrongTrend_TracksPrice()
{
var adxvma = new Adxvma(period: 14);
var time = DateTime.UtcNow;
// Strong uptrend: each bar H > prev H, L > prev L, consistent +DM
for (int i = 0; i < 100; i++)
{
double basePrice = 100 + i * 1.5;
var bar = new TBar(time.AddMinutes(i), basePrice, basePrice + 2, basePrice - 1, basePrice + 1, 1000);
adxvma.Update(bar, isNew: true);
}
double adxvmaVal = adxvma.Last.Value;
// In a strong uptrend after 100 bars, ADXVMA should be reasonably close to recent prices
Assert.True(adxvmaVal > 130, $"In strong uptrend, ADXVMA ({adxvmaVal:F2}) should be well above 130");
}
/// <summary>
/// In a choppy/range-bound market, ADX is low, sc approaches 0, ADXVMA barely moves.
/// </summary>
[Fact]
public void Adxvma_ChoppyMarket_FlattensOutput()
{
var adxvma = new Adxvma(period: 14);
var time = DateTime.UtcNow;
// Warm up with some data
for (int i = 0; i < 50; i++)
{
var bar = new TBar(time.AddMinutes(i), 100, 102, 98, 100, 1000);
adxvma.Update(bar, isNew: true);
}
// Feed choppy bars: alternating up/down moves cancel out → ADX stays low
for (int i = 50; i < 150; i++)
{
double price = 100 + Math.Sin(i * 0.5) * 2; // oscillating around 100
var bar = new TBar(time.AddMinutes(i), price, price + 1, price - 1, price, 1000);
adxvma.Update(bar, isNew: true);
}
double choppyValue = adxvma.Last.Value;
// In a choppy market, ADXVMA should stay near the center and not deviate much
Assert.True(Math.Abs(choppyValue - 100) < 10,
$"In choppy market, ADXVMA ({choppyValue:F2}) should stay near 100");
}
// ==================== Different Period Validation ====================
[Theory]
[InlineData(7)]
[InlineData(14)]
[InlineData(21)]
[InlineData(28)]
public void Adxvma_DifferentPeriods_AllProduceValidResults(int period)
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 789);
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = Adxvma.Batch(bars, period: period);
Assert.Equal(300, result.Count);
Assert.All(result, tv => Assert.True(double.IsFinite(tv.Value)));
}
/// <summary>
/// Longer periods should produce smoother output (lower variance in consecutive changes).
/// </summary>
[Fact]
public void Adxvma_LongerPeriod_SmootherOutput()
{
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var results7 = Adxvma.Batch(bars, period: 7);
var results28 = Adxvma.Batch(bars, period: 28);
// Calculate variance of consecutive changes for each
static double ChangeVariance(TSeries s, int skip)
{
double sum = 0;
double sumSq = 0;
int count = 0;
for (int i = skip + 1; i < s.Count; i++)
{
double d = s[i].Value - s[i - 1].Value;
sum += d;
sumSq += d * d;
count++;
}
double mean = sum / count;
return (sumSq / count) - (mean * mean);
}
double var7 = ChangeVariance(results7, 14);
double var28 = ChangeVariance(results28, 56);
// Longer period should have smaller change variance
Assert.True(var28 < var7,
$"Period 28 variance ({var28:F6}) should be less than period 7 ({var7:F6})");
}
/// <summary>
/// TBar and TValue (with same close data) should produce different results
/// since TBar provides actual OHLC data while TValue creates synthetic bars with TR=0.
/// </summary>
[Fact]
public void Adxvma_TBarVsTValue_DifferentResults()
{
var adxvmaTBar = new Adxvma(period: 14);
var adxvmaTValue = new Adxvma(period: 14);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
adxvmaTBar.Update(bar, isNew: true);
adxvmaTValue.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
// TBar has real OHLC → real TR/DM/ADX
// TValue creates synthetic bar with TR=0 → ADX→0 → sc→0 → flat
// They should differ
Assert.NotEqual(adxvmaTBar.Last.Value, adxvmaTValue.Last.Value);
}
}
+357
View File
@@ -0,0 +1,357 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ADXVMA: ADX Variable Moving Average
/// </summary>
/// <remarks>
/// Adaptive IIR filter that uses ADX (Average Directional Index) as its smoothing constant.
/// When ADX is high (strong trend), the filter tracks price aggressively.
/// When ADX is low (range-bound), the filter barely moves.
///
/// Uses Wilder's RMA with warmup compensation for all internal components (TR, +DM, -DM, DX).
/// Requires OHLC data for TR/DM calculation; single-value input creates synthetic bars (TR=0).
///
/// Default period=14.
/// </remarks>
/// <seealso href="Adxvma.md">Detailed documentation</seealso>
[SkipLocalsInit]
public sealed class Adxvma : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct RmaState(double Ema, double E, bool IsCompensated);
[StructLayout(LayoutKind.Auto)]
private record struct AdxvmaState(
RmaState Tr,
RmaState Pdm,
RmaState Ndm,
RmaState Dx,
double PrevHigh,
double PrevLow,
double PrevClose,
double Result,
bool IsInitialized,
int BarCount)
{
public static AdxvmaState New() => new()
{
Tr = new RmaState(0, 1.0, false),
Pdm = new RmaState(0, 1.0, false),
Ndm = new RmaState(0, 1.0, false),
Dx = new RmaState(0, 1.0, false),
PrevHigh = double.NaN,
PrevLow = double.NaN,
PrevClose = double.NaN,
Result = double.NaN,
IsInitialized = false,
BarCount = 0
};
}
private readonly int _period;
private readonly double _alpha;
private readonly double _decay;
private AdxvmaState _state;
private AdxvmaState _p_state;
private double _lastValidValue;
private double _p_lastValidValue;
private const double EPSILON = 1e-10;
public override bool IsHot => _state.BarCount >= _period * 2;
/// <summary>
/// Creates ADXVMA with specified period.
/// </summary>
/// <param name="period">ADX calculation period (must be >= 1)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Adxvma(int period = 14)
{
if (period < 1)
{
throw new ArgumentException("Period must be at least 1", nameof(period));
}
_period = period;
_alpha = 1.0 / period;
_decay = 1.0 - _alpha;
_state = AdxvmaState.New();
_p_state = _state;
_lastValidValue = double.NaN;
_p_lastValidValue = double.NaN;
Name = $"Adxvma({_period})";
WarmupPeriod = _period * 2;
}
/// <summary>
/// Creates ADXVMA connected to a data source.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Adxvma(ITValuePublisher source, int period = 14) : this(period)
{
source.Pub += Handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// Updates ADXVMA with a TBar input (uses OHLC for TR/DM, Close for source).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
_p_lastValidValue = _lastValidValue;
}
else
{
_state = _p_state;
_lastValidValue = _p_lastValidValue;
}
double sourceValue = input.Close;
if (!double.IsFinite(sourceValue))
{
sourceValue = _lastValidValue;
}
else
{
_lastValidValue = sourceValue;
}
if (!double.IsFinite(sourceValue))
{
Last = new TValue(input.Time, double.NaN);
PubEvent(Last, isNew);
return Last;
}
// Calculate True Range
double trueRange;
if (!_state.IsInitialized || double.IsNaN(_state.PrevClose))
{
trueRange = input.High - input.Low;
}
else
{
double hl = input.High - input.Low;
double hpc = Math.Abs(input.High - _state.PrevClose);
double lpc = Math.Abs(input.Low - _state.PrevClose);
trueRange = Math.Max(hl, Math.Max(hpc, lpc));
}
// Calculate Directional Movement
double upMove = _state.IsInitialized && !double.IsNaN(_state.PrevHigh)
? input.High - _state.PrevHigh
: 0.0;
double downMove = _state.IsInitialized && !double.IsNaN(_state.PrevLow)
? _state.PrevLow - input.Low
: 0.0;
double plusDm = (upMove > downMove && upMove > 0) ? upMove : 0.0;
double minusDm = (downMove > upMove && downMove > 0) ? downMove : 0.0;
// Update RMAs with warmup compensation
var tr = _state.Tr;
var pdm = _state.Pdm;
var ndm = _state.Ndm;
tr.Ema = Math.FusedMultiplyAdd(tr.Ema, _decay, _alpha * trueRange);
tr.E *= _decay;
if (tr.E <= EPSILON)
{
tr.IsCompensated = true;
}
pdm.Ema = Math.FusedMultiplyAdd(pdm.Ema, _decay, _alpha * plusDm);
pdm.E *= _decay;
if (pdm.E <= EPSILON)
{
pdm.IsCompensated = true;
}
ndm.Ema = Math.FusedMultiplyAdd(ndm.Ema, _decay, _alpha * minusDm);
ndm.E *= _decay;
if (ndm.E <= EPSILON)
{
ndm.IsCompensated = true;
}
// Compensated values
double compTr = tr.IsCompensated ? tr.Ema : tr.Ema / (1.0 - tr.E);
double compPdm = pdm.IsCompensated ? pdm.Ema : pdm.Ema / (1.0 - pdm.E);
double compNdm = ndm.IsCompensated ? ndm.Ema : ndm.Ema / (1.0 - ndm.E);
// Calculate +DI, -DI, DX
double plusDi = compTr > EPSILON ? 100.0 * compPdm / compTr : 0.0;
double minusDi = compTr > EPSILON ? 100.0 * compNdm / compTr : 0.0;
double diSum = plusDi + minusDi;
double dx = diSum > EPSILON ? 100.0 * Math.Abs(plusDi - minusDi) / diSum : 0.0;
// Smooth DX → ADX
var dxState = _state.Dx;
dxState.Ema = Math.FusedMultiplyAdd(dxState.Ema, _decay, _alpha * dx);
dxState.E *= _decay;
if (dxState.E <= EPSILON)
{
dxState.IsCompensated = true;
}
double adxVal = dxState.IsCompensated ? dxState.Ema : dxState.Ema / (1.0 - dxState.E);
// Smoothing constant from ADX
double sc = Math.Max(0.0, Math.Min(adxVal / 100.0, 1.0));
// Adaptive EMA
double result = double.IsNaN(_state.Result) ? sourceValue : _state.Result + sc * (sourceValue - _state.Result);
// Update state
_state = new AdxvmaState(
tr, pdm, ndm, dxState,
input.High, input.Low, input.Close,
result,
IsInitialized: true,
BarCount: _state.BarCount + (isNew ? 1 : 0));
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Updates ADXVMA with a TValue input.
/// Creates synthetic bar with O=H=L=C (TR=0, no directional movement).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public override TValue Update(TValue input, bool isNew = true)
{
var syntheticBar = new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0);
return Update(syntheticBar, isNew);
}
/// <summary>
/// Updates ADXVMA with a TBarSeries.
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
for (int i = 0; i < len; i++)
{
var bar = source[i];
var result = Update(bar, isNew: true);
tSpan[i] = bar.Time;
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
/// <summary>
/// Updates ADXVMA with a TSeries (single values).
/// </summary>
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var sourceTimes = source.Times;
var sourceValues = source.Values;
for (int i = 0; i < len; i++)
{
var result = Update(new TValue(sourceTimes[i], sourceValues[i]), isNew: true);
tSpan[i] = sourceTimes[i];
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
Reset();
foreach (double val in source)
{
Update(new TValue(DateTime.MinValue, val), isNew: true);
}
}
/// <summary>
/// Calculates ADXVMA from a TBarSeries.
/// </summary>
public static TSeries Batch(TBarSeries source, int period = 14)
{
var adxvma = new Adxvma(period);
return adxvma.Update(source);
}
/// <summary>
/// Calculates ADXVMA from a TSeries (single values; TR=0).
/// </summary>
public static TSeries Batch(TSeries source, int period = 14)
{
var adxvma = new Adxvma(period);
return adxvma.Update(source);
}
/// <summary>
/// Creates an ADXVMA indicator and calculates results from a TBarSeries.
/// </summary>
public static (TSeries Results, Adxvma Indicator) Calculate(TBarSeries source, int period = 14)
{
var indicator = new Adxvma(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
/// <summary>
/// Creates an ADXVMA indicator and calculates results from a TSeries.
/// </summary>
public static (TSeries Results, Adxvma Indicator) Calculate(TSeries source, int period = 14)
{
var indicator = new Adxvma(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
public override void Reset()
{
_state = AdxvmaState.New();
_p_state = _state;
_lastValidValue = double.NaN;
_p_lastValidValue = double.NaN;
Last = default;
}
}