Skip to content

Commit

Permalink
Implement ZigZag (ZZ) indicator (#8454)
Browse files Browse the repository at this point in the history
* Implement ZigZag Indicator

* Create Unit Test for ZigZag Indicator

* Update QCAlgorithm with ZigZag Indicator

* Resolved review comments

* Refactor ZigZag Indicator

- Refactored logic to streamline pivot updates and condition checks.
- Exposed PivotType enum to track pivot direction (High/Low).
- Updated CSV processing to reflect pivot type (High/Low).

* Refactor UpdatePivot logic
  • Loading branch information
JosueNina authored Dec 23, 2024
1 parent be4a34c commit edd1aea
Show file tree
Hide file tree
Showing 5 changed files with 807 additions and 0 deletions.
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
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;

/// <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

0 comments on commit edd1aea

Please sign in to comment.