GitHub で見る

Donchain Counter Strategy

Overview

The Donchain Counter strategy is a StockSharp port of the MQL5 expert advisor "Donchain counter" by Michal Rutka. The system watches how the Donchian Channel expands to detect breakouts and then defends the position by trailing the stop along the opposite band once price has moved a fixed distance away. Only one position can be opened every 24 hours, mirroring the original constraint.

Trading Logic

Long Entries

  • Evaluate signals on completed candles of the configured timeframe (default H1).
  • Observe the upper Donchian band on the previous two closed bars. When the band on bar t-1 is higher than on bar t-2 (a fresh breakout of the channel high), a long market order is placed.
  • The initial protective stop is anchored to the current lower Donchian band.

Short Entries

  • Monitor the lower Donchian band on the previous two closed bars. When the band on bar t-1 is lower than on bar t-2 (a breakout of the channel low), a short market order is submitted.
  • The first stop level is set to the current upper Donchian band.

Trade Cooldown

  • After any new entry the algorithm records the execution time and blocks subsequent entries for the duration of TradeCooldown (default 24 hours). This reproduces the “only one trade per day” rule in the MQL version.

Trailing and Exit Rules

  • A trailing mechanism engages only after price advances at least BufferSteps price steps beyond the opposite Donchian band. This replicates the requirement from the original EA where the market must move 50 points before the stop is tightened.
  • Long positions: once the trailing trigger fires, the stop is updated to the current lower band. If the candle’s low touches that level the strategy exits with a market order.
  • Short positions: after the trigger fires, the stop follows the current upper band. If the candle’s high reaches that price the position is closed.
  • When the trailing stop forces an exit the strategy does not open a new position until the next signal and the cooldown permit it.

Risk Handling

  • The strategy always trades a single position whose size is defined by the Volume parameter.
  • There is no profit target; all exits are driven by the Donchian trailing logic.

Parameters

Name Description Default
Volume Order size for entries. 1
ChannelPeriod Lookback period for the Donchian Channel calculation. 20
BufferSteps Number of price steps price must exceed beyond the opposite band before trailing activates (MQL used 50 points). 50
TradeCooldown Minimum time between new entries. 1 day
CandleType Candle series used for the indicator (default 1-hour candles). 1h candles

Indicators

  • Donchian Channels – upper and lower bands define breakout signals and dynamic stops.

Notes

  • Use instruments with a sensible PriceStep so the buffer translates to realistic price distance. The strategy defaults to a 0.0001 step if none is provided by the security.
  • Only one direction is open at a time. Before flipping direction the existing position must fully close, just like the original expert advisor.
  • Chart objects are automatically prepared if a chart area is available: candles, the Donchian channel and the strategy’s own trades.
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>
/// Donchain Counter strategy converted from MQL5 Donchain counter expert advisor.
/// Tracks Donchian channel expansions for entries and trails stops along the channel bands.
/// </summary>
public class DonchainCounterStrategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<int> _bufferSteps;
	private readonly StrategyParam<TimeSpan> _tradeCooldown;
	private readonly StrategyParam<DataType> _candleType;

	private DonchianChannels _donchian = null!;
	private decimal _priceStep;
	private decimal _tolerance;
	private decimal _currentUpper;
	private decimal _currentLower;
	private decimal _previousUpper;
	private decimal _previousLower;
	private decimal _earlierUpper;
	private decimal _earlierLower;
	private decimal? _longStopLevel;
	private decimal? _shortStopLevel;
	private DateTimeOffset? _lastTradeTime;

	/// <summary>
	/// Initializes a new instance of <see cref="DonchainCounterStrategy"/>.
	/// </summary>
	public DonchainCounterStrategy()
	{

		_channelPeriod = Param(nameof(ChannelPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Donchian Period", "Lookback period for Donchian Channel", "Indicators")
			
			.SetOptimize(10, 40, 5);

		_bufferSteps = Param(nameof(BufferSteps), 50)
			.SetGreaterThanZero()
			.SetDisplay("Buffer Steps", "Minimum price steps before trailing stop activates", "Risk");

		_tradeCooldown = Param(nameof(TradeCooldown), TimeSpan.FromMinutes(30))
			.SetDisplay("Trade Cooldown", "Minimum waiting time between new entries", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for Donchian evaluation", "General");
	}


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

	/// <summary>
	/// Number of price steps that price must move beyond the opposite band before trailing starts.
	/// </summary>
	public int BufferSteps
	{
		get => _bufferSteps.Value;
		set => _bufferSteps.Value = value;
	}

	/// <summary>
	/// Minimum cooldown between new trades.
	/// </summary>
	public TimeSpan TradeCooldown
	{
		get => _tradeCooldown.Value;
		set => _tradeCooldown.Value = value;
	}

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

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

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

		_donchian = null!;
		_priceStep = 0m;
		_tolerance = 0m;
		_currentUpper = 0m;
		_currentLower = 0m;
		_previousUpper = 0m;
		_previousLower = 0m;
		_earlierUpper = 0m;
		_earlierLower = 0m;
		_longStopLevel = null;
		_shortStopLevel = null;
		_lastTradeTime = null;
	}

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

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		{
			_priceStep = 0.0001m;
		}

		_tolerance = _priceStep / 2m;

		_donchian = new DonchianChannels
		{
			Length = ChannelPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessDonchianRaw)
			.Start();

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

	private void ProcessDonchianRaw(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var donchianValue = _donchian.Process(candle);
		if (donchianValue.IsEmpty || !_donchian.IsFormed)
			return;

		var value = (DonchianChannelsValue)donchianValue;

		if (value.UpperBand is not decimal upperBand || value.LowerBand is not decimal lowerBand)
			return;

		_currentUpper = upperBand;
		_currentLower = lowerBand;

		var hadPosition = Position != 0m;
		if (hadPosition)
		{
			ManageExistingPosition(candle);
			UpdateHistory();
			return;
		}

		TryOpenPosition(candle);
		UpdateHistory();
	}

	private void ManageExistingPosition(ICandleMessage candle)
	{
		// Buffer converts the point-based activation threshold into price units.
		var buffer = BufferSteps * _priceStep;

		if (Position > 0m)
		{
			// Activate or advance the trailing stop once price moves far enough from the lower band.
			if (candle.HighPrice > _currentLower + buffer)
			{
				if (!_longStopLevel.HasValue || _longStopLevel.Value < _currentLower - _tolerance)
				{
					_longStopLevel = _currentLower;
					// Log: $"Updated long stop to {_longStopLevel.Value}");
				}
			}

			// Exit the long position when price falls back through the protected band.
			if (_longStopLevel.HasValue && candle.LowPrice <= _longStopLevel.Value + _tolerance)
			{
				SellMarket(Position);
				// Log: $"Long exit triggered at {_longStopLevel.Value}");
				_longStopLevel = null;
			}
		}
		else if (Position < 0m)
		{
			// Activate or advance the trailing stop once price moves far enough from the upper band.
			if (candle.LowPrice < _currentUpper - buffer)
			{
				if (!_shortStopLevel.HasValue || _shortStopLevel.Value > _currentUpper + _tolerance)
				{
					_shortStopLevel = _currentUpper;
					// Log: $"Updated short stop to {_shortStopLevel.Value}");
				}
			}

			// Exit the short position when price rallies back to the protected band.
			if (_shortStopLevel.HasValue && candle.HighPrice >= _shortStopLevel.Value - _tolerance)
			{
				BuyMarket(Math.Abs(Position));
				// Log: $"Short exit triggered at {_shortStopLevel.Value}");
				_shortStopLevel = null;
			}
		}
		else
		{
			_longStopLevel = null;
			_shortStopLevel = null;
		}
	}

	private void TryOpenPosition(ICandleMessage candle)
	{
		// Require at least two completed Donchian samples for breakout comparisons.
		if (_previousUpper == 0m || _earlierUpper == 0m || _previousLower == 0m || _earlierLower == 0m)
		{
			return;
		}

		var now = candle.CloseTime;
		if (_lastTradeTime.HasValue && now - _lastTradeTime.Value < TradeCooldown)
		{
			return;
		}

		// Long entry when the upper Donchian band expanded on the previous bar.
		if (_previousUpper > _earlierUpper && !AreClose(_previousUpper, _earlierUpper))
		{
			BuyMarket(Volume);
			_longStopLevel = _currentLower;
			_lastTradeTime = now;
			// Log: $"Long entry at {candle.ClosePrice} with stop {_currentLower}");
			return;
		}

		// Short entry when the lower Donchian band contracted on the previous bar.
		if (_previousLower < _earlierLower && !AreClose(_previousLower, _earlierLower))
		{
			SellMarket(Volume);
			_shortStopLevel = _currentUpper;
			_lastTradeTime = now;
			// Log: $"Short entry at {candle.ClosePrice} with stop {_currentUpper}");
		}
	}

	private void UpdateHistory()
	{
		_earlierUpper = _previousUpper;
		_earlierLower = _previousLower;
		_previousUpper = _currentUpper;
		_previousLower = _currentLower;
	}

	private bool AreClose(decimal first, decimal second)
	{
		return Math.Abs(first - second) <= _tolerance;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			_longStopLevel = null;
			_shortStopLevel = null;
		}
		else if (Position > 0m)
		{
			_shortStopLevel = null;
		}
		else
		{
			_longStopLevel = null;
		}
	}
}