Skip to content

Commit

Permalink
Implement SqueezeMomentum (SM) indicator (#8462)
Browse files Browse the repository at this point in the history
* Implement Squeeze Momentum Indicator

* Create unit test for SqueezeMomentum Indicator

* Update QCAlgorithm with SqueezeMomentum indicator

* Resolved review comments

* Switched to using Keltner indicator instead of manual calculation

* Refactor Squeeze Momentum indicator

- Make the Bollinger Bands and Keltner Channels indicators public.
- In IndicatorBase -> Update, if T is IndicatorDataPoint, then create a new
  IndicatorDataPoint.

* Fix issue with regression tests

* Fix unit test
  • Loading branch information
JosueNina authored Dec 20, 2024
1 parent 3b559a5 commit be4a34c
Show file tree
Hide file tree
Showing 8 changed files with 1,181 additions and 9 deletions.
21 changes: 21 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1951,6 +1951,27 @@ public SortinoRatio SORTINO(Symbol symbol, int sortinoPeriod, double minimumAcce
return sortinoRatio;
}

/// <summary>
/// Creates a Squeeze Momentum indicator to identify market squeezes and potential breakouts.
/// Compares Bollinger Bands and Keltner Channels to signal low or high volatility periods.
/// </summary>
/// <param name="symbol">The symbol for which the indicator is calculated.</param>
/// <param name="bollingerPeriod">The period for Bollinger Bands.</param>
/// <param name="bollingerMultiplier">The multiplier for the Bollinger Bands' standard deviation.</param>
/// <param name="keltnerPeriod">The period for Keltner Channels.</param>
/// <param name="keltnerMultiplier">The multiplier for the Average True Range in Keltner Channels.</param>
/// <param name="resolution">The resolution of the data.</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 Squeeze Momentum indicator.</returns>
[DocumentationAttribute(Indicators)]
public SqueezeMomentum SM(Symbol symbol, int bollingerPeriod = 20, decimal bollingerMultiplier = 2m, int keltnerPeriod = 20,
decimal keltnerMultiplier = 1.5m, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
{
var name = CreateIndicatorName(symbol, $"SM({bollingerPeriod}, {bollingerMultiplier}, {keltnerPeriod}, {keltnerMultiplier})", resolution);
var squeezeMomentum = new SqueezeMomentum(name, bollingerPeriod, bollingerMultiplier, keltnerPeriod, keltnerMultiplier);
InitializeIndicator(squeezeMomentum, resolution, selector, symbol);
return squeezeMomentum;
}

/// <summary>
/// Creates an SimpleMovingAverage indicator for the symbol. The indicator will be automatically
Expand Down
11 changes: 9 additions & 2 deletions Indicators/IndicatorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public abstract class IndicatorBase<T> : IndicatorBase
/// <param name="name">The name of this indicator</param>
protected IndicatorBase(string name)
: base(name)
{}
{ }

/// <summary>
/// Updates the state of this indicator with the given value and returns true
Expand Down Expand Up @@ -271,7 +271,14 @@ public override bool Update(IBaseData input)

if (!(input is T))
{
throw new ArgumentException($"IndicatorBase.Update() 'input' expected to be of type {typeof(T)} but is of type {input.GetType()}");
if (typeof(T) == typeof(IndicatorDataPoint))
{
input = new IndicatorDataPoint(input.EndTime, input.Value);
}
else
{
throw new ArgumentException($"IndicatorBase.Update() 'input' expected to be of type {typeof(T)} but is of type {input.GetType()}");
}
}
_previousInput[input.Symbol.ID] = (T)input;

Expand Down
4 changes: 2 additions & 2 deletions Indicators/KeltnerChannels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public KeltnerChannels(string name, int period, decimal k, MovingAverageType mov
WarmUpPeriod = period;

//Initialise ATR and SMA
AverageTrueRange = new AverageTrueRange(name + "_AverageTrueRange", period, movingAverageType);
AverageTrueRange = new AverageTrueRange(name + "_AverageTrueRange", period, MovingAverageType.Simple);
MiddleBand = movingAverageType.AsIndicator(name + "_MiddleBand", period);

//Compute Lower Band
Expand Down Expand Up @@ -117,7 +117,7 @@ protected override decimal ComputeNextValue(IBaseDataBar input)
{
AverageTrueRange.Update(input);

var typicalPrice = (input.High + input.Low + input.Close)/3m;
var typicalPrice = (input.High + input.Low + input.Close) / 3m;
MiddleBand.Update(input.EndTime, typicalPrice);

// poke the upper/lower bands, they actually don't use the input, they compute
Expand Down
104 changes: 104 additions & 0 deletions Indicators/SqueezeMomentum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.Market;

namespace QuantConnect.Indicators
{
/// <summary>
/// The SqueezeMomentum indicator calculates whether the market is in a "squeeze" condition,
/// determined by comparing Bollinger Bands to Keltner Channels. When the Bollinger Bands are
/// inside the Keltner Channels, the indicator returns 1 (squeeze on). Otherwise, it returns -1 (squeeze off).
/// </summary>
public class SqueezeMomentum : BarIndicator, IIndicatorWarmUpPeriodProvider
{
/// <summary>
/// The Bollinger Bands indicator used to calculate the upper, lower, and middle bands.
/// </summary>
public BollingerBands BollingerBands { get; }

/// <summary>
/// The Keltner Channels indicator used to calculate the upper, lower, and middle channels.
/// </summary>
public KeltnerChannels KeltnerChannels { get; }

/// <summary>
/// Initializes a new instance of the <see cref="SqueezeMomentum"/> class.
/// </summary>
/// <param name="name">The name of the indicator.</param>
/// <param name="bollingerPeriod">The period used for the Bollinger Bands calculation.</param>
/// <param name="bollingerMultiplier">The multiplier for the Bollinger Bands width.</param>
/// <param name="keltnerPeriod">The period used for the Average True Range (ATR) calculation in Keltner Channels.</param>
/// <param name="keltnerMultiplier">The multiplier applied to the ATR for calculating Keltner Channels.</param>
public SqueezeMomentum(string name, int bollingerPeriod, decimal bollingerMultiplier, int keltnerPeriod, decimal keltnerMultiplier) : base(name)
{
BollingerBands = new BollingerBands(bollingerPeriod, bollingerMultiplier);
KeltnerChannels = new KeltnerChannels(keltnerPeriod, keltnerMultiplier, MovingAverageType.Exponential);
WarmUpPeriod = Math.Max(bollingerPeriod, keltnerPeriod);
}

/// <summary>
/// Gets the warm-up period required for the indicator to be ready.
/// This is determined by the warm-up period of the Bollinger Bands indicator.
/// </summary>
public int WarmUpPeriod { get; }

/// <summary>
/// Indicates whether the indicator is ready and has enough data for computation.
/// The indicator is ready when both the Bollinger Bands and the Average True Range are ready.
/// </summary>
public override bool IsReady => BollingerBands.IsReady && KeltnerChannels.IsReady;

/// <summary>
/// Computes the next value of the indicator based on the input data bar.
/// </summary>
/// <param name="input">The input data bar.</param>
/// <returns>
/// Returns 1 if the Bollinger Bands are inside the Keltner Channels (squeeze on),
/// or -1 if the Bollinger Bands are outside the Keltner Channels (squeeze off).
/// </returns>
protected override decimal ComputeNextValue(IBaseDataBar input)
{
BollingerBands.Update(input);
KeltnerChannels.Update(input);
if (!IsReady)
{
return decimal.Zero;
}

// Calculate Bollinger Bands upper, lower
var bbUpper = BollingerBands.UpperBand.Current.Value;
var bbLower = BollingerBands.LowerBand.Current.Value;

// Calculate Keltner Channels upper and lower bounds
var kcUpper = KeltnerChannels.UpperBand.Current.Value;
var kcLower = KeltnerChannels.LowerBand.Current.Value;

// Determine if the squeeze condition is on or off
return (kcUpper > bbUpper && kcLower < bbLower) ? 1m : -1m;
}

/// <summary>
/// Resets the state of the indicator, including all sub-indicators.
/// </summary>
public override void Reset()
{
BollingerBands.Reset();
KeltnerChannels.Reset();
base.Reset();
}
}
}
10 changes: 5 additions & 5 deletions Tests/Indicators/IndicatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ public void UpdatesProperly()
}

[Test]
public void ThrowsOnDifferentDataType()
public void ShouldNotThrowOnDifferentDataType()
{
var target = new TestIndicator();
Assert.Throws<ArgumentException>(() =>
Assert.DoesNotThrow(() =>
{
target.Update(new Tick());
}, "expected to be of type");
});
}

[Test]
Expand Down Expand Up @@ -174,7 +174,7 @@ public void IndicatorMustBeEqualToItself()
{
try
{
instantiatedIndicator = Activator.CreateInstance(indicator, new object[] {10});
instantiatedIndicator = Activator.CreateInstance(indicator, new object[] { 10 });
counter++;
}
catch (Exception)
Expand Down Expand Up @@ -364,7 +364,7 @@ private static MethodInfo GetOperatorMethodInfo<T>(string @operator, int argInde
{
var methodName = "op_" + @operator;
var method =
typeof (IndicatorBase).GetMethods(BindingFlags.Static | BindingFlags.Public)
typeof(IndicatorBase).GetMethods(BindingFlags.Static | BindingFlags.Public)
.SingleOrDefault(x => x.Name == methodName && x.GetParameters()[argIndex].ParameterType == typeof(T));

if (method == null)
Expand Down
34 changes: 34 additions & 0 deletions Tests/Indicators/SqueezeMomentumTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 NUnit.Framework;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Indicators
{
[TestFixture]
public class SqueezeMomentumTests : CommonIndicatorTests<IBaseDataBar>
{
protected override IndicatorBase<IBaseDataBar> CreateIndicator()
{
VolumeRenkoBarSize = 0.5m;
return new SqueezeMomentum("SM", 20, 2, 20, 1.5m);
}
protected override string TestFileName => "spy_sm.csv";

protected override string TestColumnName => "squeeze on";
}
}
3 changes: 3 additions & 0 deletions Tests/QuantConnect.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,9 @@
<Content Include="TestData\spy_hurst_exponent.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\spy_sm.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="TestData\symbol-properties\symbol-properties-database.csv">
Expand Down
Loading

0 comments on commit be4a34c

Please sign in to comment.