Keltner Macd Strategy
Strategy based on Keltner Channels and MACD. Enters long when price breaks above upper Keltner Channel with MACD > Signal. Enters short when price breaks below lower Keltner Channel with MACD < Signal. Exits when MACD crosses its signal line in the opposite direction.
Testing indicates an average annual return of about 169%. It performs best in the crypto market.
Keltner Channel breakouts serve as the trigger, and MACD momentum filters the direction. The strategy initiates trades once both signals align.
Good for traders chasing volatility expansions with momentum backing. An ATR-based stop contains risk.
Details
- Entry Criteria:
- Long:
Close > UpperBand && MACD > Signal - Short:
Close < LowerBand && MACD < Signal
- Long:
- Long/Short: Both
- Exit Criteria: MACD cross opposite
- Stops: ATR-based using
AtrMultiplier - Default Values:
EmaPeriod= 20Multiplier= 2mAtrPeriod= 14MacdFastPeriod= 12MacdSlowPeriod= 26MacdSignalPeriod= 9AtrMultiplier= 2mCandleType= TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Mean reversion
- Direction: Both
- Indicators: Keltner Channel, MACD
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Mid-term
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
using StockSharp.Algo.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on Keltner Channels and MACD.
/// Enters long when price breaks above upper Keltner Channel with MACD > Signal.
/// Enters short when price breaks below lower Keltner Channel with MACD < Signal.
/// Exits when MACD crosses its signal line in the opposite direction.
/// </summary>
public class KeltnerMacdStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPercent;
private ExponentialMovingAverage _ema;
private AverageTrueRange _atr;
private MovingAverageConvergenceDivergenceSignal _macd;
private decimal _prevMacd;
private decimal _prevSignal;
private int _cooldown;
/// <summary>
/// EMA period for Keltner Channel middle line.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// ATR multiplier for Keltner Channel bands.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// ATR period for Keltner Channel bands.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
/// <summary>
/// MACD fast EMA period.
/// </summary>
public int MacdFastPeriod
{
get => _macdFastPeriod.Value;
set => _macdFastPeriod.Value = value;
}
/// <summary>
/// MACD slow EMA period.
/// </summary>
public int MacdSlowPeriod
{
get => _macdSlowPeriod.Value;
set => _macdSlowPeriod.Value = value;
}
/// <summary>
/// MACD signal line period.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// Bars to wait between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// ATR multiplier for stop loss calculation.
/// </summary>
public decimal AtrMultiplier
{
get => _atrMultiplier.Value;
set => _atrMultiplier.Value = value;
}
/// <summary>
/// Candle type for strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="KeltnerMacdStrategy"/>.
/// </summary>
public KeltnerMacdStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetDisplay("EMA Period", "Period for EMA calculation in Keltner Channel", "Indicators")
.SetOptimize(10, 30, 5);
_multiplier = Param(nameof(Multiplier), 2m)
.SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel bands", "Indicators")
.SetOptimize(1.5m, 3m, 0.5m);
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR calculation in Keltner Channel", "Indicators");
_macdFastPeriod = Param(nameof(MacdFastPeriod), 12)
.SetDisplay("MACD Fast Period", "Fast EMA period for MACD calculation", "Indicators")
;
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
.SetDisplay("MACD Slow Period", "Slow EMA period for MACD calculation", "Indicators")
;
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 9)
.SetDisplay("MACD Signal Period", "Signal line period for MACD calculation", "Indicators")
;
_cooldownBars = Param(nameof(CooldownBars), 20)
.SetRange(1, 200)
.SetDisplay("Cooldown Bars", "Bars between entries", "General");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetDisplay("Stop Loss ATR Multiplier", "ATR multiplier for stop loss calculation", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Timeframe of data for strategy", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 1.0m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk Management")
.SetOptimize(0.5m, 2.0m, 0.5m);
}
/// <inheritdoc />
public override IEnumerable<(Security, DataType)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema?.Reset();
_atr?.Reset();
_macd?.Reset();
_prevMacd = 0;
_prevSignal = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create indicators
_ema = new EMA { Length = EmaPeriod };
_atr = new AverageTrueRange { Length = AtrPeriod };
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFastPeriod },
LongMa = { Length = MacdSlowPeriod },
},
SignalMa = { Length = MacdSignalPeriod }
};
// Initialize variables
// Create subscription
var subscription = SubscribeCandles(CandleType);
// Process candles with indicators
subscription
.BindEx(_ema, _atr, _macd, ProcessCandle)
.Start();
// Setup chart visualization
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
// MACD in separate area
var macdArea = CreateChartArea();
if (macdArea != null)
{
DrawIndicator(macdArea, _macd);
}
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue emaValue, IIndicatorValue atrValue, IIndicatorValue macdValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
var ema = emaValue.ToDecimal();
var atr = atrValue.ToDecimal();
// Calculate Keltner Channels
var upperBand = ema + Multiplier * atr;
var lowerBand = ema - Multiplier * atr;
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
// Process MACD separately to get MACD and Signal values
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
{
return;
}
// Detect MACD crosses
bool macdCrossedAboveSignal = _prevMacd <= _prevSignal && macd > signal;
bool macdCrossedBelowSignal = _prevMacd >= _prevSignal && macd < signal;
// Check if strategy is ready for trading
if (!IsFormedAndOnlineAndAllowTrading())
{
// Store current values for next candle
_prevMacd = macd;
_prevSignal = signal;
return;
}
// Trading logic
if (_cooldown > 0)
_cooldown--;
if (_cooldown == 0 && candle.ClosePrice > upperBand * 1.001m && macdCrossedAboveSignal && Position <= 0)
{
// Price breaks above upper Keltner Channel with bullish MACD - go long
BuyMarket(Volume + Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (_cooldown == 0 && candle.ClosePrice < lowerBand * 0.999m && macdCrossedBelowSignal && Position >= 0)
{
// Price breaks below lower Keltner Channel with bearish MACD - go short
SellMarket(Volume + Math.Abs(Position));
_cooldown = CooldownBars;
}
// Exit logic based on MACD crosses
if (Position > 0 && macdCrossedBelowSignal)
{
// Exit long position when MACD crosses below Signal
ClosePosition();
_cooldown = CooldownBars;
}
else if (Position < 0 && macdCrossedAboveSignal)
{
// Exit short position when MACD crosses above Signal
ClosePosition();
_cooldown = CooldownBars;
}
// Store current values for next candle
_prevMacd = macd;
_prevSignal = signal;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange, MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class keltner_macd_strategy(Strategy):
"""
Strategy based on Keltner Channels and MACD.
Enters long when price breaks above upper Keltner Channel with MACD cross above Signal.
Enters short when price breaks below lower Keltner Channel with MACD cross below Signal.
Exits when MACD crosses its signal line in the opposite direction.
"""
def __init__(self):
super(keltner_macd_strategy, self).__init__()
self._emaPeriod = self.Param("EmaPeriod", 20) \
.SetDisplay("EMA Period", "Period for EMA calculation in Keltner Channel", "Indicators")
self._multiplier = self.Param("Multiplier", 2.0) \
.SetDisplay("ATR Multiplier", "ATR multiplier for Keltner Channel bands", "Indicators")
self._atrPeriod = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "Period for ATR calculation in Keltner Channel", "Indicators")
self._macdFastPeriod = self.Param("MacdFastPeriod", 12) \
.SetDisplay("MACD Fast Period", "Fast EMA period for MACD calculation", "Indicators")
self._macdSlowPeriod = self.Param("MacdSlowPeriod", 26) \
.SetDisplay("MACD Slow Period", "Slow EMA period for MACD calculation", "Indicators")
self._macdSignalPeriod = self.Param("MacdSignalPeriod", 9) \
.SetDisplay("MACD Signal Period", "Signal line period for MACD calculation", "Indicators")
self._cooldownBars = self.Param("CooldownBars", 20) \
.SetRange(1, 200) \
.SetDisplay("Cooldown Bars", "Bars between entries", "General")
self._atrMultiplier = self.Param("AtrMultiplier", 2.0) \
.SetDisplay("Stop Loss ATR Multiplier", "ATR multiplier for stop loss calculation", "Risk Management")
self._candleType = self.Param("CandleType", tf(15)) \
.SetDisplay("Candle Type", "Timeframe of data for strategy", "General")
self._stopLossPercent = self.Param("StopLossPercent", 1.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk Management")
self._prevMacd = 0.0
self._prevSignal = 0.0
self._cooldown = 0
@property
def CandleType(self):
return self._candleType.Value
def OnReseted(self):
super(keltner_macd_strategy, self).OnReseted()
self._prevMacd = 0.0
self._prevSignal = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(keltner_macd_strategy, self).OnStarted2(time)
self._prevMacd = 0.0
self._prevSignal = 0.0
self._cooldown = 0
ema = ExponentialMovingAverage()
ema.Length = self._emaPeriod.Value
atr = AverageTrueRange()
atr.Length = self._atrPeriod.Value
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = self._macdFastPeriod.Value
macd.Macd.LongMa.Length = self._macdSlowPeriod.Value
macd.SignalMa.Length = self._macdSignalPeriod.Value
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(ema, atr, macd, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
macdArea = self.CreateChartArea()
if macdArea is not None:
self.DrawIndicator(macdArea, macd)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, ema_value, atr_value, macd_value):
if candle.State != CandleStates.Finished:
return
ema = float(ema_value)
atr = float(atr_value)
multiplier = float(self._multiplier.Value)
upperBand = ema + multiplier * atr
lowerBand = ema - multiplier * atr
if macd_value.Macd is None or macd_value.Signal is None:
return
macd = float(macd_value.Macd)
signal = float(macd_value.Signal)
macdCrossedAboveSignal = self._prevMacd <= self._prevSignal and macd > signal
macdCrossedBelowSignal = self._prevMacd >= self._prevSignal and macd < signal
if not self.IsFormedAndOnlineAndAllowTrading():
self._prevMacd = macd
self._prevSignal = signal
return
if self._cooldown > 0:
self._cooldown -= 1
cooldown_val = int(self._cooldownBars.Value)
if self._cooldown == 0 and float(candle.ClosePrice) > upperBand * 1.001 and macdCrossedAboveSignal and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._cooldown = cooldown_val
elif self._cooldown == 0 and float(candle.ClosePrice) < lowerBand * 0.999 and macdCrossedBelowSignal and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._cooldown = cooldown_val
if self.Position > 0 and macdCrossedBelowSignal:
self.ClosePosition()
self._cooldown = cooldown_val
elif self.Position < 0 and macdCrossedAboveSignal:
self.ClosePosition()
self._cooldown = cooldown_val
self._prevMacd = macd
self._prevSignal = signal
def CreateClone(self):
return keltner_macd_strategy()