Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ZigZag (ZZ) indicator #8454

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 18 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2811,6 +2811,24 @@ public ZeroLagExponentialMovingAverage ZLEMA(Symbol symbol, int period, Resoluti
return zeroLagExponentialMovingAverage;
}

/// <summary>
/// Creates a ZigZag indicator for the specified symbol, with adjustable sensitivity and minimum trend length.
/// </summary>
/// <param name="symbol">The symbol for which to create the ZigZag indicator.</param>
/// <param name="sensitivity">The sensitivity for detecting pivots.</param>
/// <param name="minTrendLength">The minimum number of bars required for a trend before a pivot is confirmed.</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 configured ZigZag indicator.</returns>
[DocumentationAttribute(Indicators)]
public ZigZag ZZ(Symbol symbol, decimal sensitivity = 0.05m, int minTrendLength = 1, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
{
var name = CreateIndicatorName(symbol, $"ZZ({sensitivity},{minTrendLength})", resolution);
var zigZag = new ZigZag(name, sensitivity, minTrendLength);
InitializeIndicator(zigZag, resolution, selector, symbol);
return zigZag;
}

/// <summary>
/// Creates a new name for an indicator created with the convenience functions (SMA, EMA, ect...)
/// </summary>
Expand Down
208 changes: 208 additions & 0 deletions Indicators/ZigZag.cs
JosueNina marked this conversation as resolved.
Show resolved Hide resolved
JosueNina marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* 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 QuantConnect.Data;
using QuantConnect.Data.Market;

namespace QuantConnect.Indicators
{
/// <summary>
/// The ZigZag indicator identifies significant turning points in price movements,
/// filtering out noise using a sensitivity threshold and a minimum trend length.
/// It alternates between high and low pivots to determine market trends.
/// </summary>
public class ZigZag : BarIndicator, IIndicatorWarmUpPeriodProvider
{
/// <summary>
/// The most recent pivot point, represented as a bar of market data.
/// Used as a reference for calculating subsequent pivots.
/// </summary>
private IBaseDataBar _lastPivot;

/// <summary>
/// The minimum number of bars required to confirm a valid trend.
/// Ensures that minor fluctuations in price do not create false pivots.
/// </summary>
private readonly int _minTrendLength;

/// <summary>
/// The sensitivity threshold for detecting significant price movements.
/// A decimal value between 0 and 1 that determines the percentage change required
/// to recognize a new pivot.
/// </summary>
private readonly decimal _sensitivity;
JosueNina marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// A counter to track the number of bars since the last pivot was identified.
/// Used to enforce the minimum trend length requirement.
/// </summary>
private int _count;

/// <summary>
/// Tracks whether the most recent pivot was a low pivot.
/// Used to alternate between identifying high and low pivots.
/// </summary>
private bool _lastPivotWasLow;

/// <summary>
/// Stores the most recent high pivot value in the ZigZag calculation.
/// Updated whenever a valid high pivot is identified.
/// </summary>
public IndicatorBase<IndicatorDataPoint> HighPivot { get; }

/// <summary>
/// Stores the most recent low pivot value in the ZigZag calculation.
/// Updated whenever a valid low pivot is identified.
/// </summary>
public IndicatorBase<IndicatorDataPoint> LowPivot { get; }

/// <summary>
/// Represents the current type of pivot (High or Low) in the ZigZag calculation.
/// The value is updated based on the most recent pivot identified:
/// </summary>
public PivotPointType PivotType { get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="ZigZag"/> class with the specified parameters.
/// </summary>
/// <param name="name">The name of the indicator.</param>
/// <param name="sensitivity">The sensitivity threshold as a decimal value between 0 and 1.</param>
/// <param name="minTrendLength">The minimum number of bars required to form a valid trend.</param>
public ZigZag(string name, decimal sensitivity = 0.05m, int minTrendLength = 1) : base(name)
{
if (sensitivity <= 0 || sensitivity >= 1)
{
throw new ArgumentException("Sensitivity must be between 0 and 1.", nameof(sensitivity));
}

if (minTrendLength < 1)
{
throw new ArgumentException("Minimum trend length must be greater than 0.", nameof(minTrendLength));
}
HighPivot = new Identity(name + "_HighPivot");
LowPivot = new Identity(name + "_LowPivot");
_sensitivity = sensitivity;
_minTrendLength = minTrendLength;
PivotType = PivotPointType.Low;
}

/// <summary>
/// Initializes a new instance of the <see cref="ZigZag"/> class using default parameters.
/// </summary>
/// <param name="sensitivity">The sensitivity threshold as a decimal value between 0 and 1.</param>
/// <param name="minTrendLength">The minimum number of bars required to form a valid trend.</param>
public ZigZag(decimal sensitivity = 0.05m, int minTrendLength = 1)
: this($"ZZ({sensitivity},{minTrendLength})", sensitivity, minTrendLength)
{
}

/// <summary>
/// Indicates whether the indicator has enough data to produce meaningful output.
/// The indicator is considered "ready" when the number of samples exceeds the minimum trend length.
/// </summary>
public override bool IsReady => Samples > _minTrendLength;

/// <summary>
/// Gets the number of periods required for the indicator to warm up.
/// This is equal to the minimum trend length plus one additional bar for initialization.
/// </summary>
public int WarmUpPeriod => _minTrendLength + 1;

/// <summary>
/// Computes the next value of the ZigZag indicator based on the input bar.
/// Determines whether the input bar forms a new pivot or updates the current trend.
/// </summary>
/// <param name="input">The current bar of market data used for the calculation.</param>
/// <returns>
/// The value of the most recent pivot, either a high or low, depending on the current trend.
/// </returns>
protected override decimal ComputeNextValue(IBaseDataBar input)
{
if (_lastPivot == null)
{
UpdatePivot(input, true);
return decimal.Zero;
}

var currentPivot = _lastPivotWasLow ? _lastPivot.Low : _lastPivot.High;

if (_lastPivotWasLow)
{
if (input.High >= _lastPivot.Low * (1m + _sensitivity) && _count >= _minTrendLength)
{
UpdatePivot(input, false);
currentPivot = HighPivot;
}
else if (input.Low <= _lastPivot.Low)
{
UpdatePivot(input, true);
currentPivot = LowPivot;
}
}
else
{
if (input.Low <= _lastPivot.High * (1m - _sensitivity) && _count >= _minTrendLength)
{
UpdatePivot(input, true);
currentPivot = LowPivot;
}
else if (input.High >= _lastPivot.High)
{
UpdatePivot(input, false);
currentPivot = HighPivot;
}
}
_count++;
return currentPivot;
}

/// <summary>
/// Updates the pivot point based on the given input bar.
/// If a change in trend is detected, the pivot type is switched and the corresponding pivot (high or low) is updated.
/// </summary>
/// <param name="input">The current bar of market data used for the update.</param>
/// <param name="pivotDirection">Indicates whether the trend has reversed.</param>
private void UpdatePivot(IBaseDataBar input, bool pivotDirection)
{
_lastPivot = input;
_count = 0;
if (_lastPivotWasLow == pivotDirection)
{
//Update previous pivot
(_lastPivotWasLow ? LowPivot : HighPivot).Update(input.EndTime, _lastPivotWasLow ? input.Low : input.High);
}
else
{
//Create new pivot
(_lastPivotWasLow ? HighPivot : LowPivot).Update(input.EndTime, _lastPivotWasLow ? input.High : input.Low);
PivotType = _lastPivotWasLow ? PivotPointType.High : PivotPointType.Low;
}
_lastPivotWasLow = pivotDirection;
}

/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
_lastPivot = null;
PivotType = PivotPointType.Low;
HighPivot.Reset();
LowPivot.Reset();
base.Reset();
}
}
}
77 changes: 77 additions & 0 deletions Tests/Indicators/ZigZagTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.Data.Market;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Indicators
{
[TestFixture]
public class ZigZagTests : CommonIndicatorTests<IBaseDataBar>
{
protected override IndicatorBase<IBaseDataBar> CreateIndicator()
{
return new ZigZag("ZigZag", 0.05m, 10);
}
protected override string TestFileName => "spy_zigzag.csv";

protected override string TestColumnName => "zigzag";

[Test]
public void HighPivotShouldBeZeroWhenAllValuesAreEqual()
{
var zigzag = new ZigZag("ZigZag", 0.05m, 5);
var date = new DateTime(2024, 12, 2, 12, 0, 0);

for (int i = 0; i < 10; i++)
{
var data = new TradeBar
{
Symbol = Symbol.Empty,
Time = date,
Open = 5,
Low = 5,
High = 5,
};
zigzag.Update(data);
}
Assert.AreEqual(0m, zigzag.HighPivot.Current.Value);
}

[Test]
public void LowPivotReflectsInputWhenValuesAreEqual()
{
var zigzag = new ZigZag("ZigZag", 0.05m, 5);
var date = new DateTime(2024, 12, 2, 12, 0, 0);
var value = 5m;

for (int i = 0; i < 10; i++)
{
var data = new TradeBar
{
Symbol = Symbol.Empty,
Time = date,
Open = value,
Low = value,
High = value,
};
zigzag.Update(data);
}
Assert.AreEqual(value, zigzag.LowPivot.Current.Value);
}
}
}
3 changes: 3 additions & 0 deletions Tests/QuantConnect.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@
<Content Include="TestData\spy_sm.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\spy_zigzag.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="TestData\symbol-properties\symbol-properties-database.csv">
Expand Down
Loading