Ver no GitHub

Keltner Channel Reversal Strategy

Volatility-based channels can highlight overextended moves. This method fades price when it pushes outside the Keltner Channel, anticipating a snap back toward the middle line. It uses an exponential moving average and ATR to size the channel width.

Testing indicates an average annual return of about 106%. It performs best in the stocks market.

As each candle completes, the strategy checks whether the close is beyond the upper or lower band and whether the candle direction agrees. Bullish candles closing below the lower band spark long entries, while bearish candles above the upper band prompt shorts. Positions exit once price crosses the middle band or when the ATR-based stop is reached.

By trading in the opposite direction of short‑term extremes, the system seeks quick mean reversion moves within a broader range.

Details

  • Entry Criteria: Close outside Keltner Channel in the direction of the candle.
  • Long/Short: Both.
  • Exit Criteria: Price crossing middle band or stop-loss.
  • Stops: Yes, ATR based.
  • Default Values:
    • EmaPeriod = 20
    • AtrPeriod = 14
    • AtrMultiplier = 2.0
    • StopLossAtrMultiplier = 2.0
    • CandleType = 5 minute
  • Filters:
    • Category: Mean Reversion
    • Direction: Both
    • Indicators: Keltner Channel
    • Stops: Yes
    • Complexity: Basic
    • Timeframe: Intraday
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
using System;
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;

/// <summary>
/// Keltner Channel Reversal strategy.
/// Enters long when price is below lower Keltner Channel with a bullish candle.
/// Enters short when price is above upper Keltner Channel with a bearish candle.
/// Exits at middle band.
/// </summary>
public class KeltnerChannelReversalStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private int _cooldown;

	/// <summary>
	/// EMA period for Keltner Channel.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for Keltner Channel.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Cooldown bars.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public KeltnerChannelReversalStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Period for EMA in Keltner Channel", "Indicators");

		_atrMultiplier = Param(nameof(AtrMultiplier), 2.0m)
			.SetNotNegative()
			.SetDisplay("ATR Multiplier", "Multiplier for ATR in Keltner Channel", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");

		_cooldownBars = Param(nameof(CooldownBars), 500)
			.SetRange(1, 1000)
			.SetDisplay("Cooldown Bars", "Bars to wait between trades", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_cooldown = default;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_cooldown = 0;

		var keltner = new KeltnerChannels
		{
			Length = EmaPeriod,
			Multiplier = AtrMultiplier,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(keltner, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, keltner);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!keltnerValue.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var kc = (IKeltnerChannelsValue)keltnerValue;
		var upper = kc.Upper;
		var lower = kc.Lower;
		var middle = kc.Middle;

		if (upper == null || lower == null || middle == null)
			return;

		var isBullish = candle.ClosePrice > candle.OpenPrice;
		var isBearish = candle.ClosePrice < candle.OpenPrice;

		if (Position == 0 && candle.ClosePrice < lower.Value && isBullish)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}
		else if (Position == 0 && candle.ClosePrice > upper.Value && isBearish)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position > 0 && candle.ClosePrice > middle.Value)
		{
			SellMarket();
			_cooldown = CooldownBars;
		}
		else if (Position < 0 && candle.ClosePrice < middle.Value)
		{
			BuyMarket();
			_cooldown = CooldownBars;
		}
	}
}