mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-09 06:27:45 +00:00
feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes. - The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator. - Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity. - Updated benchmark tests to use Batch methods for performance evaluation.
This commit is contained in:
@@ -404,6 +404,24 @@ public sealed class Adx : ITValuePublisher
|
||||
smoothed = Math.FusedMultiplyAdd(smoothed, decay, input * invPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
|
||||
{
|
||||
|
||||
@@ -153,6 +153,25 @@ public sealed class Adxr : ITValuePublisher
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
|
||||
{
|
||||
int len = high.Length;
|
||||
@@ -241,4 +260,4 @@ public sealed class Adxr : ITValuePublisher
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,25 @@ public sealed class Alligator : ITValuePublisher
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Alligator for the entire series using default parameters.
|
||||
/// </summary>
|
||||
@@ -348,4 +367,4 @@ public sealed class Alligator : ITValuePublisher
|
||||
/// Gets the Lips offset value (bars forward).
|
||||
/// </summary>
|
||||
public int LipsOffset => _lipsOffset;
|
||||
}
|
||||
}
|
||||
@@ -303,6 +303,18 @@ public sealed class Amat : ITValuePublisher, IDisposable
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar value.
|
||||
/// </summary>
|
||||
/// <param name="bar">Input bar</param>
|
||||
/// <param name="isNew">True if this is a new bar, False if it's an update to the last bar</param>
|
||||
/// <returns>Updated trend value (+1, -1, or 0)</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return Update(new TValue(bar.Time, bar.Close), isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a series of values.
|
||||
/// </summary>
|
||||
@@ -382,6 +394,24 @@ public sealed class Amat : ITValuePublisher, IDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AMAT trend values for a span of input values.
|
||||
/// </summary>
|
||||
|
||||
@@ -185,6 +185,24 @@ public sealed class Aroon : ITValuePublisher
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Aroon oscillator values using O(n) monotonic deque algorithm.
|
||||
/// </summary>
|
||||
|
||||
@@ -169,6 +169,24 @@ public sealed class AroonOsc : ITValuePublisher
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Aroon oscillator values using the shared O(n) algorithm from Aroon.
|
||||
/// </summary>
|
||||
|
||||
@@ -240,6 +240,25 @@ public sealed class Chop : ITValuePublisher
|
||||
return Math.Clamp(chop, 0.0, 100.0);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation with default parameters.
|
||||
/// </summary>
|
||||
@@ -264,4 +283,4 @@ public sealed class Chop : ITValuePublisher
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -36,6 +36,11 @@ public sealed class Dmx : ITValuePublisher
|
||||
public TValue Last { get; private set; }
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True when the indicator has enough data for valid calculations.
|
||||
/// </summary>
|
||||
public bool IsHot => _jmaDMp.IsHot && _jmaDMm.IsHot && _jmaTR.IsHot;
|
||||
|
||||
public Dmx(int period)
|
||||
{
|
||||
Name = $"Dmx({period})";
|
||||
@@ -181,7 +186,24 @@ public sealed class Dmx : ITValuePublisher
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
|
||||
@@ -355,6 +355,24 @@ public sealed class Dx : ITValuePublisher
|
||||
smoothed = smoothed - (smoothed * invPeriod) + input;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class QstickIndicatorTests
|
||||
{
|
||||
// ── Constructor & Defaults ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CreatesValidIndicator()
|
||||
{
|
||||
@@ -13,6 +15,35 @@ public class QstickIndicatorTests
|
||||
Assert.Equal("Qstick Indicator", indicator.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Description_IsNotEmpty()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
|
||||
Assert.Contains("candlestick", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SeparateWindow_IsTrue()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CreatesOneLineSeries()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LineSeries_NameIsQstick()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.Equal("Qstick", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPeriod_Is14()
|
||||
{
|
||||
@@ -28,28 +59,216 @@ public class QstickIndicatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_IncludesParameters()
|
||||
public void DefaultShowColdValues_IsTrue()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
// ── ShortName ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ShortName_DefaultParameters_IncludesPeriodAndMaType()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.Equal("QSTICK(14,SMA)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_CustomPeriod_ReflectsNewPeriod()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 20 };
|
||||
Assert.Equal("QSTICK(20,SMA)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_EmaMode_IncludesEMA()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 20, MaType = "EMA" };
|
||||
Assert.Equal("QSTICK(20,EMA)", indicator.ShortName);
|
||||
}
|
||||
|
||||
// ── MinHistoryDepths ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsZero()
|
||||
public void MinHistoryDepths_Static_EqualsZero()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 10 };
|
||||
Assert.Equal(0, QstickIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_Interface_EqualsZero()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
// ── OnInit ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CalculationIntegration_ProducesCorrectValues()
|
||||
public void Initialize_SmaMode_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new QstickIndicator { MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_EmaMode_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new QstickIndicator { MaType = "EMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_AddsZeroLineLevel()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// OnInit calls AddLineLevel(0, "Zero", ...)
|
||||
Assert.True(indicator.LineLevels.Count >= 1);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// After 5 bars with constant diff=5, Qstick=5
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_HistoricalBar_BullishBars_PositiveValue()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All bullish, diff=5 each, SMA=5.0
|
||||
Assert.Equal(5.0, indicator.LinesSeries[0].GetValue(0), 10);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100.0, 108.0, 95.0, 103.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
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));
|
||||
}
|
||||
|
||||
// ── SMA vs EMA ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SmaMode_BearishBars_ProducesNegativeQstick()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 105.0, 90.0, 95.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All bearish (close < open), diff=-5
|
||||
Assert.Equal(-5.0, indicator.LinesSeries[0].GetValue(0), 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmaMode_DojiBars_ProducesZeroQstick()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 105.0, 95.0, 100.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmaMode_BullishBars_ProducesPositiveQstick()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 5, MaType = "EMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All same diff=5, EMA converges to 5.0
|
||||
Assert.Equal(5.0, indicator.LinesSeries[0].GetValue(0), 10);
|
||||
}
|
||||
|
||||
// ── Core Calculation Integration ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CalculationIntegration_SmaMode_ProducesCorrectValues()
|
||||
{
|
||||
var qstickCore = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Simulate bar data
|
||||
var bar1 = new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
var bar2 = new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000);
|
||||
var bar3 = new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000);
|
||||
@@ -71,20 +290,34 @@ public class QstickIndicatorTests
|
||||
// Bar 1: diff = 5
|
||||
qstickCore.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
|
||||
// Bar 2: diff = -3, EMA with alpha = 0.5
|
||||
// Bar 2: diff = -3, EMA with alpha = 2/(3+1) = 0.5
|
||||
var result = qstickCore.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 97.0, 1000));
|
||||
|
||||
// EMA = 0.5 * -3 + 0.5 * 5 = 1.0
|
||||
Assert.Equal(1.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculationIntegration_MixedBullishBearish()
|
||||
{
|
||||
var qstickCore = new Qstick(4);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// 2 bullish (diff=5), 2 bearish (diff=-5) → SMA = 0
|
||||
qstickCore.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
qstickCore.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
qstickCore.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
|
||||
var result = qstickCore.Update(new TBar(time.AddMinutes(3).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
|
||||
|
||||
Assert.Equal(0.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BullishBars_ProducePositiveQstick()
|
||||
{
|
||||
var qstick = new Qstick(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All bullish bars (close > open)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
@@ -100,7 +333,6 @@ public class QstickIndicatorTests
|
||||
var qstick = new Qstick(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All bearish bars (close < open)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 90.0, 95.0, 1000));
|
||||
@@ -116,7 +348,6 @@ public class QstickIndicatorTests
|
||||
var qstick = new Qstick(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All doji bars (close = open)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
qstick.Update(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 100.0, 1000));
|
||||
@@ -125,6 +356,8 @@ public class QstickIndicatorTests
|
||||
Assert.Equal(0.0, qstick.Last.Value, 10);
|
||||
}
|
||||
|
||||
// ── Core Indicator Features ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_ResetsCorrectly()
|
||||
{
|
||||
@@ -141,4 +374,433 @@ public class QstickIndicatorTests
|
||||
Assert.False(qstick.IsHot);
|
||||
Assert.Equal(default, qstick.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_IsHot_SmaMode_AfterPeriodBars()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
Assert.False(qstick.IsHot);
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
Assert.False(qstick.IsHot);
|
||||
|
||||
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
Assert.False(qstick.IsHot);
|
||||
|
||||
qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
Assert.True(qstick.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_IsHot_EmaMode_AfterFirstBar()
|
||||
{
|
||||
var qstick = new Qstick(3, useEma: true);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
Assert.False(qstick.IsHot);
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
Assert.True(qstick.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Period_ReturnsConstructorValue()
|
||||
{
|
||||
var qstick = new Qstick(20);
|
||||
Assert.Equal(20, qstick.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_UseEma_ReturnsConstructorValue()
|
||||
{
|
||||
var qstickSma = new Qstick(10, useEma: false);
|
||||
Assert.False(qstickSma.UseEma);
|
||||
|
||||
var qstickEma = new Qstick(10, useEma: true);
|
||||
Assert.True(qstickEma.UseEma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var qstick = new Qstick(20);
|
||||
Assert.Equal(20, qstick.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Name_SmaMode_DoesNotIncludeEma()
|
||||
{
|
||||
var qstick = new Qstick(14);
|
||||
Assert.Equal("QSTICK(14)", qstick.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Name_EmaMode_IncludesEma()
|
||||
{
|
||||
var qstick = new Qstick(14, useEma: true);
|
||||
Assert.Equal("QSTICK(14,EMA)", qstick.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Qstick(0));
|
||||
Assert.Throws<ArgumentException>(() => new Qstick(-1));
|
||||
}
|
||||
|
||||
// ── NaN/Infinity Handling ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_NaNOpen_ReturnsLastValue()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
var nanResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, double.NaN, 110.0, 95.0, 105.0, 1000));
|
||||
|
||||
Assert.Equal(firstResult.Value, nanResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_InfinityClose_ReturnsLastValue()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
var infResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, double.PositiveInfinity, 1000));
|
||||
|
||||
Assert.Equal(firstResult.Value, infResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_NegativeInfinityOpen_ReturnsLastValue()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var firstResult = qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
var infResult = qstick.Update(new TBar(time.AddMinutes(1).Ticks, double.NegativeInfinity, 110.0, 95.0, 105.0, 1000));
|
||||
|
||||
Assert.Equal(firstResult.Value, infResult.Value);
|
||||
}
|
||||
|
||||
// ── Bar Correction (isNew=false) ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_BarCorrection_SmaMode_UpdatesLastBar()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 110.0, 95.0, 103.0, 1000));
|
||||
|
||||
// New bar
|
||||
qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 108.0, 95.0, 106.0, 1000), isNew: true);
|
||||
var afterNew = qstick.Last.Value;
|
||||
|
||||
// Correct the same bar (isNew=false)
|
||||
var afterCorrection = qstick.Update(new TBar(time.AddMinutes(2).Ticks, 100.0, 112.0, 93.0, 110.0, 1000), isNew: false);
|
||||
|
||||
// Value should change since close-open changed from 6 to 10
|
||||
Assert.NotEqual(afterNew, afterCorrection.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_BarCorrection_EmaMode_RollsBackState()
|
||||
{
|
||||
var qstick = new Qstick(3, useEma: true);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
|
||||
// New bar
|
||||
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 108.0, 95.0, 106.0, 1000), isNew: true);
|
||||
var afterNew = qstick.Last.Value;
|
||||
|
||||
// Correct the same bar (isNew=false)
|
||||
var afterCorrection = qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 112.0, 93.0, 110.0, 1000), isNew: false);
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection.Value);
|
||||
}
|
||||
|
||||
// ── Batch / Update(TBarSeries) / Calculate ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_UpdateTBarSeries_ReturnsTSeries()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0 + i, 1000);
|
||||
}
|
||||
|
||||
var result = qstick.Update(series);
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_UpdateTBarSeries_EmptySeries_ReturnsEmpty()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var series = new TBarSeries();
|
||||
|
||||
var result = qstick.Update(series);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Batch_ReturnsResults()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
}
|
||||
|
||||
var result = Qstick.Batch(series, period: 3);
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_BatchEma_ReturnsResults()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
}
|
||||
|
||||
var result = Qstick.Batch(series, period: 3, useEma: true);
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Equal(5.0, result.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
}
|
||||
|
||||
var (results, indicator) = Qstick.Calculate(series, period: 3);
|
||||
|
||||
Assert.Equal(5, results.Count);
|
||||
Assert.NotNull(indicator);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_Prime_WarmsUpIndicator()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
series.Add(time.AddMinutes(i).Ticks, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
}
|
||||
|
||||
qstick.Prime(series);
|
||||
|
||||
Assert.True(qstick.IsHot);
|
||||
Assert.Equal(5.0, qstick.Last.Value, 10);
|
||||
}
|
||||
|
||||
// ── Pub Event ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
int eventCount = 0;
|
||||
|
||||
qstick.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, 100.0, 110.0, 95.0, 105.0, 1000));
|
||||
qstick.Update(new TBar(time.AddMinutes(1).Ticks, 100.0, 105.0, 95.0, 103.0, 1000));
|
||||
|
||||
Assert.Equal(2, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoreIndicator_PubEvent_NaN_StillFires()
|
||||
{
|
||||
var qstick = new Qstick(3);
|
||||
var time = DateTime.UtcNow;
|
||||
int eventCount = 0;
|
||||
|
||||
qstick.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
qstick.Update(new TBar(time.Ticks, double.NaN, 110.0, 95.0, 105.0, 1000));
|
||||
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
// ── Different Periods ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var indicator1 = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
var indicator2 = new QstickIndicator { Period = 10, MaType = "SMA" };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double close = 105.0 + (i % 2 == 0 ? 3.0 : -3.0);
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, close, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, close, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double val2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
Assert.NotEqual(val1, val2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmaVsEma_SameData_ProduceDifferentResults()
|
||||
{
|
||||
var smaIndicator = new QstickIndicator { Period = 5, MaType = "SMA" };
|
||||
var emaIndicator = new QstickIndicator { Period = 5, MaType = "EMA" };
|
||||
smaIndicator.Initialize();
|
||||
emaIndicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = [105.0, 97.0, 108.0, 99.0, 102.0];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
smaIndicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, closes[i], 1000);
|
||||
smaIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
emaIndicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 115.0, 85.0, closes[i], 1000);
|
||||
emaIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double smaVal = smaIndicator.LinesSeries[0].GetValue(0);
|
||||
double emaVal = emaIndicator.LinesSeries[0].GetValue(0);
|
||||
// SMA = (5 + -3 + 8 + -1 + 2) / 5 = 11/5 = 2.2
|
||||
Assert.Equal(2.2, smaVal, 10);
|
||||
Assert.NotEqual(smaVal, emaVal);
|
||||
}
|
||||
|
||||
// ── Reinit ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reinitialize_WithDifferentParameters_ResetsState()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 5, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
indicator.Period = 10;
|
||||
indicator.MaType = "EMA";
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("QSTICK(10,EMA)", indicator.ShortName);
|
||||
}
|
||||
|
||||
// ── ShowColdValues ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ShowColdValues_CanBeSetToFalse()
|
||||
{
|
||||
var indicator = new QstickIndicator { ShowColdValues = false };
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowColdValues_False_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 5, ShowColdValues = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
// ── Multiple bars through adapter with known values ─────────────────
|
||||
|
||||
[Fact]
|
||||
public void MultipleBars_ThroughAdapter_ProducesExpectedValues()
|
||||
{
|
||||
var indicator = new QstickIndicator { Period = 3, MaType = "SMA" };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Bar 1: diff = 5 (105-100)
|
||||
indicator.HistoricalData.AddBar(now, 100.0, 110.0, 95.0, 105.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Bar 2: diff = 3 (103-100)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100.0, 105.0, 95.0, 103.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Bar 3: diff = 6 (106-100)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 100.0, 108.0, 95.0, 106.0, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// SMA(3) of [5, 3, 6] = 14/3 ≈ 4.667
|
||||
Assert.Equal(14.0 / 3.0, indicator.LinesSeries[0].GetValue(0), 10);
|
||||
}
|
||||
|
||||
// ── Parameters can be modified ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 30;
|
||||
Assert.Equal(30, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaType_CanBeChanged()
|
||||
{
|
||||
var indicator = new QstickIndicator();
|
||||
Assert.Equal("SMA", indicator.MaType);
|
||||
|
||||
indicator.MaType = "EMA";
|
||||
Assert.Equal("EMA", indicator.MaType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +265,25 @@ public sealed class Super : ITValuePublisher
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source, int period = 10, double multiplier = 3.0)
|
||||
{
|
||||
var indicator = new Super(period, multiplier);
|
||||
@@ -277,4 +296,4 @@ public sealed class Super : ITValuePublisher
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,25 @@ public sealed class Vortex : ITValuePublisher
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Vortex indicator values using O(n) sliding window algorithm.
|
||||
/// </summary>
|
||||
@@ -238,7 +257,7 @@ public sealed class Vortex : ITValuePublisher
|
||||
/// <param name="viMinus">Output VI- values</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close,
|
||||
int period, Span<double> viPlus, Span<double> viMinus)
|
||||
int period, Span<double> viPlus, Span<double> viMinus)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (len == 0 || len != low.Length || len != close.Length || len != viPlus.Length || len != viMinus.Length || period <= 1)
|
||||
|
||||
Reference in New Issue
Block a user