mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,482 @@
|
||||
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.Calculate(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.Calculate(source.AsSpan(), wrongSize.AsSpan(), strength.AsSpan(), 5, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Calculate(source.AsSpan(), trend.AsSpan(), wrongSize.AsSpan(), 5, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Calculate(source.AsSpan(), trend.AsSpan(), strength.AsSpan(), 0, 10));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Amat.Calculate(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.Calculate(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.Calculate(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(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(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.Calculate(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}%");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AMAT: Archer Moving Averages Trends
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AMAT is a trend identification system that uses multiple EMAs to identify
|
||||
/// trend direction and strength. Unlike simple crossovers, AMAT requires alignment
|
||||
/// of both fast and slow moving averages in the same direction.
|
||||
///
|
||||
/// Calculation:
|
||||
/// 1. Calculate Fast and Slow EMAs
|
||||
/// 2. Bullish (+1): Fast EMA > Slow EMA AND Fast EMA rising AND Slow EMA rising
|
||||
/// 3. Bearish (-1): Fast EMA < Slow EMA AND Fast EMA falling AND Slow EMA falling
|
||||
/// 4. Neutral (0): Mixed conditions
|
||||
/// 5. Strength = |Fast EMA - Slow EMA| / Slow EMA * 100
|
||||
///
|
||||
/// Key features:
|
||||
/// - Direction alignment reduces false signals
|
||||
/// - Trend strength measurement for conviction assessment
|
||||
/// - Clear +1/-1/0 trend signals
|
||||
///
|
||||
/// Sources:
|
||||
/// Tom Joseph (2009), based on Mark Whistler (Archer) concepts
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Amat : ITValuePublisher, IDisposable
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double FastEma,
|
||||
double SlowEma,
|
||||
double FastE,
|
||||
double SlowE,
|
||||
double PrevFastEma,
|
||||
double PrevSlowEma,
|
||||
bool FastIsHot,
|
||||
bool SlowIsHot,
|
||||
bool FastIsCompensated,
|
||||
bool SlowIsCompensated,
|
||||
int TickCount)
|
||||
{
|
||||
public static State New() => new()
|
||||
{
|
||||
FastEma = 0,
|
||||
SlowEma = 0,
|
||||
FastE = 1.0,
|
||||
SlowE = 1.0,
|
||||
PrevFastEma = 0,
|
||||
PrevSlowEma = 0,
|
||||
FastIsHot = false,
|
||||
SlowIsHot = false,
|
||||
FastIsCompensated = false,
|
||||
SlowIsCompensated = false,
|
||||
TickCount = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly double _fastAlpha;
|
||||
private readonly double _slowAlpha;
|
||||
private readonly double _fastDecay;
|
||||
private readonly double _slowDecay;
|
||||
|
||||
private State _state = State.New();
|
||||
private State _p_state = State.New();
|
||||
private double _lastValidValue;
|
||||
private double _p_lastValidValue;
|
||||
private ITValuePublisher? _source;
|
||||
private bool _disposed;
|
||||
|
||||
private const double COVERAGE_THRESHOLD = 0.05;
|
||||
private const double COMPENSATOR_THRESHOLD = 1e-10;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a new TValue is available.
|
||||
/// </summary>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current trend direction: +1 (bullish), -1 (bearish), 0 (neutral).
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current trend strength as percentage: |Fast - Slow| / Slow * 100.
|
||||
/// </summary>
|
||||
public TValue Strength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current Fast EMA value.
|
||||
/// </summary>
|
||||
public TValue FastEma { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current Slow EMA value.
|
||||
/// </summary>
|
||||
public TValue SlowEma { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if both EMAs have warmed up and are providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.FastIsHot && _state.SlowIsHot;
|
||||
|
||||
/// <summary>
|
||||
/// The number of bars required for the indicator to warm up.
|
||||
/// </summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates AMAT with specified fast and slow periods.
|
||||
/// </summary>
|
||||
/// <param name="fastPeriod">Fast EMA period (must be > 0)</param>
|
||||
/// <param name="slowPeriod">Slow EMA period (must be > fast period)</param>
|
||||
public Amat(int fastPeriod = 10, int slowPeriod = 50)
|
||||
{
|
||||
if (fastPeriod <= 0)
|
||||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||||
if (slowPeriod <= 0)
|
||||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||||
if (fastPeriod >= slowPeriod)
|
||||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||||
|
||||
_fastAlpha = 2.0 / (fastPeriod + 1);
|
||||
_slowAlpha = 2.0 / (slowPeriod + 1);
|
||||
_fastDecay = 1.0 - _fastAlpha;
|
||||
_slowDecay = 1.0 - _slowAlpha;
|
||||
|
||||
Name = $"Amat({fastPeriod},{slowPeriod})";
|
||||
WarmupPeriod = slowPeriod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates AMAT with specified source and periods.
|
||||
/// Subscribes to source.Pub event.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="fastPeriod">Fast EMA period</param>
|
||||
/// <param name="slowPeriod">Slow EMA period</param>
|
||||
public Amat(ITValuePublisher source, int fastPeriod = 10, int slowPeriod = 50)
|
||||
: this(fastPeriod, slowPeriod)
|
||||
{
|
||||
_source = source;
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources and unsubscribes from the source publisher.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (_source != null)
|
||||
{
|
||||
_source.Pub -= Handle;
|
||||
_source = null;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the AMAT state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_state = State.New();
|
||||
_p_state = State.New();
|
||||
_lastValidValue = 0;
|
||||
_p_lastValidValue = 0;
|
||||
Last = default;
|
||||
Strength = default;
|
||||
FastEma = default;
|
||||
SlowEma = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_lastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _lastValidValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a single value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True if this is a new bar, False if it's an update to the last bar</param>
|
||||
/// <returns>Updated trend value (+1, -1, or 0)</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_p_lastValidValue = _lastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_lastValidValue = _p_lastValidValue;
|
||||
}
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
// Store previous EMA values before update
|
||||
double prevFast = _state.FastEma;
|
||||
double prevSlow = _state.SlowEma;
|
||||
|
||||
// Extract state fields to local variables (record struct properties cannot be passed by ref)
|
||||
double fastEmaState = _state.FastEma;
|
||||
double fastE = _state.FastE;
|
||||
bool fastIsHot = _state.FastIsHot;
|
||||
bool fastIsCompensated = _state.FastIsCompensated;
|
||||
|
||||
double slowEmaState = _state.SlowEma;
|
||||
double slowE = _state.SlowE;
|
||||
bool slowIsHot = _state.SlowIsHot;
|
||||
bool slowIsCompensated = _state.SlowIsCompensated;
|
||||
|
||||
int tickCount = _state.TickCount;
|
||||
|
||||
// Compute Fast EMA with compensation
|
||||
double fastEma = ComputeEma(val, _fastAlpha, _fastDecay,
|
||||
ref fastEmaState, ref fastE, ref fastIsHot, ref fastIsCompensated);
|
||||
|
||||
// Compute Slow EMA with compensation
|
||||
double slowEma = ComputeEma(val, _slowAlpha, _slowDecay,
|
||||
ref slowEmaState, ref slowE, ref slowIsHot, ref slowIsCompensated);
|
||||
|
||||
// Update state with new values
|
||||
_state = new State(
|
||||
FastEma: fastEmaState,
|
||||
SlowEma: slowEmaState,
|
||||
FastE: fastE,
|
||||
SlowE: slowE,
|
||||
PrevFastEma: tickCount > 0 ? prevFast : 0,
|
||||
PrevSlowEma: tickCount > 0 ? prevSlow : 0,
|
||||
FastIsHot: fastIsHot,
|
||||
SlowIsHot: slowIsHot,
|
||||
FastIsCompensated: fastIsCompensated,
|
||||
SlowIsCompensated: slowIsCompensated,
|
||||
TickCount: tickCount + 1
|
||||
);
|
||||
|
||||
// Determine trend direction
|
||||
double trend = 0;
|
||||
double strength = 0;
|
||||
|
||||
if (_state.TickCount >= 2) // Need at least 2 ticks to compare previous values
|
||||
{
|
||||
double prevFastCompensated = GetCompensatedValue(_state.PrevFastEma, _state.FastE * (1.0 / _fastDecay), _state.FastIsCompensated);
|
||||
double prevSlowCompensated = GetCompensatedValue(_state.PrevSlowEma, _state.SlowE * (1.0 / _slowDecay), _state.SlowIsCompensated);
|
||||
|
||||
bool fastAboveSlow = fastEma > slowEma;
|
||||
bool fastRising = fastEma > prevFastCompensated;
|
||||
bool slowRising = slowEma > prevSlowCompensated;
|
||||
bool fastFalling = fastEma < prevFastCompensated;
|
||||
bool slowFalling = slowEma < prevSlowCompensated;
|
||||
|
||||
// Bullish: Fast > Slow AND both rising
|
||||
if (fastAboveSlow && fastRising && slowRising)
|
||||
{
|
||||
trend = 1.0;
|
||||
}
|
||||
// Bearish: Fast < Slow AND both falling
|
||||
else if (!fastAboveSlow && fastFalling && slowFalling)
|
||||
{
|
||||
trend = -1.0;
|
||||
}
|
||||
// Neutral: mixed conditions
|
||||
else
|
||||
{
|
||||
trend = 0;
|
||||
}
|
||||
|
||||
// Calculate strength
|
||||
if (slowEma > 0)
|
||||
{
|
||||
strength = Math.Abs(fastEma - slowEma) / slowEma * 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, trend);
|
||||
Strength = new TValue(input.Time, strength);
|
||||
FastEma = new TValue(input.Time, fastEma);
|
||||
SlowEma = new TValue(input.Time, slowEma);
|
||||
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a series of values.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Series of trend values</returns>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
|
||||
// Pre-size lists to avoid reallocations
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
tSpan[i] = source[i].Time;
|
||||
vSpan[i] = Last.Value;
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetCompensatedValue(double ema, double e, bool isCompensated)
|
||||
{
|
||||
if (isCompensated || e <= COMPENSATOR_THRESHOLD)
|
||||
return ema;
|
||||
return ema / (1.0 - e);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeEma(double input, double alpha, double decay,
|
||||
ref double ema, ref double e, ref bool isHot, ref bool isCompensated)
|
||||
{
|
||||
ema = Math.FusedMultiplyAdd(ema, decay, alpha * input);
|
||||
|
||||
double result;
|
||||
if (!isCompensated)
|
||||
{
|
||||
e *= decay;
|
||||
|
||||
if (!isHot && e <= COVERAGE_THRESHOLD)
|
||||
isHot = true;
|
||||
|
||||
if (e <= COMPENSATOR_THRESHOLD)
|
||||
{
|
||||
isCompensated = true;
|
||||
result = ema;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ema / (1.0 - e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ema;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AMAT trend values for a span of input values.
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="trend">Output trend values (+1, -1, 0)</param>
|
||||
/// <param name="strength">Output strength values (percentage)</param>
|
||||
/// <param name="fastPeriod">Fast EMA period</param>
|
||||
/// <param name="slowPeriod">Slow EMA period</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> trend, Span<double> strength,
|
||||
int fastPeriod = 10, int slowPeriod = 50)
|
||||
{
|
||||
if (source.Length != trend.Length)
|
||||
throw new ArgumentException("Source and trend must have the same length", nameof(trend));
|
||||
if (source.Length != strength.Length)
|
||||
throw new ArgumentException("Source and strength must have the same length", nameof(strength));
|
||||
if (fastPeriod <= 0)
|
||||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||||
if (slowPeriod <= 0)
|
||||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||||
if (fastPeriod >= slowPeriod)
|
||||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
double fastAlpha = 2.0 / (fastPeriod + 1);
|
||||
double slowAlpha = 2.0 / (slowPeriod + 1);
|
||||
|
||||
// Use ArrayPool for EMA buffers
|
||||
double[] fastBuffer = ArrayPool<double>.Shared.Rent(len);
|
||||
double[] slowBuffer = ArrayPool<double>.Shared.Rent(len);
|
||||
|
||||
try
|
||||
{
|
||||
Span<double> fastSpan = fastBuffer.AsSpan(0, len);
|
||||
Span<double> slowSpan = slowBuffer.AsSpan(0, len);
|
||||
|
||||
// Calculate Fast and Slow EMAs
|
||||
Ema.Batch(source, fastSpan, fastAlpha);
|
||||
Ema.Batch(source, slowSpan, slowAlpha);
|
||||
|
||||
// Calculate trend and strength
|
||||
trend[0] = 0;
|
||||
strength[0] = 0;
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double fastEma = fastSpan[i];
|
||||
double slowEma = slowSpan[i];
|
||||
double prevFastEma = fastSpan[i - 1];
|
||||
double prevSlowEma = slowSpan[i - 1];
|
||||
|
||||
bool fastAboveSlow = fastEma > slowEma;
|
||||
bool fastRising = fastEma > prevFastEma;
|
||||
bool slowRising = slowEma > prevSlowEma;
|
||||
bool fastFalling = fastEma < prevFastEma;
|
||||
bool slowFalling = slowEma < prevSlowEma;
|
||||
|
||||
// Bullish: Fast > Slow AND both rising
|
||||
if (fastAboveSlow && fastRising && slowRising)
|
||||
{
|
||||
trend[i] = 1.0;
|
||||
}
|
||||
// Bearish: Fast < Slow AND both falling
|
||||
else if (!fastAboveSlow && fastFalling && slowFalling)
|
||||
{
|
||||
trend[i] = -1.0;
|
||||
}
|
||||
// Neutral
|
||||
else
|
||||
{
|
||||
trend[i] = 0;
|
||||
}
|
||||
|
||||
// Strength
|
||||
if (slowEma > 0)
|
||||
{
|
||||
strength[i] = Math.Abs(fastEma - slowEma) / slowEma * 100.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
strength[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(fastBuffer);
|
||||
ArrayPool<double>.Shared.Return(slowBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AMAT trend values for a span (trend only, no strength).
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="trend">Output trend values (+1, -1, 0)</param>
|
||||
/// <param name="fastPeriod">Fast EMA period</param>
|
||||
/// <param name="slowPeriod">Slow EMA period</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> trend,
|
||||
int fastPeriod = 10, int slowPeriod = 50)
|
||||
{
|
||||
if (source.Length != trend.Length)
|
||||
throw new ArgumentException("Source and trend must have the same length", nameof(trend));
|
||||
if (fastPeriod <= 0)
|
||||
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
|
||||
if (slowPeriod <= 0)
|
||||
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
|
||||
if (fastPeriod >= slowPeriod)
|
||||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
double fastAlpha = 2.0 / (fastPeriod + 1);
|
||||
double slowAlpha = 2.0 / (slowPeriod + 1);
|
||||
|
||||
// Use single ArrayPool rent with slicing for both EMA buffers
|
||||
double[]? rented = ArrayPool<double>.Shared.Rent(len * 2);
|
||||
try
|
||||
{
|
||||
Span<double> buffer = rented.AsSpan(0, len * 2);
|
||||
Span<double> fastSpan = buffer.Slice(0, len);
|
||||
Span<double> slowSpan = buffer.Slice(len, len);
|
||||
|
||||
// Calculate Fast and Slow EMAs
|
||||
Ema.Batch(source, fastSpan, fastAlpha);
|
||||
Ema.Batch(source, slowSpan, slowAlpha);
|
||||
|
||||
// Calculate trend only (no strength computation needed)
|
||||
trend[0] = 0;
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double fastEma = fastSpan[i];
|
||||
double slowEma = slowSpan[i];
|
||||
double prevFastEma = fastSpan[i - 1];
|
||||
double prevSlowEma = slowSpan[i - 1];
|
||||
|
||||
bool fastAboveSlow = fastEma > slowEma;
|
||||
bool fastRising = fastEma > prevFastEma;
|
||||
bool slowRising = slowEma > prevSlowEma;
|
||||
bool fastFalling = fastEma < prevFastEma;
|
||||
bool slowFalling = slowEma < prevSlowEma;
|
||||
|
||||
// Bullish: Fast > Slow AND both rising
|
||||
if (fastAboveSlow && fastRising && slowRising)
|
||||
{
|
||||
trend[i] = 1.0;
|
||||
}
|
||||
// Bearish: Fast < Slow AND both falling
|
||||
else if (!fastAboveSlow && fastFalling && slowFalling)
|
||||
{
|
||||
trend[i] = -1.0;
|
||||
}
|
||||
// Neutral
|
||||
else
|
||||
{
|
||||
trend[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a high-performance batch calculation on history and returns
|
||||
/// a "Hot" Amat instance ready to process the next tick immediately.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical time series</param>
|
||||
/// <param name="fastPeriod">Fast EMA period</param>
|
||||
/// <param name="slowPeriod">Slow EMA period</param>
|
||||
/// <returns>A tuple containing the full calculation results and the hot indicator instance</returns>
|
||||
public static (TSeries Results, Amat Indicator) Calculate(TSeries source, int fastPeriod = 10, int slowPeriod = 50)
|
||||
{
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
TSeries results = amat.Update(source);
|
||||
return (results, amat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AMAT for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="fastPeriod">Fast EMA period</param>
|
||||
/// <param name="slowPeriod">Slow EMA period</param>
|
||||
/// <returns>AMAT trend series</returns>
|
||||
public static TSeries Batch(TSeries source, int fastPeriod = 10, int slowPeriod = 50)
|
||||
{
|
||||
var amat = new Amat(fastPeriod, slowPeriod);
|
||||
return amat.Update(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# AMAT: Archer Moving Averages Trends
|
||||
|
||||
> "Markets trend about 30% of the time. The trick isn't just finding trends—it's confirming them before your stops get hit."
|
||||
|
||||
AMAT (Archer Moving Averages Trends) is a trend identification system that uses dual EMAs to provide clear directional signals. Unlike simple moving average crossovers that generate signals on any intersection, AMAT requires **alignment** of both fast and slow averages moving in the same direction—reducing false signals during choppy, sideways markets.
|
||||
|
||||
## Historical Context
|
||||
|
||||
AMAT emerged from concepts developed by Mark Whistler (known as "Archer" in trading circles) and was formalized by Tom Joseph in 2009. The indicator addresses a fundamental problem with traditional crossover systems: they generate excessive whipsaws in ranging markets because a crossover only measures relative position, not directional agreement.
|
||||
|
||||
The innovation lies in requiring **three conditions** for a trend signal:
|
||||
|
||||
1. Relative position (fast above/below slow)
|
||||
2. Fast EMA direction (rising/falling)
|
||||
3. Slow EMA direction (rising/falling)
|
||||
|
||||
This triple-confirmation approach filters out the noise inherent in single-condition systems.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
AMAT operates on dual EMA calculations with directional analysis. The computational flow:
|
||||
|
||||
```
|
||||
Input Price
|
||||
│
|
||||
├──► Fast EMA ───► Direction (rising/falling)
|
||||
│ │
|
||||
│ ▼
|
||||
└──► Slow EMA ───► Direction (rising/falling)
|
||||
│
|
||||
▼
|
||||
Trend Logic (+1, -1, 0)
|
||||
│
|
||||
▼
|
||||
Strength = |Fast - Slow| / Slow × 100
|
||||
```
|
||||
|
||||
### Trend State Machine
|
||||
|
||||
| State | Fast vs Slow | Fast Direction | Slow Direction |
|
||||
|:------|:------------|:---------------|:---------------|
|
||||
| **Bullish (+1)** | Fast > Slow | Rising | Rising |
|
||||
| **Bearish (-1)** | Fast < Slow | Falling | Falling |
|
||||
| **Neutral (0)** | Any | Mixed | Mixed |
|
||||
|
||||
The neutral state captures market indecision: when EMAs disagree on direction or their relative position contradicts their momentum, AMAT stays flat. This is a feature, not a limitation.
|
||||
|
||||
### EMA Bias Compensation
|
||||
|
||||
QuanTAlib's implementation uses bias-compensated EMAs during the warmup phase. Traditional EMA initialization assumes the first price equals the true average—a convenient fiction. The compensator factor `e` decays exponentially:
|
||||
|
||||
$$e_{t} = e_{t-1} \times (1 - \alpha)$$
|
||||
|
||||
Until convergence, the EMA is divided by $(1 - e)$ to remove initialization bias.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. EMA Calculation
|
||||
|
||||
$$\text{EMA}_t = \alpha \times P_t + (1 - \alpha) \times \text{EMA}_{t-1}$$
|
||||
|
||||
Where $\alpha = \frac{2}{n + 1}$ and $n$ is the period.
|
||||
|
||||
### 2. Direction Detection
|
||||
|
||||
$$\text{Direction}_t = \begin{cases} \text{rising} & \text{if } \text{EMA}_t > \text{EMA}_{t-1} \\ \text{falling} & \text{if } \text{EMA}_t < \text{EMA}_{t-1} \\ \text{flat} & \text{otherwise} \end{cases}$$
|
||||
|
||||
### 3. Trend Signal
|
||||
|
||||
$$\text{Trend}_t = \begin{cases} +1 & \text{if } \text{FastEMA}_t > \text{SlowEMA}_t \land \text{FastRising} \land \text{SlowRising} \\ -1 & \text{if } \text{FastEMA}_t < \text{SlowEMA}_t \land \text{FastFalling} \land \text{SlowFalling} \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
### 4. Trend Strength
|
||||
|
||||
$$\text{Strength}_t = \frac{|\text{FastEMA}_t - \text{SlowEMA}_t|}{\text{SlowEMA}_t} \times 100$$
|
||||
|
||||
Strength quantifies the separation between EMAs as a percentage of the slow EMA—useful for gauging trend conviction or filtering weak signals.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// Standard instantiation
|
||||
var amat = new Amat(fastPeriod: 10, slowPeriod: 50);
|
||||
|
||||
// Process streaming data
|
||||
foreach (var price in prices)
|
||||
{
|
||||
amat.Update(new TValue(DateTime.UtcNow, price));
|
||||
|
||||
if (amat.Last.Value == 1.0)
|
||||
Console.WriteLine($"Bullish - Strength: {amat.Strength.Value:F2}%");
|
||||
else if (amat.Last.Value == -1.0)
|
||||
Console.WriteLine($"Bearish - Strength: {amat.Strength.Value:F2}%");
|
||||
else
|
||||
Console.WriteLine("Neutral");
|
||||
}
|
||||
|
||||
// Access individual EMAs
|
||||
double fastEma = amat.FastEma.Value;
|
||||
double slowEma = amat.SlowEma.Value;
|
||||
|
||||
// Batch processing
|
||||
var results = Amat.Batch(priceSeries, fastPeriod: 10, slowPeriod: 50);
|
||||
|
||||
// Span-based high-performance
|
||||
double[] trend = new double[prices.Length];
|
||||
double[] strength = new double[prices.Length];
|
||||
Amat.Calculate(prices.AsSpan(), trend, strength, fastPeriod: 10, slowPeriod: 50);
|
||||
```
|
||||
|
||||
### Event-Driven (Chained)
|
||||
|
||||
```csharp
|
||||
var source = new TSeries();
|
||||
var amat = new Amat(source, fastPeriod: 10, slowPeriod: 50);
|
||||
|
||||
// AMAT automatically updates when source publishes
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Console.WriteLine($"Trend: {amat.Last.Value}");
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|:----------|:-----|:--------|:------------|
|
||||
| `fastPeriod` | int | 10 | Fast EMA period (must be > 0) |
|
||||
| `slowPeriod` | int | 50 | Slow EMA period (must be > fastPeriod) |
|
||||
|
||||
### Common Period Combinations
|
||||
|
||||
| Use Case | Fast | Slow | Notes |
|
||||
|:---------|:-----|:-----|:------|
|
||||
| **Scalping** | 5 | 13 | High responsiveness, more signals |
|
||||
| **Swing** | 10 | 50 | Balanced, classic configuration |
|
||||
| **Position** | 20 | 100 | Filtered for major trends |
|
||||
| **Investment** | 50 | 200 | Long-term directional bias |
|
||||
|
||||
## Output Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|:---------|:-----|:------------|
|
||||
| `Last` | TValue | Trend direction: +1 (bullish), -1 (bearish), 0 (neutral) |
|
||||
| `Strength` | TValue | Trend strength as percentage |
|
||||
| `FastEma` | TValue | Current fast EMA value |
|
||||
| `SlowEma` | TValue | Current slow EMA value |
|
||||
| `IsHot` | bool | True when both EMAs are fully warmed |
|
||||
| `WarmupPeriod` | int | Equal to slowPeriod |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
|:-------|:------|:------|
|
||||
| **Throughput** | ~15 ns/bar | Dual EMA + direction check |
|
||||
| **Allocations** | 0 | Streaming mode is allocation-free |
|
||||
| **Complexity** | O(1) | Constant time per update |
|
||||
| **Accuracy** | 9/10 | Bias-compensated EMAs match external libs |
|
||||
| **Timeliness** | 7/10 | Triple-confirmation adds slight lag |
|
||||
| **Overshoot** | 8/10 | No overshoot; discrete {-1, 0, +1} output |
|
||||
| **Smoothness** | 6/10 | State transitions can be abrupt |
|
||||
|
||||
## Validation
|
||||
|
||||
AMAT is a custom indicator not present in standard TA libraries. Validation confirms:
|
||||
|
||||
| Component | Library | Status | Notes |
|
||||
|:----------|:--------|:-------|:------|
|
||||
| **Fast EMA** | TA-Lib | ✅ | Matches `TA_EMA` |
|
||||
| **Fast EMA** | Skender | ✅ | Matches `GetEma` |
|
||||
| **Slow EMA** | TA-Lib | ✅ | Matches `TA_EMA` |
|
||||
| **Slow EMA** | Skender | ✅ | Matches `GetEma` |
|
||||
| **Trend Logic** | Manual | ✅ | Verified against known patterns |
|
||||
| **Strength** | Manual | ✅ | Formula verification |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Expecting Continuous Signals
|
||||
|
||||
AMAT returns 0 (neutral) frequently. This is intentional—choppy markets produce neutral signals. Trading systems should respect neutral states rather than forcing a directional bias.
|
||||
|
||||
### 2. Period Selection
|
||||
|
||||
Fast periods that are too close to slow periods produce excessive neutral readings. A ratio of 1:5 (e.g., 10/50) provides reasonable separation.
|
||||
|
||||
### 3. Strength Interpretation
|
||||
|
||||
High strength doesn't guarantee trend continuation. It measures current separation, not momentum. A declining strength during a +1 trend may indicate weakening conviction.
|
||||
|
||||
### 4. Initialization Phase
|
||||
|
||||
Until `IsHot` returns true, trend signals may be unreliable. The indicator needs `slowPeriod` bars to stabilize both EMAs.
|
||||
|
||||
## See Also
|
||||
|
||||
- [EMA](../trends/ema/Ema.md) - Exponential Moving Average (AMAT's building block)
|
||||
- [MACD](../momentum/macd/Macd.md) - Another dual-EMA system with different logic
|
||||
- [ADX](../momentum/adx/Adx.md) - Trend strength without directional bias
|
||||
@@ -0,0 +1,53 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Archer Moving Averages Trends (AMAT)", "AMAT", overlay=false)
|
||||
|
||||
//@function Calculates AMAT using multiple EMAs to identify trend direction
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/amat.md
|
||||
//@param source Series to calculate AMAT from
|
||||
//@param fast Fast EMA period
|
||||
//@param slow Slow EMA period
|
||||
//@returns Tuple [bullish_count, bearish_count, trend_strength]
|
||||
amat(series float source, simple int fast = 10, simple int slow = 50) =>
|
||||
if fast <= 0 or slow <= 0
|
||||
runtime.error("Periods must be greater than 0")
|
||||
if fast >= slow
|
||||
runtime.error("Fast period must be less than slow period")
|
||||
|
||||
float alpha_fast = 2.0 / (fast + 1)
|
||||
float alpha_slow = 2.0 / (slow + 1)
|
||||
|
||||
var float ema_fast = source
|
||||
var float ema_slow = source
|
||||
var float ema_fast_prev = source
|
||||
var float ema_slow_prev = source
|
||||
|
||||
ema_fast := alpha_fast * (source - ema_fast) + ema_fast
|
||||
ema_slow := alpha_slow * (source - ema_slow) + ema_slow
|
||||
|
||||
float long_trend = ema_fast > ema_slow and ema_fast > ema_fast_prev and ema_slow > ema_slow_prev ? 1.0 : 0.0
|
||||
float short_trend = ema_fast < ema_slow and ema_fast < ema_fast_prev and ema_slow < ema_slow_prev ? -1.0 : 0.0
|
||||
|
||||
ema_fast_prev := ema_fast
|
||||
ema_slow_prev := ema_slow
|
||||
|
||||
float trend = long_trend + short_trend
|
||||
float strength = math.abs(ema_fast - ema_slow) / ema_slow * 100
|
||||
|
||||
[trend, strength, ema_fast, ema_slow]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_fast = input.int(10, "Fast Period", minval=1)
|
||||
i_slow = input.int(50, "Slow Period", minval=2)
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
[trend, strength, ema_fast, ema_slow] = amat(i_source, i_fast, i_slow)
|
||||
|
||||
// Plot
|
||||
plot(trend, "AMAT Trend", color=trend > 0 ? color.green : trend < 0 ? color.red : color.gray, style=plot.style_columns, linewidth=3)
|
||||
plot(strength, "Trend Strength %", color=color.yellow, linewidth=2)
|
||||
hline(0, "Zero", color=color.gray, linestyle=hline.style_dashed)
|
||||
Reference in New Issue
Block a user