mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +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,142 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class AmatIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AmatIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AmatIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.FastPeriod);
|
||||
Assert.Equal(50, indicator.SlowPeriod);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AMAT - Archer Moving Averages Trends", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 10, SlowPeriod = 50 };
|
||||
|
||||
Assert.Equal(0, AmatIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 8, SlowPeriod = 40 };
|
||||
|
||||
Assert.Contains("AMAT", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("8", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AmatIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Amat", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_Initialize_CreatesInternalAmat()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 10, SlowPeriod = 50 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// Trend + Strength = 2 line series
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 5, SlowPeriod = 10 };
|
||||
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 trend = indicator.LinesSeries[0].GetValue(0);
|
||||
double strength = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(trend));
|
||||
Assert.True(double.IsFinite(strength));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 5, SlowPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(15), 115, 125, 105, 120);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double trend = indicator.LinesSeries[0].GetValue(0);
|
||||
double strength = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(trend));
|
||||
Assert.True(double.IsFinite(strength));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_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 AmatIndicator { FastPeriod = 3, SlowPeriod = 8, 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 trend value");
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(0)),
|
||||
$"Source {source} should produce finite strength value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmatIndicator_Periods_CanBeChanged()
|
||||
{
|
||||
var indicator = new AmatIndicator { FastPeriod = 5, SlowPeriod = 20 };
|
||||
Assert.Equal(5, indicator.FastPeriod);
|
||||
Assert.Equal(20, indicator.SlowPeriod);
|
||||
|
||||
indicator.FastPeriod = 15;
|
||||
indicator.SlowPeriod = 60;
|
||||
Assert.Equal(15, indicator.FastPeriod);
|
||||
Assert.Equal(60, indicator.SlowPeriod);
|
||||
Assert.Equal(0, AmatIndicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AmatTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private readonly TSeries _testData;
|
||||
|
||||
public AmatTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var bars = _gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
_testData = bars.Close;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Amat(0, 50));
|
||||
Assert.Throws<ArgumentException>(() => new Amat(-1, 50));
|
||||
Assert.Throws<ArgumentException>(() => new Amat(10, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Amat(10, -1));
|
||||
Assert.Throws<ArgumentException>(() => new Amat(50, 10)); // fast >= slow
|
||||
Assert.Throws<ArgumentException>(() => new Amat(10, 10)); // fast == slow
|
||||
|
||||
var amat = new Amat(10, 50);
|
||||
Assert.NotNull(amat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidBoundaryValues()
|
||||
{
|
||||
var amat1 = new Amat(1, 2);
|
||||
Assert.NotNull(amat1);
|
||||
Assert.Equal("Amat(1,2)", amat1.Name);
|
||||
|
||||
var amat2 = new Amat(10, 50);
|
||||
Assert.Equal("Amat(10,50)", amat2.Name);
|
||||
Assert.Equal(50, amat2.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var amat = new Amat(10, 50);
|
||||
|
||||
Assert.Equal(0, amat.Last.Value);
|
||||
|
||||
TValue result = amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(result.Value, amat.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsZero()
|
||||
{
|
||||
var amat = new Amat(10, 50);
|
||||
TValue result = amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(0.0, result.Value); // First value is 0 (neutral) - not enough data for trend
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var amat = new Amat(10, 50);
|
||||
|
||||
Assert.Equal(0, amat.Last.Value);
|
||||
Assert.False(amat.IsHot);
|
||||
Assert.Contains("Amat", amat.Name, StringComparison.Ordinal);
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(amat.Last.Value));
|
||||
Assert.True(double.IsFinite(amat.Strength.Value));
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value));
|
||||
Assert.True(double.IsFinite(amat.SlowEma.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendValues_AreValid()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
// Feed rising prices to create bullish trend
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + i * 2));
|
||||
}
|
||||
|
||||
// Trend should be +1, -1, or 0
|
||||
Assert.True(amat.Last.Value >= -1 && amat.Last.Value <= 1);
|
||||
Assert.True(Math.Abs(amat.Last.Value - (-1)) < 1e-10 || Math.Abs(amat.Last.Value) < 1e-10 || Math.Abs(amat.Last.Value - 1) < 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BullishTrend_WhenPricesRising()
|
||||
{
|
||||
var amat = new Amat(3, 10);
|
||||
|
||||
// Feed steadily rising prices
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + i * 3));
|
||||
}
|
||||
|
||||
// Should be bullish when fast EMA > slow EMA and both rising
|
||||
Assert.True(amat.FastEma.Value > amat.SlowEma.Value);
|
||||
Assert.Equal(1.0, amat.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BearishTrend_WhenPricesFalling()
|
||||
{
|
||||
var amat = new Amat(3, 10);
|
||||
|
||||
// Start with a stable price
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 200));
|
||||
}
|
||||
|
||||
// Feed steadily falling prices
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 200 - i * 3));
|
||||
}
|
||||
|
||||
// Should be bearish when fast EMA < slow EMA and both falling
|
||||
Assert.True(amat.FastEma.Value < amat.SlowEma.Value);
|
||||
Assert.Equal(-1.0, amat.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var amat = new Amat(10, 50);
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = amat.Last.Value;
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = amat.Last.Value;
|
||||
|
||||
// Values may or may not change depending on trend conditions
|
||||
Assert.True(double.IsFinite(value1));
|
||||
Assert.True(double.IsFinite(value2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
// Build up some history
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
|
||||
double emaBeforeUpdate = amat.FastEma.Value;
|
||||
|
||||
// Update with new value (isNew=false should update but allow rollback)
|
||||
amat.Update(new TValue(DateTime.UtcNow, 200), isNew: false);
|
||||
double emaAfterUpdate = amat.FastEma.Value;
|
||||
|
||||
Assert.NotEqual(emaBeforeUpdate, emaAfterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
// Feed 15 new values
|
||||
TValue fifteenthInput = default;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var bar = _gbm.Next(isNew: true);
|
||||
fifteenthInput = new TValue(bar.Time, bar.Close);
|
||||
amat.Update(fifteenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 15 values
|
||||
double stateAfterFifteen = amat.FastEma.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = _gbm.Next(isNew: false);
|
||||
amat.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 15th input again with isNew=false
|
||||
amat.Update(fifteenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 15 values
|
||||
Assert.Equal(stateAfterFifteen, amat.FastEma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var amat = new Amat(10, 50);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
}
|
||||
double fastEmaBefore = amat.FastEma.Value;
|
||||
|
||||
amat.Reset();
|
||||
|
||||
Assert.Equal(0, amat.Last.Value);
|
||||
Assert.Equal(0, amat.Strength.Value);
|
||||
Assert.Equal(0, amat.FastEma.Value);
|
||||
Assert.Equal(0, amat.SlowEma.Value);
|
||||
Assert.False(amat.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
amat.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, amat.FastEma.Value);
|
||||
Assert.NotEqual(fastEmaBefore, amat.FastEma.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var amat = new Amat(5, 20);
|
||||
|
||||
Assert.False(amat.IsHot);
|
||||
|
||||
// Feed values until warmup complete
|
||||
int count = 0;
|
||||
while (!amat.IsHot && count < 200)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + count));
|
||||
count++;
|
||||
}
|
||||
|
||||
Assert.True(amat.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
amat.Update(new TValue(DateTime.UtcNow, 110));
|
||||
_ = amat.FastEma.Value;
|
||||
|
||||
var resultAfterNaN = amat.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value));
|
||||
Assert.True(double.IsFinite(amat.SlowEma.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
amat.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterPosInf = amat.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value));
|
||||
|
||||
var resultAfterNegInf = amat.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
amat.Update(new TValue(DateTime.UtcNow, 110));
|
||||
amat.Update(new TValue(DateTime.UtcNow, 120));
|
||||
|
||||
var r1 = amat.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = amat.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = amat.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
Assert.True(double.IsFinite(r3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var amatIterative = new Amat(10, 30);
|
||||
var amatBatch = new Amat(10, 30);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new List<double>();
|
||||
foreach (var item in _testData)
|
||||
{
|
||||
iterativeResults.Add(amatIterative.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = amatBatch.Update(_testData);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i], batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
int slowPeriod = 30;
|
||||
|
||||
// 1. Batch Mode (static method)
|
||||
var batchSeries = Amat.Batch(_testData, fastPeriod, slowPeriod);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode (static method with spans)
|
||||
var tValues = _testData.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Amat.Batch(spanInput, spanOutput, fastPeriod, slowPeriod);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode (instance, one value at a time)
|
||||
var streamingInd = new Amat(fastPeriod, slowPeriod);
|
||||
for (int i = 0; i < _testData.Count; i++)
|
||||
{
|
||||
streamingInd.Update(_testData[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode (chained via ITValuePublisher)
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Amat(pubSource, fastPeriod, slowPeriod);
|
||||
for (int i = 0; i < _testData.Count; i++)
|
||||
{
|
||||
pubSource.Add(_testData[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert all modes produce identical results
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] trend = new double[5];
|
||||
double[] strength = new double[5];
|
||||
double[] wrongSize = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Batch(source.AsSpan(), wrongSize.AsSpan(), strength.AsSpan(), 5, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Batch(source.AsSpan(), trend.AsSpan(), wrongSize.AsSpan(), 5, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 0, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 10, 5)); // fast >= slow
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
double[] source = _testData.Values.ToArray();
|
||||
double[] trend = new double[source.Length];
|
||||
|
||||
var tseriesResult = Amat.Batch(_testData, 10, 30);
|
||||
Amat.Batch(source.AsSpan(), trend.AsSpan(), 10, 30);
|
||||
|
||||
// Since trend values are discrete (-1, 0, 1), check after warmup where
|
||||
// both methods should converge. Early values may differ due to EMA initialization.
|
||||
int warmup = 30 * 2; // Allow extra warmup
|
||||
int matched = 0;
|
||||
for (int i = warmup; i < source.Length; i++)
|
||||
{
|
||||
if (Math.Abs(tseriesResult[i].Value - trend[i]) < 0.01)
|
||||
{
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
// At least 95% of values after warmup should match
|
||||
double matchRate = (double)matched / (source.Length - warmup);
|
||||
Assert.True(matchRate > 0.95, $"Match rate {matchRate:P1} is below 95%");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130, 140, 150, 160, 170, 180];
|
||||
double[] trend = new double[10];
|
||||
double[] strength = new double[10];
|
||||
|
||||
Amat.Batch(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 3, 5);
|
||||
|
||||
foreach (var val in trend)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
||||
}
|
||||
foreach (var val in strength)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var (results, indicator) = Amat.Calculate(_testData, 10, 30);
|
||||
|
||||
Assert.Equal(_testData.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(results.Last.Value, indicator.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var amat = new Amat(source, 10, 30);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(amat.Last.Value));
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires()
|
||||
{
|
||||
var amat = new Amat(10, 30);
|
||||
bool eventFired = false;
|
||||
amat.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatLine_ReturnsNeutral()
|
||||
{
|
||||
var amat = new Amat(5, 10);
|
||||
|
||||
// Flat prices - neither rising nor falling
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// Should be neutral (0) when EMAs are not clearly rising or falling
|
||||
Assert.Equal(0, amat.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Strength_CalculatesCorrectly()
|
||||
{
|
||||
var amat = new Amat(3, 10);
|
||||
|
||||
// Feed rising prices to create divergence
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, 100 + i * 5));
|
||||
}
|
||||
|
||||
// Strength should be positive when there's divergence
|
||||
Assert.True(amat.Strength.Value > 0);
|
||||
|
||||
// Strength formula: |fast - slow| / slow * 100
|
||||
double expectedStrength = Math.Abs(amat.FastEma.Value - amat.SlowEma.Value) / amat.SlowEma.Value * 100;
|
||||
Assert.Equal(expectedStrength, amat.Strength.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for AMAT (Archer Moving Averages Trends).
|
||||
///
|
||||
/// AMAT is a custom indicator not found in external libraries like TA-Lib, Skender, Tulip, or Ooples.
|
||||
/// Instead, we validate:
|
||||
/// 1. The underlying EMA calculations match external libraries
|
||||
/// 2. The trend logic produces expected results for known input patterns
|
||||
/// 3. Cross-validation between streaming and batch modes
|
||||
/// </summary>
|
||||
public sealed class AmatValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AmatValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that AMAT's Fast EMA matches Skender's EMA calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_FastEma_Against_Skender()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Calculate QuanTAlib AMAT (streaming to access FastEma)
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
var qFastEma = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amat.Update(item);
|
||||
qFastEma.Add(amat.FastEma.Value);
|
||||
}
|
||||
|
||||
// Calculate Skender EMA (fast period)
|
||||
var sResult = _testData.SkenderQuotes.GetEma(fastPeriod).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qFastEma, sResult, (s) => s.Ema);
|
||||
|
||||
_output.WriteLine($"AMAT Fast EMA (period {fastPeriod}) validated successfully against Skender");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that AMAT's Slow EMA matches Skender's EMA calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_SlowEma_Against_Skender()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Calculate QuanTAlib AMAT (streaming to access SlowEma)
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
var qSlowEma = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amat.Update(item);
|
||||
qSlowEma.Add(amat.SlowEma.Value);
|
||||
}
|
||||
|
||||
// Calculate Skender EMA (slow period)
|
||||
var sResult = _testData.SkenderQuotes.GetEma(slowPeriod).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qSlowEma, sResult, (s) => s.Ema);
|
||||
|
||||
_output.WriteLine($"AMAT Slow EMA (period {slowPeriod}) validated successfully against Skender");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that AMAT's Fast EMA matches TA-Lib's EMA calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_FastEma_Against_Talib()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Prepare data for TA-Lib
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] outEma = new double[tData.Length];
|
||||
|
||||
// Calculate QuanTAlib AMAT (streaming to access FastEma)
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
var qFastEma = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amat.Update(item);
|
||||
qFastEma.Add(amat.FastEma.Value);
|
||||
}
|
||||
|
||||
// Calculate TA-Lib EMA (fast period)
|
||||
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, outEma, out var outRange, fastPeriod);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.EmaLookback(fastPeriod);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qFastEma, outEma, outRange, lookback);
|
||||
|
||||
_output.WriteLine($"AMAT Fast EMA (period {fastPeriod}) validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that AMAT's Slow EMA matches TA-Lib's EMA calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_SlowEma_Against_Talib()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Prepare data for TA-Lib
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] outEma = new double[tData.Length];
|
||||
|
||||
// Calculate QuanTAlib AMAT (streaming to access SlowEma)
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
var qSlowEma = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amat.Update(item);
|
||||
qSlowEma.Add(amat.SlowEma.Value);
|
||||
}
|
||||
|
||||
// Calculate TA-Lib EMA (slow period)
|
||||
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, outEma, out var outRange, slowPeriod);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.EmaLookback(slowPeriod);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qSlowEma, outEma, outRange, lookback);
|
||||
|
||||
_output.WriteLine($"AMAT Slow EMA (period {slowPeriod}) validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates trend logic: Rising prices should eventually produce bullish signal (+1).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_BullishTrend_Logic()
|
||||
{
|
||||
const int fastPeriod = 5;
|
||||
const int slowPeriod = 10;
|
||||
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
|
||||
// Create steadily rising prices - should produce bullish trend
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + i; // Steadily increasing
|
||||
amat.Update(new TValue(time.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
// After warmup, a steadily rising market should be bullish
|
||||
Assert.Equal(1.0, amat.Last.Value);
|
||||
Assert.True(amat.Strength.Value > 0, "Strength should be positive");
|
||||
Assert.True(amat.FastEma.Value > amat.SlowEma.Value, "Fast EMA should be above Slow EMA in uptrend");
|
||||
|
||||
_output.WriteLine($"Bullish trend logic validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates trend logic: Falling prices should eventually produce bearish signal (-1).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_BearishTrend_Logic()
|
||||
{
|
||||
const int fastPeriod = 5;
|
||||
const int slowPeriod = 10;
|
||||
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
|
||||
// Create steadily falling prices - should produce bearish trend
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 200 - i; // Steadily decreasing
|
||||
amat.Update(new TValue(time.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
// After warmup, a steadily falling market should be bearish
|
||||
Assert.Equal(-1.0, amat.Last.Value);
|
||||
Assert.True(amat.Strength.Value > 0, "Strength should be positive");
|
||||
Assert.True(amat.FastEma.Value < amat.SlowEma.Value, "Fast EMA should be below Slow EMA in downtrend");
|
||||
|
||||
_output.WriteLine($"Bearish trend logic validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates trend logic: Flat prices should produce neutral signal (0).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_NeutralTrend_Logic()
|
||||
{
|
||||
const int fastPeriod = 5;
|
||||
const int slowPeriod = 10;
|
||||
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
|
||||
// Create flat prices - should produce neutral trend
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
amat.Update(new TValue(time.AddMinutes(i), 100.0)); // Constant price
|
||||
}
|
||||
|
||||
// Flat market: EMAs converge, no clear direction
|
||||
Assert.Equal(0.0, amat.Last.Value);
|
||||
Assert.True(amat.Strength.Value < 1.0, "Strength should be near zero for flat market");
|
||||
|
||||
_output.WriteLine($"Neutral trend logic validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates trend transition from bullish to bearish.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_TrendTransition_BullishToBearish()
|
||||
{
|
||||
const int fastPeriod = 5;
|
||||
const int slowPeriod = 10;
|
||||
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Phase 1: Rising prices
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double price = 100 + i;
|
||||
amat.Update(new TValue(time.AddMinutes(i), price));
|
||||
}
|
||||
double bullishTrend = amat.Last.Value;
|
||||
|
||||
// Phase 2: Falling prices (reversal)
|
||||
for (int i = 50; i < 150; i++)
|
||||
{
|
||||
double price = 150 - (i - 50) * 2; // Fall faster than rise
|
||||
amat.Update(new TValue(time.AddMinutes(i), price));
|
||||
}
|
||||
double bearishTrend = amat.Last.Value;
|
||||
|
||||
Assert.Equal(1.0, bullishTrend);
|
||||
Assert.Equal(-1.0, bearishTrend);
|
||||
|
||||
_output.WriteLine($"Trend transition validated: Bullish({bullishTrend}) -> Bearish({bearishTrend})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that streaming and batch modes produce identical results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Streaming_Matches_Batch()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Calculate streaming
|
||||
var amatStreaming = new Amat(fastPeriod, slowPeriod);
|
||||
var streamingResults = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amatStreaming.Update(item);
|
||||
streamingResults.Add(amatStreaming.Last.Value);
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Amat.Batch(_testData.Data, fastPeriod, slowPeriod);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(streamingResults.Count, batchResults.Count);
|
||||
|
||||
int matchCount = 0;
|
||||
int totalCount = streamingResults.Count;
|
||||
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (Math.Abs(streamingResults[i] - batchResults[i].Value) < 1e-10)
|
||||
{
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
double matchRate = (double)matchCount / totalCount;
|
||||
Assert.True(matchRate > 0.99, $"Expected >99% match rate, got {matchRate:P2}");
|
||||
|
||||
_output.WriteLine($"Streaming vs Batch validation: {matchRate:P2} match rate ({matchCount}/{totalCount})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that span-based Calculate matches streaming results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Span_Matches_Streaming()
|
||||
{
|
||||
const int fastPeriod = 10;
|
||||
const int slowPeriod = 50;
|
||||
|
||||
// Calculate streaming
|
||||
var amatStreaming = new Amat(fastPeriod, slowPeriod);
|
||||
var streamingTrend = new List<double>();
|
||||
var streamingStrength = new List<double>();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amatStreaming.Update(item);
|
||||
streamingTrend.Add(amatStreaming.Last.Value);
|
||||
streamingStrength.Add(amatStreaming.Strength.Value);
|
||||
}
|
||||
|
||||
// Calculate span
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] spanTrend = new double[sourceData.Length];
|
||||
double[] spanStrength = new double[sourceData.Length];
|
||||
Amat.Batch(sourceData, spanTrend, spanStrength, fastPeriod, slowPeriod);
|
||||
|
||||
// Compare trend values (after warmup period)
|
||||
int warmup = slowPeriod * 2; // Allow extra warmup for convergence
|
||||
int trendMatchCount = 0;
|
||||
int strengthMatchCount = 0;
|
||||
int totalCount = sourceData.Length - warmup;
|
||||
|
||||
for (int i = warmup; i < sourceData.Length; i++)
|
||||
{
|
||||
if (Math.Abs(streamingTrend[i] - spanTrend[i]) < 1e-10)
|
||||
{
|
||||
trendMatchCount++;
|
||||
}
|
||||
if (Math.Abs(streamingStrength[i] - spanStrength[i]) < 1e-6)
|
||||
{
|
||||
strengthMatchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
double trendMatchRate = (double)trendMatchCount / totalCount;
|
||||
double strengthMatchRate = (double)strengthMatchCount / totalCount;
|
||||
|
||||
Assert.True(trendMatchRate > 0.95, $"Expected >95% trend match rate after warmup, got {trendMatchRate:P2}");
|
||||
Assert.True(strengthMatchRate > 0.95, $"Expected >95% strength match rate after warmup, got {strengthMatchRate:P2}");
|
||||
|
||||
_output.WriteLine("Streaming vs Span validation:");
|
||||
_output.WriteLine($" Trend: {trendMatchRate:P2} match rate ({trendMatchCount}/{totalCount})");
|
||||
_output.WriteLine($" Strength: {strengthMatchRate:P2} match rate ({strengthMatchCount}/{totalCount})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates strength calculation is correct.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Strength_Calculation()
|
||||
{
|
||||
const int fastPeriod = 5;
|
||||
const int slowPeriod = 10;
|
||||
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
|
||||
// Create scenario where we can predict the strength
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + i;
|
||||
amat.Update(new TValue(time.AddMinutes(i), price));
|
||||
}
|
||||
|
||||
// Verify strength formula: |Fast - Slow| / Slow * 100
|
||||
double expectedStrength = Math.Abs(amat.FastEma.Value - amat.SlowEma.Value) / amat.SlowEma.Value * 100.0;
|
||||
Assert.Equal(expectedStrength, amat.Strength.Value, 10);
|
||||
|
||||
_output.WriteLine($"Strength calculation validated: {amat.Strength.Value:F4}%");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates multiple period combinations.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(5, 10)]
|
||||
[InlineData(10, 20)]
|
||||
[InlineData(12, 26)]
|
||||
[InlineData(20, 50)]
|
||||
[InlineData(50, 100)]
|
||||
public void Validate_Multiple_Period_Combinations(int fastPeriod, int slowPeriod)
|
||||
{
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
|
||||
// Feed data
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
amat.Update(item);
|
||||
}
|
||||
|
||||
// Verify output is valid
|
||||
Assert.True(amat.Last.Value >= -1.0 && amat.Last.Value <= 1.0,
|
||||
$"Trend should be -1, 0, or 1, got {amat.Last.Value}");
|
||||
Assert.True(amat.Strength.Value >= 0, "Strength should be non-negative");
|
||||
Assert.True(double.IsFinite(amat.FastEma.Value), "FastEma should be finite");
|
||||
Assert.True(double.IsFinite(amat.SlowEma.Value), "SlowEma should be finite");
|
||||
Assert.True(amat.IsHot, "Indicator should be hot after processing data");
|
||||
|
||||
_output.WriteLine($"Period combination ({fastPeriod}, {slowPeriod}) validated: Trend={amat.Last.Value}, Strength={amat.Strength.Value:F2}%");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user