Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,27 @@ public ImpliedVolatility IV(Symbol symbol, Symbol mirrorOption = null, decimal?
return iv;
}

/// <summary>
/// Creates a new JurikMovingAverage indicator.
/// </summary>
/// <param name="symbol">The symbol whose JMA we want</param>
/// <param name="period">The period of the JMA</param>
/// <param name="phase">The phase parameter (-100 to 100), controls the tradeoff between lag and overshoot</param>
/// <param name="power">The power parameter, controls smoothing aggressiveness</param>
/// <param name="resolution">The resolution</param>
/// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value)</param>
/// <returns>The JurikMovingAverage indicator for the requested symbol over the specified period</returns>
[DocumentationAttribute(Indicators)]
public JurikMovingAverage JMA(Symbol symbol, int period, decimal phase = 0, decimal power = 2,
Resolution? resolution = null, Func<IBaseData, decimal> selector = null)
{
var name = CreateIndicatorName(symbol, $"JMA({period},{phase},{power})", resolution);
var jurikMovingAverage = new JurikMovingAverage(name, period, phase, power);
InitializeIndicator(jurikMovingAverage, resolution, selector, symbol);

return jurikMovingAverage;
}

/// <summary>
/// Creates a new KaufmanAdaptiveMovingAverage indicator.
/// </summary>
Expand Down
143 changes: 143 additions & 0 deletions Indicators/JurikMovingAverage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;

namespace QuantConnect.Indicators
{
/// <summary>
/// Represents the Jurik Moving Average (JMA) indicator.
/// JMA is a three-stage adaptive filter that produces smoother output with less lag
/// than the traditional EMA by combining an adaptive EMA, Kalman-style velocity
/// estimation, and error correction.
/// Note: The original JMA algorithm is proprietary (Jurik Research). This implementation
/// follows the community-standard reverse-engineered formula used by pandas_ta,
/// TradingView, and other open-source libraries.
/// </summary>
public class JurikMovingAverage : Indicator, IIndicatorWarmUpPeriodProvider
{
private readonly int _period;
private readonly decimal _phaseRatio;
private readonly decimal _alpha;
private readonly decimal _beta;

private decimal _e0;
private decimal _e1;
private decimal _e2;
private decimal _jma;

/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => Samples >= _period;

/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod => _period;

/// <summary>
/// Initializes a new instance of the <see cref="JurikMovingAverage"/> class using the specified name and period.
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="period">The period of the JMA</param>
/// <param name="phase">The phase parameter (-100 to 100), controls the tradeoff between lag and overshoot</param>
/// <param name="power">The power parameter, controls smoothing aggressiveness</param>
public JurikMovingAverage(string name, int period, decimal phase = 0, decimal power = 2)
: base(name)
{
_period = period;

// Compute phase ratio: clamp phase to [-100, 100] range
if (phase < -100m)
{
_phaseRatio = 0.5m;
}
else if (phase > 100m)
{
_phaseRatio = 2.5m;
}
else
{
_phaseRatio = phase / 100m + 1.5m;
}

// Compute smoothing constants
_beta = 0.45m * (_period - 1) / (0.45m * (_period - 1) + 2m);
_alpha = (decimal)Math.Pow((double)_beta, (double)power);
}

/// <summary>
/// Initializes a new instance of the <see cref="JurikMovingAverage"/> class using the specified period.
/// </summary>
/// <param name="period">The period of the JMA</param>
/// <param name="phase">The phase parameter (-100 to 100), controls the tradeoff between lag and overshoot</param>
/// <param name="power">The power parameter, controls smoothing aggressiveness</param>
public JurikMovingAverage(int period, decimal phase = 0, decimal power = 2)
: this($"JMA({period},{phase},{power})", period, phase, power)
{
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>A new value for this indicator</returns>
protected override decimal ComputeNextValue(IndicatorDataPoint input)
{
if (!IsReady)
{
return 0;
}

if (Samples == _period)
{
// Seed the filter with the first price
_e0 = input.Value;
_e1 = 0;
_e2 = 0;
_jma = input.Value;
return input.Value;
}

// Stage 1: Adaptive EMA
_e0 = (1 - _alpha) * input.Value + _alpha * _e0;

// Stage 2: Kalman-style velocity estimation
_e1 = (input.Value - _e0) * (1 - _beta) + _beta * _e1;

// Stage 3: Error correction with phase adjustment
var oneMinusAlpha = 1 - _alpha;
_e2 = (_e0 + _phaseRatio * _e1 - _jma) * (oneMinusAlpha * oneMinusAlpha) + _alpha * _alpha * _e2;

// Final JMA value
_jma = _jma + _e2;

return _jma;
}

/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
_e0 = 0;
_e1 = 0;
_e2 = 0;
_jma = 0;
base.Reset();
}
}
}
84 changes: 84 additions & 0 deletions Tests/Indicators/JurikMovingAverageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using NUnit.Framework;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Indicators
{
[TestFixture]
public class JurikMovingAverageTests : CommonIndicatorTests<IndicatorDataPoint>
{
protected override IndicatorBase<IndicatorDataPoint> CreateIndicator()
{
return new JurikMovingAverage(7);
}

protected override string TestFileName => "spy_jma.txt";

protected override string TestColumnName => "JMA_7";

[Test]
public void JmaComputesCorrectly()
{
// Hand-computed values for JMA(7, phase=0, power=2)
// with prices: 10, 11, 12, 11, 10, 11, 12, 13, 12, 11
var jma = new JurikMovingAverage(7, 0, 2);
var time = new DateTime(2024, 1, 1);
var prices = new decimal[] { 10, 11, 12, 11, 10, 11, 12, 13, 12, 11 };

// Feed prices and verify
for (var i = 0; i < prices.Length; i++)
{
jma.Update(time.AddDays(i), prices[i]);
}

// Bars 1-6: not ready (returns 0)
Assert.IsFalse(new JurikMovingAverage(7, 0, 2).IsReady);

// Build fresh indicator and check each step
var jma2 = new JurikMovingAverage(7, 0, 2);

// Feed first 6 bars — not ready
for (var i = 0; i < 6; i++)
{
jma2.Update(time.AddDays(i), prices[i]);
Assert.IsFalse(jma2.IsReady);
Assert.AreEqual(0m, jma2.Current.Value);
}

// Bar 7 (seed): JMA = 12.0
jma2.Update(time.AddDays(6), prices[6]);
Assert.IsTrue(jma2.IsReady);
Assert.AreEqual(12m, jma2.Current.Value);

// Bar 8: JMA ≈ 12.395300300975162
jma2.Update(time.AddDays(7), prices[7]);
Assert.AreEqual(12.395300300975162, (double)jma2.Current.Value, 1e-6,
"JMA at bar 8 should match hand-computed value");

// Bar 9: JMA ≈ 12.351126982231602
jma2.Update(time.AddDays(8), prices[8]);
Assert.AreEqual(12.351126982231602, (double)jma2.Current.Value, 1e-6,
"JMA at bar 9 should match hand-computed value");

// Bar 10: JMA ≈ 11.800059939173682
jma2.Update(time.AddDays(9), prices[9]);
Assert.AreEqual(11.800059939173682, (double)jma2.Current.Value, 1e-6,
"JMA at bar 10 should match hand-computed value");
}
}
}
Loading