mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +00:00
python wrapper
This commit is contained in:
@@ -1,297 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class AcfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AcfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AcfIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(1, indicator.Lag);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("ACF - Autocorrelation Function", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AcfIndicator();
|
||||
|
||||
Assert.Equal(0, AcfIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ShortName_IncludesPeriodAndLag()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 14, Lag = 2 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("ACF", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("2", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_Initialize_CreatesInternalAcf()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 10, Lag = 1 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_Lag_CanBeChanged()
|
||||
{
|
||||
var indicator = new AcfIndicator { Lag = 1 };
|
||||
|
||||
Assert.Equal(1, indicator.Lag);
|
||||
|
||||
indicator.Lag = 5;
|
||||
Assert.Equal(5, indicator.Lag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new AcfIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new AcfIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ShortName_UpdatesWhenPeriodChanges()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.Period = 20;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ShortName_UpdatesWhenLagChanges()
|
||||
{
|
||||
var indicator = new AcfIndicator { Lag = 1 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("1", StringComparison.Ordinal));
|
||||
|
||||
indicator.Lag = 3;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("3", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("ACF", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_DifferentLagValues_Work()
|
||||
{
|
||||
var lags = new[] { 1, 2, 3, 5, 10 };
|
||||
|
||||
foreach (var lag in lags)
|
||||
{
|
||||
// Period must be > lag + 1
|
||||
int period = Math.Max(20, lag + 5);
|
||||
var indicator = new AcfIndicator { Period = period, Lag = lag };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars to fill the buffer
|
||||
for (int i = 0; i < period + 5; i++)
|
||||
{
|
||||
double close = 100 + (i % 10);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Last value should be finite and bounded
|
||||
double acfValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(acfValue), $"Lag {lag} should produce finite value");
|
||||
Assert.True(acfValue >= -1 && acfValue <= 1, $"ACF at lag {lag} should be bounded [-1, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcfIndicator_AcfValuesAreBounded()
|
||||
{
|
||||
var indicator = new AcfIndicator { Period = 10, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 98, 105, 97, 110, 95, 108, 92, 115, 90, 120 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 5, close - 5, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All ACF values should be bounded between -1 and 1
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double value = indicator.LinesSeries[0].GetValue(closes.Length - 1 - i);
|
||||
Assert.True(value >= -1 && value <= 1, $"ACF value at index {i} should be bounded [-1, 1], got {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AcfIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Lag", sortIndex: 2, 1, 100, 1, 0)]
|
||||
public int Lag { get; set; } = 1;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Acf _acf = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ACF ({Period},{Lag})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/acf/Acf.Quantower.cs";
|
||||
|
||||
public AcfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ACF - Autocorrelation Function";
|
||||
Description = "Measures the correlation of a time series with a lagged copy of itself";
|
||||
|
||||
_series = new LineSeries(name: "ACF", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_acf = new Acf(Period, Lag);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _acf.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _acf.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BetaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BetaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BetaIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.AssetSource);
|
||||
Assert.Equal(SourceType.Close, indicator.MarketSource);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Beta Coefficient", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(2, BetaIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(2, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 14 };
|
||||
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Beta", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_Initialize_CreatesInternalBeta()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Beta", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data - need enough bars for warmup
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double beta = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(beta));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add initial bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(11, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BetaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var assetSources = new[]
|
||||
{
|
||||
SourceType.Open,
|
||||
SourceType.High,
|
||||
SourceType.Low,
|
||||
SourceType.Close,
|
||||
};
|
||||
|
||||
foreach (var source in assetSources)
|
||||
{
|
||||
var indicator = new BetaIndicator { Period = 5, AssetSource = source, MarketSource = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"AssetSource {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class BetaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Asset Source", sortIndex: 2)]
|
||||
public SourceType AssetSource { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Market Source", sortIndex: 3)]
|
||||
public SourceType MarketSource { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Beta _beta = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _assetSelector = null!;
|
||||
private Func<IHistoryItem, double> _marketSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Beta({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/beta/Beta.Quantower.cs";
|
||||
|
||||
public BetaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Beta Coefficient";
|
||||
Description = "Measures the volatility of an asset in relation to the overall market.";
|
||||
|
||||
_series = new LineSeries(name: "Beta", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_beta = new Beta(Period);
|
||||
_assetSelector = AssetSource.GetPriceSelector();
|
||||
_marketSelector = MarketSource.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double assetVal = _assetSelector(item);
|
||||
double marketVal = _marketSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var assetInput = new TValue(time, assetVal);
|
||||
var marketInput = new TValue(time, marketVal);
|
||||
|
||||
TValue result = _beta.Update(assetInput, marketInput, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _beta.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CMA - Cumulative Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Equal(0, CmaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ShortName_IncludesSource()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Contains("CMA", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_Initialize_CreatesInternalCma()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
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 CmaIndicator_MultipleUpdates_ProducesCorrectCmaSequence()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// Last CMA should be average of all values: (100 + 102 + 104 + 103 + 105) / 5 = 102.8
|
||||
double lastCma = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(102.8, lastCma, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CmaIndicator { Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_CalculatesRunningAverage()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with known close prices: 10, 20, 30
|
||||
indicator.HistoricalData.AddBar(now, 10, 10, 10, 10);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(10.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = 10
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 20, 20, 20, 20);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(15.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20)/2 = 15
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 30, 30, 30, 30);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(20.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20+30)/3 = 20
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class CmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Cma _cma = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CMA:{_sourceName}";
|
||||
|
||||
public CmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "CMA - Cumulative Moving Average";
|
||||
Description = "Cumulative Moving Average (Running Average)";
|
||||
_series = new LineSeries(name: "CMA", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_cma = new Cma();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _cma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _cma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CointegrationIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CointegrationIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CointegrationIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("COINT - Cointegration (Engle-Granger)", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new CointegrationIndicator();
|
||||
|
||||
Assert.Equal(2, CointegrationIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("COINT", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_Initialize_CreatesInternalCointegration()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value (may be NaN during warmup)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { 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);
|
||||
|
||||
// NewTick should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Values should be produced (may be NaN during warmup, but should not throw)
|
||||
Assert.True(double.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add bars with different O/C patterns to create cointegration signals
|
||||
double[] opens = { 100, 101, 102, 103, 104, 105 };
|
||||
double[] closes = { 100, 101, 102, 103, 104, 105 };
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// All values should exist
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Should have computed a value (may be NaN during warmup, but should not throw)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_CointegrationInterpretation()
|
||||
{
|
||||
// This test verifies the indicator produces meaningful cointegration values
|
||||
// when given perfectly correlated data (Open = Close), we expect strong cointegration
|
||||
var indicator = new CointegrationIndicator { Period = 5, Source = SourceType.Close, Source2 = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add perfectly proportional bars: Open always equals Close
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// After warmup, should have finite values
|
||||
// (Note: when Close == Open exactly, residuals have zero variance, may produce NaN)
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_DifferentSource2Types_Work()
|
||||
{
|
||||
var source2Types = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.HL2 };
|
||||
|
||||
foreach (var source2 in source2Types)
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 5, Source = SourceType.Close, Source2 = source2 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_Source2_CanBeChanged()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Source2 = SourceType.High };
|
||||
|
||||
Assert.Equal(SourceType.High, indicator.Source2);
|
||||
|
||||
indicator.Source2 = SourceType.Low;
|
||||
Assert.Equal(SourceType.Low, indicator.Source2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_ReInitialize_ResetsState()
|
||||
{
|
||||
var indicator = new CointegrationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, indicator.LinesSeries[0].Count);
|
||||
|
||||
// Re-initialize should work without errors
|
||||
var indicator2 = new CointegrationIndicator { Period = 5 };
|
||||
indicator2.Initialize();
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(100), 200, 210, 190, 205);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator2.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_HighLow_ProducesValues()
|
||||
{
|
||||
// Test with High vs Low as a practical use case
|
||||
var indicator = new CointegrationIndicator { Period = 10, Source = SourceType.High, Source2 = SourceType.Low };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with varying spread between high and low
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double mid = 100 + (i * 0.5);
|
||||
double spread = 5 + (i % 3); // Varying spread
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), mid, mid + spread, mid - spread, mid);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(15, indicator.LinesSeries[0].Count);
|
||||
|
||||
// After warmup period, should have finite values
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
// High and Low should be cointegrated (they move together)
|
||||
Assert.True(double.IsFinite(lastValue) || double.IsNaN(lastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CointegrationIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new CointegrationIndicator();
|
||||
|
||||
Assert.Contains("cointegration", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("ADF", indicator.Description, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Cointegration indicator.
|
||||
/// Measures the statistical equilibrium relationship between two price series
|
||||
/// using the Engle-Granger two-step method with ADF test.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
|
||||
/// Close vs Volume, High vs Low). For cross-symbol cointegration analysis, use the core
|
||||
/// Cointegration class directly with data from multiple symbols.
|
||||
///
|
||||
/// The output is the ADF test statistic. More negative values indicate stronger cointegration.
|
||||
/// Critical values: -3.43 (1%), -2.86 (5%), -2.57 (10%)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class CointegrationIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Cointegration _cointegration = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"COINT({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public CointegrationIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "COINT - Cointegration (Engle-Granger)";
|
||||
Description = "Measures statistical equilibrium between two price sources using ADF test. More negative = stronger cointegration.";
|
||||
_series = new LineSeries(name: "ADF", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_cointegration = new Cointegration(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
// Get both price sources from the same bar
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueA = _priceSelector(item);
|
||||
double valueB = _priceSelector2(item);
|
||||
|
||||
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
|
||||
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
|
||||
|
||||
double value = _cointegration.Update(tvalA, tvalB, isNew).Value;
|
||||
_series.SetValue(value, _cointegration.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CorrelationIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CORR - Pearson Correlation Coefficient", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Equal(2, CorrelationIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("CORR", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Initialize_CreatesInternalCorrelation()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value (may be NaN during warmup)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { 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);
|
||||
|
||||
// NewTick should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Values should be produced (may be NaN during warmup, but should not throw)
|
||||
Assert.True(double.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add bars with different O/C patterns to create varying correlation
|
||||
double[] opens = { 100, 101, 102, 103, 104, 105 };
|
||||
double[] closes = { 100, 101, 102, 103, 104, 105 };
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// All values should exist
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Should have computed a value (may be NaN during warmup, but should not throw)
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_CorrelationBounds()
|
||||
{
|
||||
// This test verifies the indicator produces values in valid range [-1, +1]
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with varying patterns
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double open = 100 + i;
|
||||
double close = 100 + i + (i % 2 == 0 ? 2 : -1); // Varying relationship
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, open + 5, open - 5, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// After warmup, should have values in valid range
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
|
||||
// Check that values are bounded
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double value = indicator.LinesSeries[0].GetValue(i);
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
Assert.InRange(value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_DifferentSource2Types_Work()
|
||||
{
|
||||
var source2Types = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.HL2 };
|
||||
|
||||
foreach (var source2 in source2Types)
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = source2 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Source2_CanBeChanged()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Source2 = SourceType.High };
|
||||
|
||||
Assert.Equal(SourceType.High, indicator.Source2);
|
||||
|
||||
indicator.Source2 = SourceType.Low;
|
||||
Assert.Equal(SourceType.Low, indicator.Source2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_ReInitialize_ResetsState()
|
||||
{
|
||||
var indicator = new CorrelationIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, indicator.LinesSeries[0].Count);
|
||||
|
||||
// Re-initialize should work without errors
|
||||
var indicator2 = new CorrelationIndicator { Period = 5 };
|
||||
indicator2.Initialize();
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(100), 200, 210, 190, 205);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator2.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_HighLow_ProducesPositiveCorrelation()
|
||||
{
|
||||
// Test with High vs Low - they should be positively correlated
|
||||
var indicator = new CorrelationIndicator { Period = 10, Source = SourceType.High, Source2 = SourceType.Low };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with typical High > Low relationship
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double mid = 100 + (i * 0.5);
|
||||
double spread = 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), mid, mid + spread, mid - spread, mid);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(15, indicator.LinesSeries[0].Count);
|
||||
|
||||
// After warmup period, High and Low should show positive correlation
|
||||
// (they both trend together as price moves)
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
if (double.IsFinite(lastValue))
|
||||
{
|
||||
Assert.True(lastValue > 0, $"Expected positive correlation for High vs Low, got {lastValue}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new CorrelationIndicator();
|
||||
|
||||
Assert.Contains("linear", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("-1", indicator.Description, StringComparison.Ordinal);
|
||||
Assert.Contains("+1", indicator.Description, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorrelationIndicator_PerfectCorrelation_ReturnsOne()
|
||||
{
|
||||
// When Close == Open for all bars, correlation should be 1.0 (or NaN if zero variance)
|
||||
var indicator = new CorrelationIndicator { Period = 5, Source = SourceType.Close, Source2 = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars where Close always equals Open (perfect linear relationship)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + i * 2; // Trending up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, indicator.LinesSeries[0].Count);
|
||||
|
||||
// When Open == Close exactly, we get perfect correlation = 1.0
|
||||
double lastValue = indicator.LinesSeries[0].GetValue(0);
|
||||
if (double.IsFinite(lastValue))
|
||||
{
|
||||
Assert.Equal(1.0, lastValue, precision: 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Correlation indicator.
|
||||
/// Measures the Pearson correlation coefficient between two price series.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
|
||||
/// Close vs Volume, High vs Low). For cross-symbol correlation analysis, use the core
|
||||
/// Correlation class directly with data from multiple symbols.
|
||||
///
|
||||
/// The output is the Pearson correlation coefficient, ranging from -1 to +1.
|
||||
/// Values near +1 indicate strong positive correlation, near -1 indicate strong negative correlation.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class CorrelationIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Correlation _correlation = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CORR({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public CorrelationIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "CORR - Pearson Correlation Coefficient";
|
||||
Description = "Measures linear relationship between two price sources. Range: -1 (inverse) to +1 (perfect positive).";
|
||||
_series = new LineSeries(name: "Correlation", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_correlation = new Correlation(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
// Get both price sources from the same bar
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueA = _priceSelector(item);
|
||||
double valueB = _priceSelector2(item);
|
||||
|
||||
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
|
||||
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
|
||||
|
||||
double value = _correlation.Update(tvalA, tvalB, isNew).Value;
|
||||
_series.SetValue(value, _correlation.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CovarianceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CovarianceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CovarianceIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.Equal(SourceType.Close, indicator.Source1);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Covariance", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CovarianceIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new CovarianceIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(2, CovarianceIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(2, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CovarianceIndicator_Initialize_CreatesInternalCovariance()
|
||||
{
|
||||
var indicator = new CovarianceIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Covariance", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CovarianceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CovarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double cov = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(cov));
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class CovarianceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Population", sortIndex: 2)]
|
||||
public bool IsPopulation { get; set; } = false;
|
||||
|
||||
[InputParameter("Source 1", sortIndex: 3)]
|
||||
public SourceType Source1 { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2", sortIndex: 4)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Covariance _cov = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector1 = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Cov({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/covariance/Covariance.Quantower.cs";
|
||||
|
||||
public CovarianceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Covariance";
|
||||
Description = "Measures the joint variability of two random variables.";
|
||||
|
||||
_series = new LineSeries(name: "Covariance", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_cov = new Covariance(Period, IsPopulation);
|
||||
_priceSelector1 = Source1.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double val1 = _priceSelector1(item);
|
||||
double val2 = _priceSelector2(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input1 = new TValue(time, val1);
|
||||
var input2 = new TValue(time, val2);
|
||||
|
||||
TValue result = _cov.Update(input1, input2, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _cov.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class EntropyIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EntropyIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EntropyIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Entropy - Shannon Entropy", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, EntropyIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_Initialize_CreatesInternalEntropy()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Entropy", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double entropy = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(entropy));
|
||||
// Allow tiny floating-point overshoot above 1.0
|
||||
Assert.True(entropy >= -1e-10 && entropy <= 1.0 + 1e-10,
|
||||
$"Expected entropy in [0, 1], got {entropy}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double entropy = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(entropy));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 20 };
|
||||
Assert.Equal("Entropy 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntropyIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new EntropyIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add enough bars to warm up
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should change after adding a significantly different bar
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class EntropyIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Entropy _entropy = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Entropy {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/entropy/Entropy.Quantower.cs";
|
||||
|
||||
public EntropyIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Entropy - Shannon Entropy";
|
||||
Description = "Measures the randomness/predictability of price data using normalized Shannon entropy";
|
||||
|
||||
_series = new LineSeries(name: "Entropy", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_entropy = new Entropy(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _entropy.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _entropy.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class GeomeanIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GeomeanIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new GeomeanIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("GEOMEAN - Geometric Mean", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, GeomeanIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_Initialize_CreatesInternalGeomean()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Geomean", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double geomean = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(geomean));
|
||||
Assert.True(geomean > 0, $"Geometric mean should be positive, got {geomean}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double geomean = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(geomean));
|
||||
Assert.True(geomean > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 20 };
|
||||
Assert.Equal("Geomean 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeomeanIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new GeomeanIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add enough bars to warm up
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should change after adding a significantly different bar
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
Assert.True(valueAfter > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class GeomeanIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Geomean _geomean = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Geomean {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/geomean/Geomean.Quantower.cs";
|
||||
|
||||
public GeomeanIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "GEOMEAN - Geometric Mean";
|
||||
Description = "Rolling geometric mean of price data using log-sum approach";
|
||||
|
||||
_series = new LineSeries(name: "Geomean", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_geomean = new Geomean(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _geomean.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _geomean.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class GrangerIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void GrangerIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new GrangerIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("GRANGER - Granger Causality F-Statistic", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new GrangerIndicator();
|
||||
|
||||
Assert.Equal(2, GrangerIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new GrangerIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("GRANGER", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_Initialize_CreatesInternalGranger()
|
||||
{
|
||||
var indicator = new GrangerIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new GrangerIndicator { 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new GrangerIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new GrangerIndicator { 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.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new GrangerIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] opens = { 100, 101, 102, 103, 104, 105 };
|
||||
double[] closes = { 100, 101, 102, 103, 104, 105 };
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrangerIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new GrangerIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Granger Causality indicator.
|
||||
/// Tests whether one price source Granger-causes another using F-statistic.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Volume).
|
||||
/// For cross-symbol Granger causality analysis, use the core Granger class directly.
|
||||
///
|
||||
/// Higher F-statistic values indicate stronger evidence that Source 2 Granger-causes Source 1.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class GrangerIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 4, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Granger _granger = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"GRANGER({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public GrangerIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "GRANGER - Granger Causality F-Statistic";
|
||||
Description = "Tests whether one price source helps predict another. Higher F-statistic = stronger evidence of Granger causality.";
|
||||
_series = new LineSeries(name: "F-Stat", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_granger = new Granger(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueY = _priceSelector(item);
|
||||
double valueX = _priceSelector2(item);
|
||||
|
||||
var tvalY = new TValue(item.TimeLeft.Ticks, valueY);
|
||||
var tvalX = new TValue(item.TimeLeft.Ticks, valueX);
|
||||
|
||||
double value = _granger.Update(tvalY, tvalX, isNew).Value;
|
||||
_series.SetValue(value, _granger.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class HarmeanIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HarmeanIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HarmeanIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("HARMEAN - Harmonic Mean", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, HarmeanIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_Initialize_CreatesInternalHarmean()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Harmean", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double harmean = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(harmean));
|
||||
Assert.True(harmean > 0, $"Harmonic mean should be positive, got {harmean}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double harmean = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(harmean));
|
||||
Assert.True(harmean > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 20 };
|
||||
Assert.Equal("Harmean 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HarmeanIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new HarmeanIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add enough bars to warm up
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should change after adding a significantly different bar
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
Assert.True(valueAfter > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class HarmeanIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Harmean _harmean = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Harmean {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/harmean/Harmean.Quantower.cs";
|
||||
|
||||
public HarmeanIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "HARMEAN - Harmonic Mean";
|
||||
Description = "Rolling harmonic mean of price data using reciprocal-sum approach";
|
||||
|
||||
_series = new LineSeries(name: "Harmean", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_harmean = new Harmean(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _harmean.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _harmean.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class HurstIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HurstIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HurstIndicator();
|
||||
|
||||
Assert.Equal(100, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Hurst - Hurst Exponent", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 100 };
|
||||
|
||||
Assert.Equal(0, HurstIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_Initialize_CreatesInternalHurst()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 20 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Hurst line + 0.5 reference line)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Hurst", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("0.5", indicator.LinesSeries[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double hurst = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(hurst));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 20, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double hurst = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(hurst));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 50 };
|
||||
Assert.Equal("Hurst 50", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HurstIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new HurstIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class HurstIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 20, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 100;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Hurst _hurst = null!;
|
||||
private readonly LineSeries _series;
|
||||
private readonly LineSeries _halfLine;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Hurst {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/hurst/Hurst.Quantower.cs";
|
||||
|
||||
public HurstIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Hurst - Hurst Exponent";
|
||||
Description = "Measures long-range dependence using Rescaled Range (R/S) analysis. H > 0.5 = trending, H < 0.5 = mean-reverting, H ≈ 0.5 = random walk";
|
||||
|
||||
_series = new LineSeries(name: "Hurst", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
_halfLine = new LineSeries(name: "0.5", color: Color.Gray, width: 1, style: LineStyle.Dash);
|
||||
AddLineSeries(_series);
|
||||
AddLineSeries(_halfLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_hurst = new Hurst(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _hurst.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _hurst.IsHot, ShowColdValues);
|
||||
_halfLine.SetValue(0.5);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
using Skender.Stock.Indicators;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
// HURST Validation Tests - Hurst Exponent via Rescaled Range (R/S) Analysis
|
||||
// Validated against self-consistency and known mathematical properties
|
||||
// No external library provides a direct R/S-based Hurst exponent equivalent
|
||||
@@ -181,4 +184,40 @@ public sealed class HurstValidationTests
|
||||
Assert.Equal(h1.Last.Value, h2.Last.Value, 1e-15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structural comparison with Skender GetHurst — both compute Hurst exponent
|
||||
/// but may use different R/S subdivision strategies and regression methods.
|
||||
/// Validates that Skender produces finite results in the same range.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Skender_Hurst_Structural()
|
||||
{
|
||||
const int period = 20;
|
||||
using var data = new ValidationTestData(10000);
|
||||
|
||||
// QuanTAlib streaming
|
||||
var indicator = new Hurst(period);
|
||||
foreach (var tv in data.Data)
|
||||
{
|
||||
indicator.Update(tv);
|
||||
}
|
||||
|
||||
// Skender
|
||||
var sResult = data.SkenderQuotes.GetHurst(period).ToList();
|
||||
|
||||
// QuanTAlib produces finite output
|
||||
Assert.True(double.IsFinite(indicator.Last.Value), "QuanTAlib Hurst last must be finite");
|
||||
|
||||
// Skender produces finite Hurst exponents
|
||||
int sFinite = sResult.Count(r => r.HurstExponent is not null && double.IsFinite(r.HurstExponent.Value));
|
||||
Assert.True(sFinite > 50, $"Skender produced only {sFinite} finite Hurst values");
|
||||
|
||||
// Both Hurst exponents should be finite
|
||||
foreach (var r in sResult.Where(r => r.HurstExponent is not null))
|
||||
{
|
||||
Assert.True(double.IsFinite(r.HurstExponent!.Value),
|
||||
$"Skender Hurst value {r.HurstExponent.Value} is not finite");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class IqrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void IqrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new IqrIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("IQR - Interquartile Range", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, IqrIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_Initialize_CreatesInternalIqr()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("IQR", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double iqr = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(iqr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double iqr = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(iqr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 30 };
|
||||
Assert.Equal("IQR 30", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IqrIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new IqrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class IqrIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Iqr _iqr = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"IQR {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/iqr/Iqr.Quantower.cs";
|
||||
|
||||
public IqrIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "IQR - Interquartile Range";
|
||||
Description = "Measures spread of the middle 50% of data (Q3 - Q1)";
|
||||
|
||||
_series = new LineSeries(name: "IQR", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_iqr = new Iqr(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _iqr.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _iqr.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class JbIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void JbIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new JbIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("JB - Jarque-Bera Test", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, JbIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_Initialize_CreatesInternalJb()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
Assert.Equal("JB", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("10%", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("5%", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("1%", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double jb = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(jb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double jb = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(jb));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 30 };
|
||||
Assert.Equal("JB 30", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JbIndicator_CriticalValueLines_AreSet()
|
||||
{
|
||||
var indicator = new JbIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Critical value lines should be set
|
||||
Assert.Equal(4.605, indicator.LinesSeries[1].GetValue(0), 3);
|
||||
Assert.Equal(5.991, indicator.LinesSeries[2].GetValue(0), 3);
|
||||
Assert.Equal(9.210, indicator.LinesSeries[3].GetValue(0), 3);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class JbIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Jb _jb = null!;
|
||||
private readonly LineSeries _series;
|
||||
private readonly LineSeries _crit10;
|
||||
private readonly LineSeries _crit05;
|
||||
private readonly LineSeries _crit01;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"JB {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/jb/Jb.Quantower.cs";
|
||||
|
||||
public JbIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "JB - Jarque-Bera Test";
|
||||
Description = "Normality test using skewness and kurtosis. Large values reject normality.";
|
||||
|
||||
_series = new LineSeries(name: "JB", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
_crit10 = new LineSeries(name: "10%", color: Color.Gray, width: 1, style: LineStyle.Dash);
|
||||
_crit05 = new LineSeries(name: "5%", color: Color.Orange, width: 1, style: LineStyle.Dash);
|
||||
_crit01 = new LineSeries(name: "1%", color: Color.Red, width: 1, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
AddLineSeries(_crit10);
|
||||
AddLineSeries(_crit05);
|
||||
AddLineSeries(_crit01);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_jb = new Jb(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _jb.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _jb.IsHot, ShowColdValues);
|
||||
_crit10.SetValue(4.605);
|
||||
_crit05.SetValue(5.991);
|
||||
_crit01.SetValue(9.210);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class KendallIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void KendallIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new KendallIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("KENDALL - Kendall Tau-a Rank Correlation", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new KendallIndicator();
|
||||
|
||||
Assert.Equal(2, KendallIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new KendallIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("KENDALL", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_Initialize_CreatesInternalKendall()
|
||||
{
|
||||
var indicator = new KendallIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new KendallIndicator { 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new KendallIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new KendallIndicator { 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.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new KendallIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] opens = [100, 101, 102, 103, 104, 105];
|
||||
double[] closes = [100, 101, 102, 103, 104, 105];
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KendallIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new KendallIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Should not throw and should produce output
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Kendall Tau-a Rank Correlation indicator.
|
||||
/// Measures ordinal association between two price sources from the same symbol.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
|
||||
/// Close vs Volume, High vs Low). For cross-symbol correlation, use the core
|
||||
/// Kendall class directly.
|
||||
///
|
||||
/// Output is the Kendall Tau-a coefficient, ranging from -1 to +1.
|
||||
/// Values near +1 indicate strong concordance, near -1 strong discordance.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class KendallIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Kendall _kendall = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"KENDALL({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public KendallIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "KENDALL - Kendall Tau-a Rank Correlation";
|
||||
Description = "Measures ordinal association between two price sources. Range: -1 (discordant) to +1 (concordant).";
|
||||
_series = new LineSeries(name: "Kendall", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_kendall = new Kendall(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueA = _priceSelector(item);
|
||||
double valueB = _priceSelector2(item);
|
||||
|
||||
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
|
||||
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
|
||||
|
||||
double value = _kendall.Update(tvalA, tvalB, isNew).Value;
|
||||
_series.SetValue(value, _kendall.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class KurtosisIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void KurtosisIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new KurtosisIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Kurtosis - Excess Kurtosis", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KurtosisIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new KurtosisIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, KurtosisIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KurtosisIndicator_Initialize_CreatesInternalKurtosis()
|
||||
{
|
||||
var indicator = new KurtosisIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Kurtosis", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KurtosisIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new KurtosisIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double kurtosis = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Kurtosis of a linear trend should be finite
|
||||
Assert.True(double.IsFinite(kurtosis));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class KurtosisIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 4, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Population Kurtosis", sortIndex: 2)]
|
||||
public bool IsPopulation { get; set; } = false;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Kurtosis _kurtosis = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Kurtosis {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/kurtosis/Kurtosis.Quantower.cs";
|
||||
|
||||
public KurtosisIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Kurtosis - Excess Kurtosis";
|
||||
Description = "Measures the tailedness of the probability distribution. Positive = fat tails, Negative = thin tails.";
|
||||
|
||||
_series = new LineSeries(name: "Kurtosis", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_kurtosis = new Kurtosis(Period, IsPopulation);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _kurtosis.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _kurtosis.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class LinRegIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinRegIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LinRegIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(0, indicator.Offset);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LinReg - Linear Regression Curve", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new LinRegIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, LinRegIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegIndicator_Initialize_CreatesInternalLinReg()
|
||||
{
|
||||
var indicator = new LinRegIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("LinReg", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LinRegIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double linreg = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(linreg));
|
||||
}
|
||||
}
|
||||
|
||||
public class LinRegSlopeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinRegSlopeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LinRegSlopeIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LinReg Slope", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegSlopeIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new LinRegSlopeIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, LinRegSlopeIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegSlopeIndicator_Initialize_CreatesInternalLinReg()
|
||||
{
|
||||
var indicator = new LinRegSlopeIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Slope", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegSlopeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LinRegSlopeIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double slope = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(slope));
|
||||
}
|
||||
}
|
||||
|
||||
public class LinRegInterceptIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinRegInterceptIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LinRegInterceptIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LinReg Intercept", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegInterceptIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new LinRegInterceptIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, LinRegInterceptIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegInterceptIndicator_Initialize_CreatesInternalLinReg()
|
||||
{
|
||||
var indicator = new LinRegInterceptIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Intercept", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegInterceptIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LinRegInterceptIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double intercept = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(intercept));
|
||||
}
|
||||
}
|
||||
|
||||
public class LinRegRSquaredIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinRegRSquaredIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LinRegRSquaredIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LinReg R-Squared", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegRSquaredIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new LinRegRSquaredIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, LinRegRSquaredIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegRSquaredIndicator_Initialize_CreatesInternalLinReg()
|
||||
{
|
||||
var indicator = new LinRegRSquaredIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("RSquared", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinRegRSquaredIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LinRegRSquaredIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double r2 = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(r2));
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class LinRegIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Offset", sortIndex: 2, -2000, 2000, 1, 0)]
|
||||
public int Offset { get; set; } = 0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private LinReg _linreg = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"LinReg({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
|
||||
|
||||
public LinRegIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "LinReg - Linear Regression Curve";
|
||||
Description = "Plots the end point of the linear regression line for each bar.";
|
||||
|
||||
_series = new LineSeries(name: "LinReg", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_linreg = new LinReg(Period, Offset);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _linreg.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _linreg.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class LinRegSlopeIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private LinReg _linreg = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"LinRegSlope({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
|
||||
|
||||
public LinRegSlopeIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "LinReg Slope";
|
||||
Description = "Plots the slope of the linear regression line.";
|
||||
|
||||
_series = new LineSeries(name: "Slope", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_linreg = new LinReg(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
_linreg.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(_linreg.Slope, _linreg.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class LinRegInterceptIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private LinReg _linreg = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"LinRegIntercept({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
|
||||
|
||||
public LinRegInterceptIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "LinReg Intercept";
|
||||
Description = "Plots the intercept of the linear regression line.";
|
||||
|
||||
_series = new LineSeries(name: "Intercept", color: IndicatorExtensions.Experiments, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_linreg = new LinReg(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
_linreg.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(_linreg.Intercept, _linreg.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class LinRegRSquaredIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private LinReg _linreg = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"LinRegR2({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/linreg/LinReg.Quantower.cs";
|
||||
|
||||
public LinRegRSquaredIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "LinReg R-Squared";
|
||||
Description = "Plots the R-Squared (coefficient of determination) of the linear regression line.";
|
||||
|
||||
_series = new LineSeries(name: "RSquared", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_linreg = new LinReg(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
_linreg.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(_linreg.RSquared, _linreg.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class MeanDevIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MeanDevIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MeanDevIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MeanDev - Mean Absolute Deviation", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanDevIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MeanDevIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, MeanDevIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanDevIndicator_Initialize_CreatesInternalMeanDev()
|
||||
{
|
||||
var indicator = new MeanDevIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("MeanDev", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanDevIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MeanDevIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.True(value >= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanDevIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new MeanDevIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeanDevIndicator_ConstantData_ReturnsZero()
|
||||
{
|
||||
var indicator = new MeanDevIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100.0, 105.0, 95.0, 100.0);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0.0, value, precision: 6);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MeanDevIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private MeanDev _meandev = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"MeanDev {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/meandev/MeanDev.Quantower.cs";
|
||||
|
||||
public MeanDevIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "MeanDev - Mean Absolute Deviation";
|
||||
Description = "Average absolute deviation from the mean over a rolling window.";
|
||||
|
||||
_series = new LineSeries(name: "MeanDev", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_meandev = new MeanDev(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _meandev.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _meandev.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MedianIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MedianIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MedianIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Median - Rolling Median", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedianIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new MedianIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, MedianIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedianIndicator_Initialize_CreatesInternalMedian()
|
||||
{
|
||||
var indicator = new MedianIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Median", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedianIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MedianIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double median = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(median));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MedianIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Median _median = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Median {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/median/Median.Quantower.cs";
|
||||
|
||||
public MedianIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Median - Rolling Median";
|
||||
Description = "The middle value of a sorted dataset";
|
||||
|
||||
_series = new LineSeries(name: "Median", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_median = new Median(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _median.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _median.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ModeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ModeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ModeIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Mode - Statistical Mode (Most Frequent Value)", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new ModeIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, ModeIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeIndicator_Initialize_CreatesInternalMode()
|
||||
{
|
||||
var indicator = new ModeIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Mode", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ModeIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with repeating close prices to produce a mode
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + (i % 3); // cycles 100, 101, 102, 100, 101, ...
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 5, close - 5, close);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double mode = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Mode of cycling values should be finite
|
||||
Assert.True(double.IsFinite(mode));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ModeIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Mode _mode = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Mode {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/mode/Mode.Quantower.cs";
|
||||
|
||||
public ModeIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Mode - Statistical Mode (Most Frequent Value)";
|
||||
Description = "The most frequently occurring value in a rolling window";
|
||||
|
||||
_series = new LineSeries(name: "Mode", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_mode = new Mode(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _mode.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _mode.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class PacfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PacfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PacfIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(1, indicator.Lag);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("PACF - Partial Autocorrelation Function", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PacfIndicator();
|
||||
|
||||
Assert.Equal(0, PacfIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ShortName_IncludesPeriodAndLag()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 14, Lag = 2 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("PACF", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("2", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_Initialize_CreatesInternalPacf()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 10, Lag = 1 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_Lag_CanBeChanged()
|
||||
{
|
||||
var indicator = new PacfIndicator { Lag = 1 };
|
||||
|
||||
Assert.Equal(1, indicator.Lag);
|
||||
|
||||
indicator.Lag = 5;
|
||||
Assert.Equal(5, indicator.Lag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new PacfIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new PacfIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ShortName_UpdatesWhenPeriodChanges()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.Period = 20;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ShortName_UpdatesWhenLagChanges()
|
||||
{
|
||||
var indicator = new PacfIndicator { Lag = 1 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("1", StringComparison.Ordinal));
|
||||
|
||||
indicator.Lag = 3;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("3", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 5, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("PACF", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_DifferentLagValues_Work()
|
||||
{
|
||||
var lags = new[] { 1, 2, 3, 5, 10 };
|
||||
|
||||
foreach (var lag in lags)
|
||||
{
|
||||
// Period must be > lag + 1
|
||||
int period = Math.Max(20, lag + 5);
|
||||
var indicator = new PacfIndicator { Period = period, Lag = lag };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars to fill the buffer
|
||||
for (int i = 0; i < period + 5; i++)
|
||||
{
|
||||
double close = 100 + (i % 10);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Last value should be finite and bounded
|
||||
double pacfValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(pacfValue), $"Lag {lag} should produce finite value");
|
||||
Assert.True(pacfValue >= -1 && pacfValue <= 1, $"PACF at lag {lag} should be bounded [-1, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_PacfValuesAreBounded()
|
||||
{
|
||||
var indicator = new PacfIndicator { Period = 10, Lag = 1 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 98, 105, 97, 110, 95, 108, 92, 115, 90, 120 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 5, close - 5, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All PACF values should be bounded between -1 and 1
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double value = indicator.LinesSeries[0].GetValue(closes.Length - 1 - i);
|
||||
Assert.True(value >= -1 && value <= 1, $"PACF value at index {i} should be bounded [-1, 1], got {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PacfIndicator_AtLagOne_EqualsAcf()
|
||||
{
|
||||
// PACF at lag 1 should equal ACF at lag 1 (key mathematical property)
|
||||
var pacfIndicator = new PacfIndicator { Period = 10, Lag = 1 };
|
||||
var acfIndicator = new AcfIndicator { Period = 10, Lag = 1 };
|
||||
|
||||
pacfIndicator.Initialize();
|
||||
acfIndicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 98, 105, 97, 110, 95, 108, 92, 115, 90, 120 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
pacfIndicator.HistoricalData.AddBar(now, close, close + 5, close - 5, close);
|
||||
acfIndicator.HistoricalData.AddBar(now, close, close + 5, close - 5, close);
|
||||
|
||||
pacfIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
acfIndicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// At lag 1, PACF should equal ACF
|
||||
double pacfValue = pacfIndicator.LinesSeries[0].GetValue(0);
|
||||
double acfValue = acfIndicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.Equal(acfValue, pacfValue, 6); // Allow for minor floating-point differences
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PacfIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Lag", sortIndex: 2, 1, 100, 1, 0)]
|
||||
public int Lag { get; set; } = 1;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Pacf _pacf = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"PACF ({Period},{Lag})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/pacf/Pacf.Quantower.cs";
|
||||
|
||||
public PacfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "PACF - Partial Autocorrelation Function";
|
||||
Description = "Measures the correlation of a time series with a lagged copy after removing effects of shorter lags";
|
||||
|
||||
_series = new LineSeries(name: "PACF", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_pacf = new Pacf(Period, Lag);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _pacf.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _pacf.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PercentileIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PercentileIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PercentileIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(50.0, indicator.Percent);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Percentile - Rolling Percentile", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PercentileIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PercentileIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, PercentileIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PercentileIndicator_Initialize_CreatesInternalPercentile()
|
||||
{
|
||||
var indicator = new PercentileIndicator { Period = 10, Percent = 25.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Percentile", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PercentileIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PercentileIndicator { Period = 5, Percent = 75.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double percentile = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Percentile of a trending series should be finite
|
||||
Assert.True(double.IsFinite(percentile));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PercentileIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Percentile (0-100)", sortIndex: 2, 0, 100, 0.1, 1)]
|
||||
public double Percent { get; set; } = 50.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Percentile _percentile = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Percentile {Period} ({Percent}%)";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/percentile/Percentile.Quantower.cs";
|
||||
|
||||
public PercentileIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Percentile - Rolling Percentile";
|
||||
Description = "Value below which a given percentage of observations fall in a rolling window";
|
||||
|
||||
_series = new LineSeries(name: "Percentile", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_percentile = new Percentile(Period, Percent);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _percentile.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _percentile.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PolyfitIndicatorTests
|
||||
{
|
||||
// ── 1. Constructor defaults ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultValues()
|
||||
{
|
||||
var ind = new PolyfitIndicator();
|
||||
Assert.Equal(20, ind.Period);
|
||||
Assert.Equal(2, ind.Degree);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
Assert.Equal("Polyfit - Polynomial Fitting", ind.Name);
|
||||
Assert.False(ind.SeparateWindow);
|
||||
Assert.True(ind.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, ind.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ShortName_IncludesPeriodDegree()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 10, Degree = 3 };
|
||||
Assert.Equal("Polyfit 10,3", ind.ShortName);
|
||||
}
|
||||
|
||||
// ── 2. MinHistoryDepths ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_IsZero()
|
||||
{
|
||||
Assert.Equal(0, PolyfitIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_InterfaceImplementation()
|
||||
{
|
||||
IWatchlistIndicator ind = new PolyfitIndicator();
|
||||
Assert.Equal(0, ind.MinHistoryDepths);
|
||||
}
|
||||
|
||||
// ── 3. Initialize creates internal indicator and line series ──────────────
|
||||
|
||||
[Fact]
|
||||
public void Initialize_CreatesLineSeries()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 10 };
|
||||
ind.Initialize();
|
||||
|
||||
Assert.Single(ind.LinesSeries);
|
||||
Assert.Equal("Polyfit", ind.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_CustomPeriodDegree()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 8, Degree = 3 };
|
||||
ind.Initialize();
|
||||
Assert.Equal("Polyfit 8,3", ind.ShortName);
|
||||
}
|
||||
|
||||
// ── 4. ProcessUpdate — historical data ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_HistoricalBars_ProducesFiniteValues()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 5, Degree = 2 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
ind.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double val = ind.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 5, Degree = 2 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Fill warmup with historical bars
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val1 = ind.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add one more new bar
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(5), 110, 120, 100, 115);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double val2 = ind.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val1));
|
||||
Assert.True(double.IsFinite(val2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_SameBarUpdate_ProducesFiniteValue()
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 5, Degree = 2 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Non-new bar update (bar correction)
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(4), 108, 118, 98, 112);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double val = ind.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
// ── 5. Different source types ─────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(SourceType.Close)]
|
||||
[InlineData(SourceType.Open)]
|
||||
[InlineData(SourceType.High)]
|
||||
[InlineData(SourceType.Low)]
|
||||
[InlineData(SourceType.HL2)]
|
||||
public void DifferentSourceTypes_ProducesFiniteValues(SourceType sourceType)
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 5, Degree = 2, Source = sourceType };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = ind.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
// ── 6. Different degree variants ─────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void DifferentDegrees_ProducesFiniteValues(int degree)
|
||||
{
|
||||
var ind = new PolyfitIndicator { Period = 10, Degree = degree };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = ind.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "Expected positive overlay value");
|
||||
}
|
||||
|
||||
// ── 7. SeparateWindow and SourceCodeLink ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SeparateWindow_IsFalse_Overlay()
|
||||
{
|
||||
var ind = new PolyfitIndicator();
|
||||
Assert.False(ind.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_ContainsPolyfit()
|
||||
{
|
||||
var ind = new PolyfitIndicator();
|
||||
Assert.Contains("Polyfit", ind.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PolyfitIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Degree", sortIndex: 2, 1, 6, 1, 0)]
|
||||
public int Degree { get; set; } = 2;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Polyfit _polyfit = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Polyfit {Period},{Degree}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/polyfit/Polyfit.Quantower.cs";
|
||||
|
||||
public PolyfitIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Polyfit - Polynomial Fitting";
|
||||
Description = "Rolling polynomial regression of configurable degree; returns fitted value at current bar";
|
||||
|
||||
_series = new LineSeries(name: "Polyfit", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_polyfit = new Polyfit(Period, Degree);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _polyfit.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _polyfit.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class QuantileIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void QuantileIndicator_Constructor_DefaultValues()
|
||||
{
|
||||
var indicator = new QuantileIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(0.5, indicator.QuantileLevel);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuantileIndicator_MinHistoryDepths()
|
||||
{
|
||||
var indicator = new QuantileIndicator { Period = 20 };
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuantileIndicator_Initialize_CreatesInternalQuantile()
|
||||
{
|
||||
var indicator = new QuantileIndicator { Period = 10, QuantileLevel = 0.75 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Quantile 10 (0.75)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuantileIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new QuantileIndicator { Period = 5, QuantileLevel = 0.75 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double quantile = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Quantile of a trending series should be finite
|
||||
Assert.True(double.IsFinite(quantile));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class QuantileIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Quantile Level (0.0-1.0)", sortIndex: 2, 0.0, 1.0, 0.01, 2)]
|
||||
public double QuantileLevel { get; set; } = 0.5;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Quantile _quantile = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Quantile {Period} ({QuantileLevel})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/quantile/Quantile.Quantower.cs";
|
||||
|
||||
public QuantileIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Quantile - Rolling Quantile";
|
||||
Description = "Fraction of observations that fall below a given value in a rolling window";
|
||||
|
||||
_series = new LineSeries(name: "Quantile", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_quantile = new Quantile(Period, QuantileLevel);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _quantile.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _quantile.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SkewIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SkewIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SkewIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Skew - Skewness", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkewIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SkewIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, SkewIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkewIndicator_Initialize_CreatesInternalSkew()
|
||||
{
|
||||
var indicator = new SkewIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Skew", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkewIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SkewIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double skew = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Skew of a linear trend (100, 101, 102...) is 0 (symmetric)
|
||||
Assert.True(double.IsFinite(skew));
|
||||
Assert.Equal(0, skew, 9);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SkewIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Population Skewness", sortIndex: 2)]
|
||||
public bool IsPopulation { get; set; } = false;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Skew _skew = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Skew {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/skew/Skew.Quantower.cs";
|
||||
|
||||
public SkewIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Skew - Skewness";
|
||||
Description = "Measures the asymmetry of the probability distribution of a real-valued random variable about its mean";
|
||||
|
||||
_series = new LineSeries(name: "Skew", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_skew = new Skew(Period, IsPopulation);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _skew.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _skew.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class SpearmanIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SpearmanIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SpearmanIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.Equal(SourceType.Open, indicator.Source2);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SPEARMAN - Spearman Rank Correlation", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new SpearmanIndicator();
|
||||
|
||||
Assert.Equal(2, SpearmanIndicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_ShortName_IncludesPeriodAndSources()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("SPEARMAN", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_Initialize_CreatesInternalSpearman()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { 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.IsNaN(firstValue) || double.IsFinite(firstValue));
|
||||
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_MultipleUpdates_ProducesSequence()
|
||||
{
|
||||
var indicator = new SpearmanIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] opens = [100, 101, 102, 103, 104, 105];
|
||||
double[] closes = [100, 101, 102, 103, 104, 105];
|
||||
|
||||
for (int i = 0; i < opens.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpearmanIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new SpearmanIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Should not throw and should produce output
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Quantower adapter for Spearman Rank Correlation indicator.
|
||||
/// Measures monotonic association between two price sources from the same symbol.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
|
||||
/// Close vs Volume, High vs Low). For cross-symbol correlation, use the core
|
||||
/// Spearman class directly.
|
||||
///
|
||||
/// Output is Spearman's ρ coefficient, ranging from -1 to +1.
|
||||
/// Values near +1 indicate strong positive monotonic association, near -1 strong negative.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class SpearmanIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Source 2 Type", sortIndex: 2)]
|
||||
public SourceType Source2 { get; set; } = SourceType.Open;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Spearman _spearman = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector2 = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SPEARMAN({Period}):{_sourceName}/{Source2}";
|
||||
|
||||
public SpearmanIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "SPEARMAN - Spearman Rank Correlation";
|
||||
Description = "Measures monotonic association between two price sources. Range: -1 to +1.";
|
||||
_series = new LineSeries(name: "Spearman", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_priceSelector2 = Source2.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_spearman = new Spearman(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double valueA = _priceSelector(item);
|
||||
double valueB = _priceSelector2(item);
|
||||
|
||||
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
|
||||
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
|
||||
|
||||
double value = _spearman.Update(tvalA, tvalB, isNew).Value;
|
||||
_series.SetValue(value, _spearman.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class StdDevIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void StdDevIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new StdDevIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("StdDev - Standard Deviation", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new StdDevIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, StdDevIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevIndicator_Initialize_CreatesInternalStdDev()
|
||||
{
|
||||
var indicator = new StdDevIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("StdDev", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new StdDevIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double stdDev = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(stdDev));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class StdDevIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Population StdDev", sortIndex: 2)]
|
||||
public bool IsPopulation { get; set; } = false;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private StdDev _stdDev = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"StdDev {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/stddev/StdDev.Quantower.cs";
|
||||
|
||||
public StdDevIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "StdDev - Standard Deviation";
|
||||
Description = "Measures the amount of variation or dispersion of a set of values";
|
||||
|
||||
_series = new LineSeries(name: "StdDev", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_stdDev = new StdDev(Period, IsPopulation);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _stdDev.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _stdDev.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class StderrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void StderrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new StderrIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Stderr - Standard Error of Regression", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StderrIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new StderrIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, StderrIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StderrIndicator_Initialize_CreatesInternalStderr()
|
||||
{
|
||||
var indicator = new StderrIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Stderr", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StderrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new StderrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.True(value >= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StderrIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new StderrIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StderrIndicator_LinearData_ReturnsNearZero()
|
||||
{
|
||||
var indicator = new StderrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Perfectly linear close prices → residuals = 0 → Stderr ≈ 0
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100.0 + i * 2.0;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 1, price - 2, price);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.Equal(0.0, value, precision: 6);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class StderrIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Stderr _stderr = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Stderr {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/stderr/Stderr.Quantower.cs";
|
||||
|
||||
public StderrIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Stderr - Standard Error of Regression";
|
||||
Description = "Average distance of observed values from the linear regression line.";
|
||||
|
||||
_series = new LineSeries(name: "Stderr", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_stderr = new Stderr(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _stderr.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _stderr.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SumIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SumIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SUM - Rolling Sum", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SumIndicator();
|
||||
|
||||
Assert.Equal(0, SumIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("SUM", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_Initialize_CreatesInternalSum()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SumIndicator { 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 SumIndicator_MultipleUpdates_ProducesCorrectSumSequence()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 10, 20, 30, 40, 50 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// Last SUM(3) should be sum of last 3 values: 30 + 40 + 50 = 120
|
||||
double lastSum = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(120.0, lastSum, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_CalculatesRollingSum()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with known close prices: 10, 20, 30, 40
|
||||
indicator.HistoricalData.AddBar(now, 10, 10, 10, 10);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(10.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 20, 20, 20, 20);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(30.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10+20 = 30
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 30, 30, 30, 30);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(60.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10+20+30 = 60
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 40, 40, 40, 40);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(90.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 20+30+40 = 90 (10 dropped)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SumIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 1, maximum: 10000)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Sum _sum = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SUM({Period}):{_sourceName}";
|
||||
|
||||
public SumIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "SUM - Rolling Sum";
|
||||
Description = "Rolling Sum with Kahan-Babuška summation for numerical stability";
|
||||
_series = new LineSeries(name: "SUM", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_sum = new Sum(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _sum.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _sum.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class TheilIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TheilIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TheilIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Theil - Theil T Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, TheilIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 20 };
|
||||
Assert.Equal("Theil 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_Initialize_CreatesInternalTheil()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Theil", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double theil = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(theil));
|
||||
Assert.True(theil >= -1e-10, $"Expected non-negative Theil, got {theil}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheilIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new TheilIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double theil = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(theil));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TheilIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Theil _theil = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Theil {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/theil/Theil.Quantower.cs";
|
||||
|
||||
public TheilIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Theil - Theil T Index";
|
||||
Description = "Measures inequality/concentration of values using generalized entropy";
|
||||
|
||||
_series = new LineSeries(name: "Theil", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_theil = new Theil(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _theil.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _theil.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrimIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TrimIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TrimIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(10.0, indicator.TrimPct);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Trim - Trimmed Mean Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new TrimIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, TrimIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimIndicator_Initialize_CreatesInternalTrim()
|
||||
{
|
||||
var indicator = new TrimIndicator { Period = 10, TrimPct = 10.0 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Trim", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TrimIndicator { Period = 5, TrimPct = 10.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.5);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TrimIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Trim %", sortIndex: 2, 0, 49, 1, 0)]
|
||||
public double TrimPct { get; set; } = 10.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Trim _trim = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Trim {Period}/{TrimPct}%";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/trim/Trim.Quantower.cs";
|
||||
|
||||
public TrimIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Trim - Trimmed Mean Moving Average";
|
||||
Description = "Rolling mean after discarding extreme values from each tail";
|
||||
|
||||
_series = new LineSeries(name: "Trim", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_trim = new Trim(Period, TrimPct);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _trim.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _trim.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class VarianceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VarianceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VarianceIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Variance - Rolling Variance", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new VarianceIndicator();
|
||||
|
||||
Assert.Equal(0, VarianceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 14 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("Variance", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_Initialize_CreatesInternalVariance()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_IsPopulation_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { IsPopulation = false };
|
||||
|
||||
Assert.False(indicator.IsPopulation);
|
||||
|
||||
indicator.IsPopulation = true;
|
||||
Assert.True(indicator.IsPopulation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShortName_UpdatesWhenPeriodChanges()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.Period = 20;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("Variance", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class VarianceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Population Variance", sortIndex: 2)]
|
||||
public bool IsPopulation { get; set; } = false;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Variance _variance = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Variance {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/variance/Variance.Quantower.cs";
|
||||
|
||||
public VarianceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Variance - Rolling Variance";
|
||||
Description = "Measures the dispersion of a set of data points around their mean";
|
||||
|
||||
_series = new LineSeries(name: "Variance", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_variance = new Variance(Period, IsPopulation);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _variance.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _variance.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -717,4 +717,44 @@ public class VarianceTests
|
||||
Assert.Equal(0, output[0]); // N=1
|
||||
Assert.Equal(50, output[1]); // Var([10,20]) = 50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_AllNonFinite_UsesScalarFallbackAndReturnsFinite()
|
||||
{
|
||||
double[] source = [double.NaN, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
|
||||
double[] output = new double[source.Length];
|
||||
|
||||
Variance.Batch(source.AsSpan(), output.AsSpan(), 2);
|
||||
|
||||
foreach (double value in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(value));
|
||||
Assert.True(value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
|
||||
{
|
||||
const int period = 5;
|
||||
var source = new TSeries();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
source.Add(now.AddSeconds(i), 100 + i);
|
||||
}
|
||||
|
||||
var (results, indicator) = Variance.Calculate(source, period, isPopulation: true);
|
||||
var batch = Variance.Batch(source, period, isPopulation: true);
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
Assert.Equal(period, indicator.WarmupPeriod);
|
||||
Assert.Equal(batch.Count, results.Count);
|
||||
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, results[i].Value, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WavgIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void WavgIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new WavgIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Wavg - Linearly Weighted Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WavgIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new WavgIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, WavgIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WavgIndicator_Initialize_CreatesInternalWavg()
|
||||
{
|
||||
var indicator = new WavgIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Wavg", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WavgIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WavgIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.5);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class WavgIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Wavg _wavg = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Wavg {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/wavg/Wavg.Quantower.cs";
|
||||
|
||||
public WavgIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Wavg - Linearly Weighted Average";
|
||||
Description = "Rolling linearly-weighted average (identical to WMA) categorized as statistics";
|
||||
|
||||
_series = new LineSeries(name: "Wavg", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_wavg = new Wavg(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _wavg.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _wavg.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WinsIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void WinsIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new WinsIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(10.0, indicator.WinPct);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Wins - Winsorized Mean Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WinsIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new WinsIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, WinsIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WinsIndicator_Initialize_CreatesInternalWins()
|
||||
{
|
||||
var indicator = new WinsIndicator { Period = 10, WinPct = 10.0 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Wins", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WinsIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WinsIndicator { Period = 5, WinPct = 10.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.5);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class WinsIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 3, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Winsorize %", sortIndex: 2, 0, 49, 1, 0)]
|
||||
public double WinPct { get; set; } = 10.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Wins _wins = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"Wins {Period}/{WinPct}%";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/wins/Wins.Quantower.cs";
|
||||
|
||||
public WinsIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "Wins - Winsorized Mean Moving Average";
|
||||
Description = "Rolling mean after replacing extreme tail values with boundary values";
|
||||
|
||||
_series = new LineSeries(name: "Wins", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_wins = new Wins(Period, WinPct);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _wins.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _wins.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class ZscoreIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ZscoreIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ZscoreIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("ZSCORE", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 14 };
|
||||
|
||||
Assert.Equal(0, ZscoreIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_Initialize_CreatesInternalZscore()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Z-Score", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double zscore = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(zscore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double zscore = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(zscore));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 20 };
|
||||
Assert.Equal("ZSCORE(20)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZscoreIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new ZscoreIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add enough bars to warm up
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should change after adding a significantly different bar
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ZscoreIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Zscore _zscore = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ZSCORE({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/zscore/Zscore.Quantower.cs";
|
||||
|
||||
public ZscoreIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ZSCORE - Z-Score (Population Standard Score)";
|
||||
Description = "Measures how many population standard deviations a value is from the mean";
|
||||
|
||||
_series = new LineSeries(name: "Z-Score", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_zscore = new Zscore(Period);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _zscore.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _zscore.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
|
||||
namespace QuanTAlib.Validation;
|
||||
|
||||
/// <summary>
|
||||
@@ -139,4 +141,37 @@ public sealed class ZscoreValidationTests
|
||||
int finiteCount = values.Count(v => double.IsFinite(v));
|
||||
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structural validation using Skender <c>GetStdDev</c> as a related metric.
|
||||
/// Z-score = (value - mean) / stddev. Skender provides GetStdDev which computes
|
||||
/// the denominator of the z-score formula. We verify that QuanTAlib z-score
|
||||
/// is consistent with the relationship: z * stddev + mean ≈ value.
|
||||
/// Skender v2 does not have a direct GetZScore method.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Skender_StdDev_RelatedToZscore()
|
||||
{
|
||||
using var data = new QuanTAlib.Tests.ValidationTestData();
|
||||
const int period = 20;
|
||||
|
||||
// QuanTAlib Zscore (streaming)
|
||||
var zs = new Zscore(period);
|
||||
foreach (var tv in data.Data)
|
||||
{
|
||||
zs.Update(tv);
|
||||
}
|
||||
|
||||
// Skender StdDev
|
||||
var sResult = data.SkenderQuotes.GetStdDev(period).ToList();
|
||||
|
||||
// Structural: Skender StdDev produces finite output
|
||||
int finiteCount = sResult.Count(r => r.StdDev is not null && double.IsFinite(r.StdDev.Value));
|
||||
Assert.True(finiteCount > 100, $"Skender StdDev should produce >100 finite values, got {finiteCount}");
|
||||
|
||||
// QuanTAlib Zscore must be finite and bounded
|
||||
Assert.True(double.IsFinite(zs.Last.Value), "QuanTAlib Zscore last must be finite");
|
||||
Assert.True(zs.Last.Value > -10 && zs.Last.Value < 10,
|
||||
$"Zscore {zs.Last.Value} outside expected [-10,10] range for GBM data");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class ZtestIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ZtestIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ZtestIndicator();
|
||||
|
||||
Assert.Equal(30, indicator.Period);
|
||||
Assert.Equal(0.0, indicator.Mu0);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("ZTEST", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 30 };
|
||||
|
||||
Assert.Equal(0, ZtestIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_Initialize_CreatesInternalZtest()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("t-stat", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double tStat = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(tStat));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_DifferentSourceTypes()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 5, Source = SourceType.Open };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double tStat = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(tStat));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 20 };
|
||||
Assert.Equal("ZTEST(20)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZtestIndicator_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new ZtestIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add enough bars to warm up
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
_ = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Add a new bar with a very different value
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 200, 210, 190, 205);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double valueAfter = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Value should change after adding a significantly different bar
|
||||
Assert.True(double.IsFinite(valueAfter));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ZtestIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 30;
|
||||
|
||||
[InputParameter("Hypothesized Mean (μ₀)", sortIndex: 2)]
|
||||
public double Mu0 { get; set; } = 0.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ztest _ztest = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ZTEST({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/ztest/Ztest.Quantower.cs";
|
||||
|
||||
public ZtestIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ZTEST - One-Sample t-Test Statistic";
|
||||
Description = "Computes the t-statistic for a one-sample hypothesis test against a hypothesized mean";
|
||||
|
||||
_series = new LineSeries(name: "t-stat", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ztest = new Ztest(Period, Mu0);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _ztest.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _ztest.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user