View on GitHub

Trade in Channel Strategy

Contrarian channel strategy that fades Donchian channel extremes when the band width stays unchanged. The system compares the latest high/low against the previous channel boundaries and a pivot computed from the prior close to decide whether to fade the move. Protective stops rely on ATR distance and an optional trailing stop maintains profits once price runs in favor of the position.

Details

  • Entry Criteria:
    • Short: channel upper band unchanged and either the last candle high touched the upper band or the previous close sits between the pivot and the upper band.
    • Long: channel lower band unchanged and either the last candle low touched the lower band or the previous close sits between the pivot and the lower band.
  • Long/Short: Both.
  • Exit Criteria:
    • Close long if the upper band is flat and price tags it, or if the ATR stop or trailing stop is hit.
    • Close short if the lower band is flat and price tags it, or if the ATR stop or trailing stop is hit.
  • Stops:
    • Initial stop for longs at support - ATR and for shorts at resistance + ATR.
    • Trailing stop moves behind the best price once profit exceeds the TrailingStopPips distance (converted into price steps).
  • Default Values:
    • ChannelPeriod = 20 (Donchian lookback)
    • AtrPeriod = 4 (ATR smoothing)
    • Volume = 1 contract/lot
    • TrailingStopPips = 30 price steps
    • CandleType = 1 hour timeframe
  • Filters:
    • Category: Channel / Mean Reversion
    • Direction: Long & Short
    • Indicators: Donchian Channel, ATR
    • Stops: ATR hard stop + trailing stop
    • Complexity: Intermediate
    • Timeframe: Intraday
    • Seasonality: No
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium

Notes

  • The pivot equals (upper band + lower band + previous close) / 3, matching the original MQL implementation.
  • The strategy keeps only one net position and flips direction only after the previous trade is fully closed.
  • Trailing distance is specified in price steps ("pips"); it is multiplied by the instrument PriceStep to obtain the actual price offset.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Channel breakout reversal strategy based on Donchian channel and ATR stops.
/// </summary>
public class TradeInChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<DataType> _candleType;

	private DonchianChannels _donchian = null!;
	private AverageTrueRange _atr = null!;

	private decimal? _previousUpper;
	private decimal? _previousLower;
	private decimal? _previousClose;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStop;
	private decimal? _shortStop;
	private decimal? _longBestPrice;
	private decimal? _shortBestPrice;
	private decimal? _longTrailingLevel;
	private decimal? _shortTrailingLevel;

	private decimal _priceStep = 1m;

	/// <summary>
	/// Donchian channel lookback.
	/// </summary>
	public int ChannelPeriod
	{
		get => _channelPeriod.Value;
		set => _channelPeriod.Value = value;
	}

	/// <summary>
	/// ATR calculation period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="TradeInChannelStrategy"/>.
	/// </summary>
	public TradeInChannelStrategy()
	{
		_channelPeriod = Param(nameof(ChannelPeriod), 20)
			.SetDisplay("Channel Period", "Donchian channel lookback", "Channel")
			
			.SetGreaterThanZero();

		_atrPeriod = Param(nameof(AtrPeriod), 4)
			.SetDisplay("ATR Period", "Average True Range length", "Volatility")
			
			.SetGreaterThanZero();

		_trailingStopPips = Param(nameof(TrailingStopPips), 30m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in price steps", "Risk")
			
			.SetNotNegative();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for signals", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_donchian = null!;
		_atr = null!;

		_previousUpper = null;
		_previousLower = null;
		_previousClose = null;

		ResetLongState();
		ResetShortState();
		_priceStep = 1m;
	}

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

		var ps = Security?.PriceStep;
		_priceStep = ps is > 0m ? ps.Value : 1m;

		_donchian = new DonchianChannels
		{
			Length = ChannelPeriod
		};

		_atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

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

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

		// no protection needed
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue donchianValue, IIndicatorValue atrValue)
	{
		// Ignore unfinished candles to work only with confirmed data.
		if (candle.State != CandleStates.Finished)
		return;

		if (!_donchian.IsFormed || !_atr.IsFormed)
		return;

		var donchian = (DonchianChannelsValue)donchianValue;

		if (donchian.UpperBand is not decimal upper || donchian.LowerBand is not decimal lower)
		return;

		if (!atrValue.IsFinal)
		return;

		var atr = atrValue.ToDecimal();

		var previousUpper = _previousUpper;
		var previousLower = _previousLower;
		var previousClose = _previousClose;

		// Need at least one full bar history to evaluate pivots and channel stability.
		if (previousUpper is null || previousLower is null || previousClose is null)
		{
			_previousUpper = upper;
			_previousLower = lower;
			_previousClose = candle.ClosePrice;
			return;
		}

		var pivot = (upper + lower + previousClose.Value) / 3m;

		var closedLong = ManageLongPosition(candle, upper, previousUpper.Value);
		var closedShort = ManageShortPosition(candle, lower, previousLower.Value);

		if (Position == 0 && !closedLong && !closedShort)
		{
			EvaluateEntries(candle, upper, lower, previousUpper.Value, previousLower.Value, previousClose.Value, pivot, atr);
		}

		_previousUpper = upper;
		_previousLower = lower;
		_previousClose = candle.ClosePrice;
	}

	private bool ManageLongPosition(ICandleMessage candle, decimal upper, decimal previousUpper)
	{
		if (Position <= 0)
		return false;

		// Hard stop based on ATR.
		if (_longStop is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket();
			ResetLongState();
			return true;
		}

		// Exit when price breaks above a flat resistance level.
		if (upper == previousUpper && candle.HighPrice >= upper)
		{
			SellMarket();
			ResetLongState();
			return true;
		}

		return ApplyLongTrailing(candle);
	}

	private bool ManageShortPosition(ICandleMessage candle, decimal lower, decimal previousLower)
	{
		if (Position >= 0)
		return false;

		// Hard stop based on ATR.
		if (_shortStop is decimal stop && candle.HighPrice >= stop)
		{
			BuyMarket();
			ResetShortState();
			return true;
		}

		// Exit when price breaks below a flat support level.
		if (lower == previousLower && candle.LowPrice <= lower)
		{
			BuyMarket();
			ResetShortState();
			return true;
		}

		return ApplyShortTrailing(candle);
	}

	private bool ApplyLongTrailing(ICandleMessage candle)
	{
		if (Position <= 0)
		return false;

		var offset = GetTrailingOffset();
		if (offset <= 0m || _longEntryPrice is not decimal entryPrice)
		{
			_longBestPrice = candle.HighPrice;
			return false;
		}

		_longBestPrice = _longBestPrice.HasValue
		? Math.Max(_longBestPrice.Value, candle.HighPrice)
		: candle.HighPrice;

		if (_longBestPrice is decimal best && best - entryPrice > offset)
		{
			var newLevel = best - offset;

			if (_longTrailingLevel is null || newLevel > _longTrailingLevel.Value)
			_longTrailingLevel = newLevel;

			if (_longTrailingLevel is decimal level && candle.LowPrice <= level)
			{
				SellMarket();
				ResetLongState();
				return true;
			}
		}

		return false;
	}

	private bool ApplyShortTrailing(ICandleMessage candle)
	{
		if (Position >= 0)
		return false;

		var offset = GetTrailingOffset();
		if (offset <= 0m || _shortEntryPrice is not decimal entryPrice)
		{
			_shortBestPrice = candle.LowPrice;
			return false;
		}

		_shortBestPrice = _shortBestPrice.HasValue
		? Math.Min(_shortBestPrice.Value, candle.LowPrice)
		: candle.LowPrice;

		if (_shortBestPrice is decimal best && entryPrice - best > offset)
		{
			var newLevel = best + offset;

			if (_shortTrailingLevel is null || newLevel < _shortTrailingLevel.Value)
			_shortTrailingLevel = newLevel;

			if (_shortTrailingLevel is decimal level && candle.HighPrice >= level)
			{
				BuyMarket();
				ResetShortState();
				return true;
			}
		}

		return false;
	}

	private void EvaluateEntries(
		ICandleMessage candle,
	decimal upper,
	decimal lower,
	decimal previousUpper,
	decimal previousLower,
	decimal previousClose,
	decimal pivot,
	decimal atr)
	{
		var resistanceFlat = upper == previousUpper;
		var supportFlat = lower == previousLower;

		var shouldOpenShort = resistanceFlat &&
		(candle.HighPrice >= upper || (previousClose < upper && previousClose > pivot));

		var shouldOpenLong = supportFlat &&
		(candle.LowPrice <= lower || (previousClose > lower && previousClose < pivot));

		if (shouldOpenLong)
		{
			OpenLong(candle, lower, atr);
		}
		else if (shouldOpenShort)
		{
			OpenShort(candle, upper, atr);
		}
	}

	private void OpenLong(ICandleMessage candle, decimal support, decimal atr)
	{
		if (Volume <= 0m)
		return;

		BuyMarket();

		_longEntryPrice = candle.ClosePrice;
		_longBestPrice = candle.ClosePrice;
		_longTrailingLevel = null;
		_longStop = support - atr;

		ResetShortState();
	}

	private void OpenShort(ICandleMessage candle, decimal resistance, decimal atr)
	{
		if (Volume <= 0m)
		return;

		SellMarket();

		_shortEntryPrice = candle.ClosePrice;
		_shortBestPrice = candle.ClosePrice;
		_shortTrailingLevel = null;
		_shortStop = resistance + atr;

		ResetLongState();
	}

	private decimal GetTrailingOffset()
	{
		return TrailingStopPips * _priceStep;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStop = null;
		_longBestPrice = null;
		_longTrailingLevel = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStop = null;
		_shortBestPrice = null;
		_shortTrailingLevel = null;
	}
}