mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 23:58:04 +00:00
Implement Jurik Moving Average (JMA) with adaptive smoothing and comprehensive tests
This commit is contained in:
@@ -32,7 +32,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
| HPF | Ehlers Highpass Filter | |
|
||||
| HTIT | Hilbert Transform Instantaneous Trend | |
|
||||
| HWMA | Holt Weighted MA | |
|
||||
| JMA | Jurik MA | |
|
||||
| [JMA](trends/jma/Jma.md) | Jurik MA | Adaptive moving average that adjusts to market volatility for superior smoothing with minimal lag. |
|
||||
| [KAMA](trends/kama/Kama.md) | Kaufman Adaptive MA | Adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio. |
|
||||
| KF | Kalman Filter | |
|
||||
| LOESS | LOESS/LOWESS Smoothing | |
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void JmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new JmaIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(0, indicator.Phase);
|
||||
Assert.Equal(0.45, indicator.Power);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("JMA - Jurik Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 15, Phase = 50, Power = 0.8 };
|
||||
|
||||
Assert.Contains("JMA", indicator.ShortName);
|
||||
Assert.Contains("15", indicator.ShortName);
|
||||
Assert.Contains("50", indicator.ShortName);
|
||||
Assert.Contains("0.8", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new JmaIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Jma.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_Initialize_CreatesInternalJma()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_OnPaintChart_DoesNotThrow()
|
||||
{
|
||||
var indicator = new JmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var method = indicator.GetType().GetMethod("OnPaintChart");
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal(typeof(JmaIndicator), method.DeclaringType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_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 JmaIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmaIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new JmaIndicator { Period = 5, Phase = 10, Power = 0.5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
Assert.Equal(10, indicator.Phase);
|
||||
Assert.Equal(0.5, indicator.Power);
|
||||
|
||||
indicator.Period = 20;
|
||||
indicator.Phase = -10;
|
||||
indicator.Power = 0.9;
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(-10, indicator.Phase);
|
||||
Assert.Equal(0.9, indicator.Power);
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class JmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[InputParameter("Phase", sortIndex: 2, -100, 100, 1, 0)]
|
||||
public int Phase { get; set; } = 0;
|
||||
|
||||
[InputParameter("Power", sortIndex: 3, 0.1, 10.0, 0.1, 1)]
|
||||
public double Power { get; set; } = 0.45;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Jma? ma;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
private int _warmupBarIndex = -1;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"JMA {Period}:{Phase}:{Power}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/jma/Jma.Quantower.cs";
|
||||
|
||||
public JmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "JMA - Jurik Moving Average";
|
||||
Description = "Jurik Moving Average";
|
||||
Series = new(name: $"JMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Jma(Period, Phase, Power);
|
||||
SourceName = Source.ToString();
|
||||
_warmupBarIndex = -1;
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue input = this.GetInputValue(args, Source);
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
TValue result = ma!.Update(input, isNew);
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent);
|
||||
|
||||
if (_warmupBarIndex < 0 && ma!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
|
||||
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Jma_Constructor_ValidatesInput()
|
||||
{
|
||||
// JMA doesn't explicitly throw on period currently, but let's check if it handles valid inputs
|
||||
var jma = new Jma(10);
|
||||
Assert.NotNull(jma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_Calc_ReturnsValue()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
Assert.Equal(0, jma.Last.Value);
|
||||
|
||||
TValue result = jma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, jma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = jma.Last.Value;
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = jma.Last.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
jma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = jma.Last.Value;
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = jma.Last.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_Reset_ClearsState()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
jma.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = jma.Last.Value;
|
||||
|
||||
jma.Reset();
|
||||
|
||||
Assert.Equal(0, jma.Last.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
jma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, jma.Last.Value);
|
||||
Assert.NotEqual(valueBefore, jma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
Assert.False(jma.IsHot);
|
||||
|
||||
// Warmup for JMA(10) is approx 203 bars
|
||||
// ceil(20 + 80 * 10^0.36) = 203
|
||||
int warmup = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(10, 0.36));
|
||||
|
||||
for (int i = 1; i < warmup; i++)
|
||||
{
|
||||
jma.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(jma.IsHot);
|
||||
}
|
||||
|
||||
jma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(jma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 20 new values (enough to fill buffer and stabilize)
|
||||
TValue lastInput = default;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
lastInput = new TValue(bar.Time, bar.Close);
|
||||
jma.Update(lastInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember JMA state
|
||||
double jmaAfter = jma.Last.Value;
|
||||
|
||||
// Generate 5 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
jma.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered last input again with isNew=false
|
||||
TValue finalJma = jma.Update(lastInput, isNew: false);
|
||||
|
||||
// JMA should match the original state
|
||||
Assert.Equal(jmaAfter, finalJma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var jma = new Jma(10);
|
||||
|
||||
// Feed some valid values
|
||||
jma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
jma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = jma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Result should be finite (not NaN)
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate with TSeries API
|
||||
var tseriesResult = new Jma(10).Update(series);
|
||||
|
||||
// Calculate with Span API
|
||||
Jma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = new Jma(period).Update(series);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Jma.Calculate(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Jma(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Jma(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_Phase_AffectsResult()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var jmaPhase0 = new Jma(10, phase: 0).Update(series);
|
||||
var jmaPhase100 = new Jma(10, phase: 100).Update(series);
|
||||
var jmaPhaseMinus100 = new Jma(10, phase: -100).Update(series);
|
||||
|
||||
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhase100.Last.Value);
|
||||
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class JmaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Jma_FollowsPriceTrend()
|
||||
{
|
||||
// JMA should generally follow the price.
|
||||
// If price goes up, JMA should eventually go up.
|
||||
|
||||
var jma = new Jma(10);
|
||||
double previousJma = 0;
|
||||
|
||||
// Uptrend
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var result = jma.Update(new TValue(DateTime.UtcNow, i));
|
||||
if (i > 20) // Allow warmup
|
||||
{
|
||||
Assert.True(result.Value > previousJma, $"JMA should be increasing in uptrend at step {i}");
|
||||
}
|
||||
previousJma = result.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jma_WithinBounds()
|
||||
{
|
||||
// JMA should stay within the range of recent prices (roughly)
|
||||
// It's a moving average, so it shouldn't overshoot wildly unless phase is negative and high volatility?
|
||||
// With default phase 0, it should be well behaved.
|
||||
|
||||
var jma = new Jma(10);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0, sigma: 0.5);
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var result = jma.Update(new TValue(bar.Time, bar.Close));
|
||||
|
||||
if (i > 20)
|
||||
{
|
||||
// Update bounds of recent price history (simplified)
|
||||
// This is a loose check.
|
||||
// Just check it's finite and positive for this GBM
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Jurik Moving Average (JMA):
|
||||
/// - 10-bar SMA of local deviation
|
||||
/// - 128-sample volatility distribution
|
||||
/// - middle-65 trimmed mean as volatility reference
|
||||
/// - Jurik dynamic exponent and 2-pole IIR core
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Jma : ITValuePublisher
|
||||
{
|
||||
private const int VolWindowSize = 128; // volatility history length
|
||||
private const int DevWindowSize = 10; // short SMA length for deviation
|
||||
|
||||
// Jurik core parameters derived from period/phase
|
||||
private readonly double _phaseParam; // 0.5 .. 2.5
|
||||
private readonly double _logParam; // log(sqrt(L))/log(2) + 2, clamped >= 0
|
||||
private readonly double _lengthDivider; // L'/(L'+2), L' = 0.9*L
|
||||
private readonly double _logSqrtDivider; // Precomputed log(_sqrtDivider) for Exp optimization
|
||||
private readonly double _logLengthDivider; // Precomputed log(_lengthDivider) for Exp optimization
|
||||
private readonly int _warmupBars; // for IsHot
|
||||
|
||||
// Constants for trimmed mean
|
||||
private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples
|
||||
|
||||
// Buffers
|
||||
private readonly RingBuffer _devBuffer;
|
||||
private readonly RingBuffer _volBuffer;
|
||||
private readonly double[] _sorted;
|
||||
|
||||
// Streaming state (current + previous snapshot for isNew=false)
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private record struct State
|
||||
{
|
||||
// Jurik "envelope" anchors
|
||||
public double UpperBand;
|
||||
public double LowerBand;
|
||||
|
||||
// IIR filter internal state
|
||||
public double LastC0;
|
||||
public double LastC8;
|
||||
public double LastA8;
|
||||
public double LastJma;
|
||||
|
||||
// last finite price (for NaN handling)
|
||||
public double LastPrice;
|
||||
|
||||
// counters
|
||||
public int Bars;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public event Action<TValue>? Pub;
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// JMA is considered "hot" when enough bars have passed to stabilize
|
||||
/// the internal volatility distribution.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.Bars >= _warmupBars;
|
||||
|
||||
public Jma(int period, int phase = 0, double power = 0.45)
|
||||
{
|
||||
if (period < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
|
||||
// --- Phase parameter: maps -100..100 -> 0.5..2.5 (Jurik convention) ---
|
||||
if (phase < -100)
|
||||
_phaseParam = 0.5;
|
||||
else if (phase > 100)
|
||||
_phaseParam = 2.5;
|
||||
else
|
||||
_phaseParam = (phase * 0.01) + 1.5;
|
||||
|
||||
// --- Length / log / divider parameters (from decompiled JMA) ---
|
||||
// L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0)
|
||||
double lengthParam = period < 1.0000000002
|
||||
? 0.0000000001
|
||||
: (period - 1.0) / 2.0;
|
||||
|
||||
double logParam = Math.Log(Math.Sqrt(lengthParam)) / Math.Log(2.0);
|
||||
logParam = (logParam + 2.0) < 0.0 ? 0.0 : (logParam + 2.0);
|
||||
_logParam = logParam;
|
||||
|
||||
double sqrtParam = Math.Sqrt(lengthParam) * _logParam;
|
||||
lengthParam *= 0.9;
|
||||
_lengthDivider = lengthParam / (lengthParam + 2.0);
|
||||
double sqrtDivider = sqrtParam / (sqrtParam + 1.0);
|
||||
|
||||
// Precompute logs for Math.Exp optimization
|
||||
_logLengthDivider = Math.Log(_lengthDivider);
|
||||
_logSqrtDivider = Math.Log(sqrtDivider);
|
||||
|
||||
// same warmup heuristic used in the AFL port (SetBarsRequired)
|
||||
_warmupBars = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
|
||||
|
||||
Name = $"Jma({period},{phase},{power})"; // power kept for signature compatibility
|
||||
|
||||
_devBuffer = new RingBuffer(DevWindowSize);
|
||||
_volBuffer = new RingBuffer(VolWindowSize);
|
||||
_sorted = new double[VolWindowSize];
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
public Jma(ITValuePublisher source, int period, int phase = 0, double power = 0.45)
|
||||
: this(period, phase, power)
|
||||
{
|
||||
source.Pub += item => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
_devBuffer.Clear();
|
||||
_volBuffer.Clear();
|
||||
Array.Clear(_sorted, 0, _sorted.Length);
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core streaming step: feed a single value, get JMA.
|
||||
/// Honors isNew semantics by snapshotting state+buffers.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double Step(double value, bool isNew)
|
||||
{
|
||||
// --- Snapshot/rollback support for "amending" last bar ---
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
// --- Handle NaN/inf: reuse last finite price ---
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = _state.Bars > 0 ? _state.LastPrice : 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastPrice = value;
|
||||
}
|
||||
|
||||
_state.Bars++;
|
||||
|
||||
// --- First bar: initialize anchors and IIR state ---
|
||||
if (_state.Bars == 1)
|
||||
{
|
||||
_state.UpperBand = value;
|
||||
_state.LowerBand = value;
|
||||
_state.LastC0 = value;
|
||||
_state.LastC8 = 0.0;
|
||||
_state.LastA8 = 0.0;
|
||||
_state.LastJma = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
// 1. Local deviation: |price - {UpperBand, LowerBand}|
|
||||
double diffA = value - _state.UpperBand;
|
||||
double diffB = value - _state.LowerBand;
|
||||
double absA = Math.Abs(diffA);
|
||||
double absB = Math.Abs(diffB);
|
||||
double absValue = absA > absB ? absA : absB;
|
||||
double deviation = absValue + 1e-10;
|
||||
|
||||
// 2. 10-bar SMA of local deviation -> "volatility"
|
||||
_devBuffer.Add(deviation, isNew);
|
||||
double volatility = _devBuffer.Average;
|
||||
|
||||
// 3. 128-bar volatility history + middle-65 trimmed mean
|
||||
_volBuffer.Add(volatility, isNew);
|
||||
double refVolatility = CalculateTrimmedMean(volatility);
|
||||
|
||||
if (refVolatility <= 0.0)
|
||||
refVolatility = deviation;
|
||||
|
||||
// 4. Jurik dynamic exponent d from abs/refVolatility
|
||||
// d = clamp( (abs/refVolatility)^p, 1 .. logParam )
|
||||
double ratio = absValue / refVolatility;
|
||||
if (ratio < 0.0) ratio = 0.0;
|
||||
|
||||
double p = Math.Max(_logParam - 2.0, 0.5);
|
||||
double d = Math.Pow(ratio, p);
|
||||
if (d > _logParam) d = _logParam;
|
||||
if (d < 1.0) d = 1.0;
|
||||
|
||||
// 5. Update UpperBand / LowerBand using sqrtDivider ^ sqrt(d)
|
||||
// Optimization: Use Exp(log(x) * y) instead of Pow(x, y)
|
||||
double adapt = Math.Exp(_logSqrtDivider * Math.Sqrt(d));
|
||||
|
||||
_state.UpperBand = (value > _state.UpperBand) ? value : value - (value - _state.UpperBand) * adapt;
|
||||
_state.LowerBand = (value < _state.LowerBand) ? value : value - (value - _state.LowerBand) * adapt;
|
||||
|
||||
// 6. 2-pole IIR core using d as the "speed"
|
||||
// alpha = lengthDivider ^ d
|
||||
// matches the Jurik decompiled structure (fC0/fC8/fA8)
|
||||
double prevJma = _state.LastJma;
|
||||
if (double.IsNaN(prevJma) || _state.Bars == 2)
|
||||
prevJma = value;
|
||||
|
||||
double alpha = Math.Exp(_logLengthDivider * d);
|
||||
double alpha2 = alpha * alpha;
|
||||
|
||||
double c0 = (1.0 - alpha) * value + alpha * _state.LastC0;
|
||||
double c8 = (value - c0) * (1.0 - _lengthDivider) + _lengthDivider * _state.LastC8;
|
||||
double a8 = (_phaseParam * c8 + c0 - prevJma) *
|
||||
(alpha * (-2.0) + alpha2 + 1.0) +
|
||||
alpha2 * _state.LastA8;
|
||||
|
||||
double jma = prevJma + a8;
|
||||
|
||||
_state.LastC0 = c0;
|
||||
_state.LastC8 = c8;
|
||||
_state.LastA8 = a8;
|
||||
_state.LastJma = jma;
|
||||
|
||||
return jma;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double j = Step(input.Value, isNew);
|
||||
Last = new TValue(input.Time, j);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch update: recomputes JMA for entire series using the same
|
||||
/// streaming core, so results match Update(TValue) applied bar-by-bar.
|
||||
/// </summary>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
int n = source.Count;
|
||||
if (n == 0)
|
||||
return new TSeries(0);
|
||||
|
||||
var t = new List<long>(n);
|
||||
var v = new List<double>(n);
|
||||
|
||||
CollectionsMarshal.SetCount(t, n);
|
||||
CollectionsMarshal.SetCount(v, n);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Reset();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double j = Step(source.Values[i], true);
|
||||
vSpan[i] = j;
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static helper compatible with your existing signature.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source,
|
||||
Span<double> output,
|
||||
int period,
|
||||
int phase = 0,
|
||||
double power = 0.45)
|
||||
{
|
||||
if (output.Length < source.Length)
|
||||
throw new ArgumentException("output span is shorter than source span.", nameof(output));
|
||||
|
||||
var jma = new Jma(period, phase, power);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
output[i] = jma.Step(source[i], true);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateTrimmedMean(double fallback)
|
||||
{
|
||||
int count = _volBuffer.Count;
|
||||
if (count < 16)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Copy current buffer to _sorted for sorting
|
||||
_volBuffer.CopyTo(_sorted, 0);
|
||||
Array.Sort(_sorted, 0, count);
|
||||
|
||||
int start, end;
|
||||
if (count >= VolWindowSize)
|
||||
{
|
||||
// canonical JMA: central 65 of 128 -> indices 32..96
|
||||
// Approximately removes the outer 25% on each tail
|
||||
int leftSkip = (int)Math.Ceiling((VolWindowSize - JurikTrimCount) / 2.0);
|
||||
start = leftSkip;
|
||||
end = start + JurikTrimCount - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// for shorter history, use central ~50% as a reasonable proxy
|
||||
int slice = (int)Math.Max(5, Math.Round(count * 0.5));
|
||||
int drop = (count - slice) / 2;
|
||||
start = drop;
|
||||
end = drop + slice - 1;
|
||||
}
|
||||
|
||||
if (start < 0) start = 0;
|
||||
if (end >= count) end = count - 1;
|
||||
|
||||
int len = end - start + 1;
|
||||
return _sorted.AsSpan(start, len).SumSIMD() / len;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
# JMA - Jurik Moving Average
|
||||
|
||||
The Jurik Moving Average (JMA) is an advanced adaptive moving average that provides superior smoothing with minimal lag. It dynamically adjusts its response based on market volatility using a sophisticated multi-stage algorithm involving volatility distribution analysis and adaptive IIR filtering.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Volatility-Based Adaptation:** JMA uses a 128-sample volatility distribution with trimmed mean to estimate market conditions.
|
||||
- **Dynamic Exponent:** The smoothing factor adjusts automatically based on the ratio of local deviation to reference volatility.
|
||||
- **Phase Control:** Fine-tunes the balance between responsiveness and stability (-100 to +100).
|
||||
- **Minimal Lag:** Tracks price action closely while filtering noise, outperforming traditional moving averages.
|
||||
- **Warmup Period:** JMA requires approximately `20 + 80 × period^0.36` bars to stabilize its internal volatility distribution.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| Period | 10 | The base period for the moving average calculation. |
|
||||
| Phase | 0 | Phase shift (-100 to 100). Negative values reduce lag but may increase overshoot. Positive values increase smoothing and stability. |
|
||||
| Power | 0.45 | Legacy parameter kept for API compatibility. Not actively used in current implementation. |
|
||||
|
||||
## Algorithm
|
||||
|
||||
JMA employs a sophisticated multi-stage process:
|
||||
|
||||
1. **Adaptive Envelope:** Maintains upper and lower bands that adapt to price movement using dynamic smoothing.
|
||||
|
||||
2. **Local Deviation:** Calculates the maximum absolute distance between price and the envelope bands.
|
||||
|
||||
3. **Short-Term Volatility:** Computes a 10-bar simple moving average of the local deviation.
|
||||
|
||||
4. **Volatility Distribution:** Maintains a rolling 128-sample buffer of the short-term volatility values.
|
||||
|
||||
5. **Reference Volatility:** Calculates a trimmed mean of the volatility distribution:
|
||||
- Sorts the 128 samples
|
||||
- Takes the central 65 samples (indices 32-96)
|
||||
- Computes their mean, effectively removing outliers from both tails
|
||||
|
||||
6. **Dynamic Exponent:** Derives an adaptive smoothing factor:
|
||||
- Computes ratio: `local_deviation / reference_volatility`
|
||||
- Raises ratio to power `p = max(logParam - 2.0, 0.5)`
|
||||
- Clamps result between 1.0 and `logParam`
|
||||
|
||||
7. **2-Pole IIR Filter:** Applies a dual-pole Infinite Impulse Response filter using the dynamic exponent to produce the final JMA value with controlled phase shift.
|
||||
|
||||
This implementation is a high-fidelity port of the reverse-engineered JMA algorithm found in AmiBroker and MT4, optimized for performance using logarithmic transformations for power calculations.
|
||||
|
||||
## Usage
|
||||
|
||||
### Standard Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Create JMA with period 10, phase 0
|
||||
var jma = new Jma(period: 10, phase: 0);
|
||||
|
||||
// Update with new values
|
||||
var result = jma.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
// Check if the indicator has warmed up
|
||||
if (jma.IsHot)
|
||||
{
|
||||
Console.WriteLine($"JMA: {jma.Last.Value}");
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming (Event-driven)
|
||||
|
||||
```csharp
|
||||
var source = new TSeries();
|
||||
var jma = new Jma(source, period: 10);
|
||||
|
||||
source.Pub += (item) => {
|
||||
if (jma.IsHot)
|
||||
{
|
||||
Console.WriteLine($"JMA: {jma.Last.Value}");
|
||||
}
|
||||
};
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
```
|
||||
|
||||
### Batch Calculation
|
||||
|
||||
For high-performance batch processing:
|
||||
|
||||
```csharp
|
||||
double[] prices = { 100.0, 101.5, 99.8, ... };
|
||||
double[] output = new double[prices.Length];
|
||||
|
||||
Jma.Calculate(prices, output, period: 10, phase: 0);
|
||||
```
|
||||
|
||||
### Batch with TSeries
|
||||
|
||||
```csharp
|
||||
TSeries prices = GetPriceData();
|
||||
var jma = new Jma(period: 10);
|
||||
TSeries results = jma.Update(prices);
|
||||
```
|
||||
|
||||
## Key Properties
|
||||
|
||||
- **IsHot:** Returns `true` when JMA has processed enough bars to stabilize its internal volatility distribution (approximately `20 + 80 × period^0.36` bars).
|
||||
- **Last:** The most recent calculated JMA value.
|
||||
- **Name:** Identifier string in format `"Jma(period,phase,power)"`.
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Trend Identification:** Rising JMA indicates uptrend; falling JMA indicates downtrend.
|
||||
- **Dynamic Support/Resistance:** JMA often acts as adaptive support in uptrends and resistance in downtrends.
|
||||
- **Crossovers:** Price crossing above JMA can signal bullish momentum; crossing below can signal bearish momentum.
|
||||
- **Phase Adjustment:**
|
||||
- Phase < 0: More responsive, faster signals, but may overshoot
|
||||
- Phase = 0: Balanced (default)
|
||||
- Phase > 0: Smoother, more stable, but with slightly more lag
|
||||
- **Multi-Phase Ribbons:** Using multiple JMAs with different phases creates a visual "ribbon" showing trend strength and potential reversals.
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Uses `Math.Exp` optimization for power calculations (faster than `Math.Pow`)
|
||||
- Employs SIMD operations for trimmed mean calculation
|
||||
- Maintains minimal memory footprint with efficient buffer management
|
||||
- Supports `isNew` parameter for bar amendment scenarios
|
||||
|
||||
## References
|
||||
|
||||
- [Jurik Research](http://www.jurikres.com/) - Original JMA developer
|
||||
- [Pine Script Implementation](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/jma.pine)
|
||||
Reference in New Issue
Block a user