McGinley Dynamic (Improved) Strategy
Implements the "McGinley Dynamic (Improved)" indicator by John R. McGinley, Jr. and trades when the close price crosses the dynamic line. The strategy supports modern, original, and custom coefficient formulas and can optionally display the unconstrained variant for comparison.
Details
- Entry Long: close crosses above McGinley Dynamic.
- Entry Short: close crosses below McGinley Dynamic.
- Indicators: McGinley Dynamic, optional Unconstrained McGinley Dynamic, EMA for reference.
- Default Values: Period = 14, Formula = Modern, Custom k = 0.5, Exponent = 4.
- Direction: Both.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class McGinleyDynamicImprovedStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _signalThresholdPercent;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal? _mdPrev;
private ExponentialMovingAverage _ema;
private decimal _previousDiff;
private bool _hasPreviousDiff;
private int _barsFromSignal;
public int Period { get => _period.Value; set => _period.Value = value; }
public decimal SignalThresholdPercent { get => _signalThresholdPercent.Value; set => _signalThresholdPercent.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public McGinleyDynamicImprovedStrategy()
{
_period = Param(nameof(Period), 20)
.SetGreaterThanZero()
.SetDisplay("Period", "McGinley base period", "General");
_signalThresholdPercent = Param(nameof(SignalThresholdPercent), 0.25m)
.SetGreaterThanZero()
.SetDisplay("Signal Threshold %", "Minimum distance from McGinley in percent", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 10)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown Bars", "Minimum bars between entries", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candles timeframe", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_mdPrev = null;
_ema = null;
_previousDiff = 0m;
_hasPreviousDiff = false;
_barsFromSignal = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(null, null);
_ema = new ExponentialMovingAverage { Length = Period };
_mdPrev = null;
_previousDiff = 0m;
_hasPreviousDiff = false;
_barsFromSignal = SignalCooldownBars;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_ema, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_ema.IsFormed)
return;
var close = candle.ClosePrice;
// Calculate McGinley Dynamic
decimal md;
if (_mdPrev == null)
{
md = close;
}
else
{
var prev = _mdPrev.Value;
if (prev == 0m) prev = close;
var k = 0.6m;
var period = (decimal)Period;
var ratio = close / prev;
var pow = (decimal)Math.Pow((double)ratio, 4.0);
var denom = k * period * pow;
if (denom == 0m) denom = 1m;
md = prev + (close - prev) / denom;
}
_mdPrev = md;
if (close <= 0m)
return;
var diff = (close - md) / close * 100m;
var threshold = SignalThresholdPercent;
var crossedUp = _hasPreviousDiff && _previousDiff <= threshold && diff > threshold;
var crossedDown = _hasPreviousDiff && _previousDiff >= -threshold && diff < -threshold;
_previousDiff = diff;
_hasPreviousDiff = true;
_barsFromSignal++;
if (_barsFromSignal < SignalCooldownBars)
return;
if (crossedUp && Position <= 0)
{
BuyMarket();
_barsFromSignal = 0;
}
else if (crossedDown && Position >= 0)
{
SellMarket();
_barsFromSignal = 0;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class mcginley_dynamic_improved_strategy(Strategy):
def __init__(self):
super(mcginley_dynamic_improved_strategy, self).__init__()
self._period = self.Param("Period", 20) \
.SetGreaterThanZero() \
.SetDisplay("Period", "McGinley base period", "General")
self._signal_threshold_percent = self.Param("SignalThresholdPercent", 0.25) \
.SetGreaterThanZero() \
.SetDisplay("Signal Threshold %", "Minimum distance from McGinley in percent", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 10) \
.SetGreaterThanZero() \
.SetDisplay("Signal Cooldown Bars", "Minimum bars between entries", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Candles timeframe", "General")
self._md_prev = None
self._previous_diff = 0.0
self._has_previous_diff = False
self._bars_from_signal = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(mcginley_dynamic_improved_strategy, self).OnReseted()
self._md_prev = None
self._previous_diff = 0.0
self._has_previous_diff = False
self._bars_from_signal = 0
def OnStarted2(self, time):
super(mcginley_dynamic_improved_strategy, self).OnStarted2(time)
self._md_prev = None
self._previous_diff = 0.0
self._has_previous_diff = False
self._bars_from_signal = self._signal_cooldown_bars.Value
self._ema = ExponentialMovingAverage()
self._ema.Length = self._period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._ema, self.OnProcess).Start()
def OnProcess(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed:
return
close = float(candle.ClosePrice)
if self._md_prev is None:
md = close
else:
prev = self._md_prev
if prev == 0.0:
prev = close
k = 0.6
period = float(self._period.Value)
ratio = close / prev if prev != 0.0 else 1.0
pw = math.pow(ratio, 4.0)
denom = k * period * pw
if denom == 0.0:
denom = 1.0
md = prev + (close - prev) / denom
self._md_prev = md
if close <= 0.0:
return
diff = (close - md) / close * 100.0
threshold = float(self._signal_threshold_percent.Value)
crossed_up = self._has_previous_diff and self._previous_diff <= threshold and diff > threshold
crossed_down = self._has_previous_diff and self._previous_diff >= -threshold and diff < -threshold
self._previous_diff = diff
self._has_previous_diff = True
self._bars_from_signal += 1
cd = self._signal_cooldown_bars.Value
if self._bars_from_signal < cd:
return
if crossed_up and self.Position <= 0:
self.BuyMarket()
self._bars_from_signal = 0
elif crossed_down and self.Position >= 0:
self.SellMarket()
self._bars_from_signal = 0
def CreateClone(self):
return mcginley_dynamic_improved_strategy()