mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HemaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HemaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HemaIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("HEMA - Exponential Hull Analog", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, HemaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("HEMA", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new HemaIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Hema.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HemaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 3 };
|
||||
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 HemaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new HemaIndicator { Period = 3 };
|
||||
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 HemaIndicator_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 HemaIndicator { Period = 3, 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HemaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hema_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hema(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hema(-1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Hema(1));
|
||||
|
||||
var hema = new Hema(2);
|
||||
Assert.Equal("Hema(2)", hema.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_BasicCalculation_ReturnsFinite()
|
||||
{
|
||||
var hema = new Hema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
int iterations = hema.WarmupPeriod + 2;
|
||||
|
||||
TValue result = default;
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
result = hema.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(hema.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_IsNewFalse_RestoresState()
|
||||
{
|
||||
var hema = new Hema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 7);
|
||||
|
||||
TValue lastInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
lastInput = new TValue(bar.Time, bar.Close);
|
||||
hema.Update(lastInput, isNew: true);
|
||||
}
|
||||
|
||||
double original = hema.Last.Value;
|
||||
var corrected = new TValue(lastInput.Time, lastInput.Value * 1.1);
|
||||
|
||||
hema.Update(corrected, isNew: false);
|
||||
hema.Update(lastInput, isNew: false);
|
||||
|
||||
Assert.Equal(original, hema.Last.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_Reset_ClearsState()
|
||||
{
|
||||
var hema = new Hema(10);
|
||||
hema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
hema.Reset();
|
||||
|
||||
Assert.Equal(default, hema.Last);
|
||||
Assert.False(hema.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_Robustness_NaNAndInfinity_UsesLastValid()
|
||||
{
|
||||
var hema = new Hema(10);
|
||||
hema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
hema.Update(new TValue(DateTime.UtcNow, 110.0));
|
||||
|
||||
TValue nanResult = hema.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
TValue posInfResult = hema.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
TValue negInfResult = hema.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
Assert.True(double.IsFinite(posInfResult.Value));
|
||||
Assert.True(double.IsFinite(negInfResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_BatchMatchesStreaming()
|
||||
{
|
||||
int period = 12;
|
||||
TSeries series = BuildSeries(120, seed: 11);
|
||||
|
||||
TSeries batch = Hema.Batch(series, period);
|
||||
var hema = new Hema(period);
|
||||
|
||||
var streamValues = new List<double>(series.Count);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamValues.Add(hema.Update(series[i]).Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, streamValues[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_SpanMatchesBatch()
|
||||
{
|
||||
int period = 16;
|
||||
TSeries series = BuildSeries(200, seed: 21);
|
||||
double[] values = series.Values.ToArray();
|
||||
var output = new double[values.Length];
|
||||
|
||||
Hema.Batch(values, output, period);
|
||||
TSeries batch = Hema.Batch(series, period);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(batch[i].Value, output[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_EventingMatchesStreaming()
|
||||
{
|
||||
int period = 8;
|
||||
var source = new TSeries();
|
||||
var hema = new Hema(source, period);
|
||||
|
||||
var eventValues = new List<double>();
|
||||
hema.Pub += (object? sender, in TValueEventArgs args) => eventValues.Add(args.Value.Value);
|
||||
|
||||
TSeries series = BuildSeries(60, seed: 32);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
source.Add(series[i]);
|
||||
}
|
||||
|
||||
var stream = new Hema(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
double expected = stream.Update(series[i]).Value;
|
||||
Assert.Equal(expected, eventValues[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_SpanValidatesOutputLength()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[3];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Hema.Batch(source, output, 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_WarmupPeriod_TransitionsIsHot()
|
||||
{
|
||||
var hema = new Hema(20);
|
||||
int warmup = hema.WarmupPeriod;
|
||||
|
||||
for (int i = 0; i < warmup - 1; i++)
|
||||
{
|
||||
hema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.False(hema.IsHot);
|
||||
}
|
||||
|
||||
hema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(hema.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_Prime_PopulatesState()
|
||||
{
|
||||
var hema = new Hema(10);
|
||||
TSeries series = BuildSeries(50, seed: 100);
|
||||
double[] values = series.Values.ToArray();
|
||||
|
||||
hema.Prime(values);
|
||||
|
||||
Assert.True(double.IsFinite(hema.Last.Value));
|
||||
Assert.True(hema.IsHot);
|
||||
}
|
||||
|
||||
private static TSeries BuildSeries(int count, int seed)
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: seed);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HemaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hema_Streaming_MatchesReference()
|
||||
{
|
||||
int period = 20;
|
||||
TSeries series = BuildSeries(300, seed: 5);
|
||||
double[] reference = new double[series.Count];
|
||||
|
||||
ReferenceHema(series.Values, reference, period);
|
||||
|
||||
var hema = new Hema(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
double actual = hema.Update(series[i]).Value;
|
||||
Assert.Equal(reference[i], actual, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_Batch_MatchesReference()
|
||||
{
|
||||
int period = 14;
|
||||
TSeries series = BuildSeries(250, seed: 9);
|
||||
double[] reference = new double[series.Count];
|
||||
|
||||
ReferenceHema(series.Values, reference, period);
|
||||
TSeries batch = Hema.Batch(series, period);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(reference[i], batch[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hema_Span_MatchesReference()
|
||||
{
|
||||
int period = 30;
|
||||
TSeries series = BuildSeries(200, seed: 12);
|
||||
double[] values = series.Values.ToArray();
|
||||
var output = new double[values.Length];
|
||||
var reference = new double[values.Length];
|
||||
|
||||
ReferenceHema(values, reference, period);
|
||||
Hema.Batch(values, output, period);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Assert.Equal(reference[i], output[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReferenceHema(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int n = Math.Max(period, 2);
|
||||
int halfN = n / 2; // integer floor, same as HMA
|
||||
int sqrtN = Math.Max((int)Math.Sqrt(n), 1); // integer floor, same as HMA
|
||||
|
||||
double aS = AlphaFromWmaLag(n);
|
||||
double aF = AlphaFromWmaLag(Math.Max(halfN, 1));
|
||||
double aM = AlphaFromWmaLag(Math.Max(sqrtN, 1));
|
||||
|
||||
double bS = 1.0 - aS;
|
||||
double bF = 1.0 - aF;
|
||||
double bM = 1.0 - aM;
|
||||
|
||||
double lagS = bS / aS;
|
||||
double lagF = bF / aF;
|
||||
double ratio = Math.Clamp(lagF / lagS, 0.0, 0.999999);
|
||||
double invOneMinusRatio = 1.0 / Math.Max(1.0 - ratio, 1e-12);
|
||||
|
||||
bool warmup = true;
|
||||
double decayS = 1.0;
|
||||
double decayF = 1.0;
|
||||
double decayM = 1.0;
|
||||
|
||||
double eSraw = 0.0;
|
||||
double eFraw = 0.0;
|
||||
double eMraw = 0.0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
if (double.IsNaN(val))
|
||||
{
|
||||
output[i] = double.NaN;
|
||||
continue;
|
||||
}
|
||||
|
||||
eSraw = Math.FusedMultiplyAdd(eSraw, bS, aS * val);
|
||||
eFraw = Math.FusedMultiplyAdd(eFraw, bF, aF * val);
|
||||
|
||||
if (warmup)
|
||||
{
|
||||
decayS *= bS;
|
||||
decayF *= bF;
|
||||
decayM *= bM;
|
||||
|
||||
double invS = 1.0 / Math.Max(1.0 - decayS, 1e-12);
|
||||
double invF = 1.0 / Math.Max(1.0 - decayF, 1e-12);
|
||||
double invM = 1.0 / Math.Max(1.0 - decayM, 1e-12);
|
||||
|
||||
double eS = eSraw * invS;
|
||||
double eF = eFraw * invF;
|
||||
double deLag = Math.FusedMultiplyAdd(-ratio, eS, eF) * invOneMinusRatio;
|
||||
|
||||
eMraw = Math.FusedMultiplyAdd(eMraw, bM, aM * deLag);
|
||||
output[i] = eMraw * invM;
|
||||
|
||||
double maxDecay = Math.Max(decayS, Math.Max(decayF, decayM));
|
||||
warmup = maxDecay > 1e-10;
|
||||
}
|
||||
else
|
||||
{
|
||||
double deLag = Math.FusedMultiplyAdd(-ratio, eSraw, eFraw) * invOneMinusRatio;
|
||||
eMraw = Math.FusedMultiplyAdd(eMraw, bM, aM * deLag);
|
||||
output[i] = eMraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double AlphaFromWmaLag(int p)
|
||||
{
|
||||
// WMA-lag-matched alpha: EMA lag = (1-α)/α = (P-1)/3
|
||||
// Solving: α = 3/(P+2)
|
||||
return 3.0 / (Math.Max(p, 1) + 2.0);
|
||||
}
|
||||
|
||||
private static TSeries BuildSeries(int count, int seed)
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: seed);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user