View on GitHub

Donchian Stochastic Strategy

Donchian Channel + Stochastic strategy. Strategy enters the market when the price breaks out of Donchian Channel with Stochastic confirming oversold/overbought conditions.

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

Breakouts beyond the Donchian channel are confirmed with Stochastic momentum. Trades start as soon as price escapes the range and the oscillator agrees.

Useful for traders expecting immediate follow-through. An ATR multiple sets the stop.

Details

  • Entry Criteria:
    • Long: Close > DonchianHigh && StochK < 20
    • Short: Close < DonchianLow && StochK > 80
  • Long/Short: Both
  • Exit Criteria: Breakout failure or opposite signal
  • Stops: Percent-based using StopLossPercent
  • Default Values:
    • DonchianPeriod = 20
    • StochPeriod = 14
    • StochK = 3
    • StochD = 3
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
    • StopLossPercent = 2m
  • Filters:
    • Category: Breakout
    • Direction: Both
    • Indicators: Donchian Channel, Stochastic Oscillator
    • 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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Donchian Channel + Stochastic strategy.
/// Strategy enters the market when the price breaks out of Donchian Channel with Stochastic confirming oversold/overbought conditions.
/// </summary>
public class DonchianStochasticStrategy : Strategy
{
	private readonly StrategyParam<int> _donchianPeriod;
	private readonly StrategyParam<int> _stochPeriod;
	private readonly StrategyParam<int> _stochK;
	private readonly StrategyParam<int> _stochD;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<decimal> _stopLossPercent;

	// Indicators
	private DonchianChannels _donchian;
	private StochasticOscillator _stochastic;
	private int _cooldown;

	/// <summary>
	/// Donchian Channel period.
	/// </summary>
	public int DonchianPeriod
	{
		get => _donchianPeriod.Value;
		set => _donchianPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic period.
	/// </summary>
	public int StochPeriod
	{
		get => _stochPeriod.Value;
		set => _stochPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic %K period.
	/// </summary>
	public int StochK
	{
		get => _stochK.Value;
		set => _stochK.Value = value;
	}

	/// <summary>
	/// Stochastic %D period.
	/// </summary>
	public int StochD
	{
		get => _stochD.Value;
		set => _stochD.Value = value;
	}

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

	/// <summary>
	/// Bars to wait between trades.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public DonchianStochasticStrategy()
	{
		_donchianPeriod = Param(nameof(DonchianPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Donchian Period", "Donchian Channel lookback period", "Indicators")
			
			.SetOptimize(10, 50, 5);

		_stochPeriod = Param(nameof(StochPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Period", "Stochastic oscillator period", "Indicators")
			
			.SetOptimize(5, 30, 5);

		_stochK = Param(nameof(StochK), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "Stochastic %K period", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_stochD = Param(nameof(StochD), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "Stochastic %D period", "Indicators")
			
			.SetOptimize(1, 10, 1);

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

		_cooldownBars = Param(nameof(CooldownBars), 100)
			.SetRange(5, 500)
			.SetDisplay("Cooldown Bars", "Bars between trades", "General");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
			
			.SetOptimize(1m, 5m, 0.5m);
	}

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

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

		_donchian = null;
		_stochastic = null;
		_cooldown = 0;
	}

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

		// Create indicators
		_donchian = new DonchianChannels
		{
			Length = DonchianPeriod
		};

		_stochastic = new StochasticOscillator
		{
			K = { Length = StochK },
			D = { Length = StochD },
		};

		// Enable position protection
		var takeProfitUnit = new Unit(0, UnitTypes.Absolute); // No take profit - we'll exit based on strategy rules
		var stopLossUnit = new Unit(StopLossPercent, UnitTypes.Percent);
		StartProtection(takeProfitUnit, stopLossUnit);

		// Subscribe to candles and bind indicators
		var subscription = SubscribeCandles(CandleType);
		
		subscription
			.BindEx(_donchian, _stochastic, ProcessCandle)
			.Start();

		// Setup chart
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _donchian);
			
			var secondArea = CreateChartArea();
			if (secondArea != null)
			{
				DrawIndicator(secondArea, _stochastic);
			}
			
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(
		ICandleMessage candle,
		IIndicatorValue donchianValue,
		IIndicatorValue stochValue)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
			return;

		// Check if strategy is ready to trade
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var donchianTyped = (DonchianChannelsValue)donchianValue;
		
		if (donchianTyped.UpperBand is not decimal upperBand ||
			donchianTyped.LowerBand is not decimal lowerBand ||
			donchianTyped.Middle is not decimal middleBand)
		{
			return;
		}

		var stochTyped = (StochasticOscillatorValue)stochValue;
		
		if (stochTyped.K is not decimal stochK || stochTyped.D is not decimal stochD)
		{
			return;
		}

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

		// Trading logic:
		// Enter in trend direction with stochastic confirmation.
		if (candle.ClosePrice >= middleBand && stochK > 55 && Position == 0)
		{
			BuyMarket();
			_cooldown = CooldownBars;
			LogInfo($"Long entry: Price={candle.ClosePrice}, Middle Band={middleBand}, Stochastic %K={stochK}");
		}
		else if (candle.ClosePrice <= middleBand && stochK < 45 && Position == 0)
		{
			SellMarket();
			_cooldown = CooldownBars;
			LogInfo($"Short entry: Price={candle.ClosePrice}, Middle Band={middleBand}, Stochastic %K={stochK}");
		}
		// Exit long position when price falls below middle band
		else if (Position > 0 && candle.ClosePrice < middleBand)
		{
			SellMarket();
			_cooldown = CooldownBars;
			LogInfo($"Long exit: Price={candle.ClosePrice}, Middle Band={middleBand}");
		}
		// Exit short position when price rises above middle band
		else if (Position < 0 && candle.ClosePrice > middleBand)
		{
			BuyMarket();
			_cooldown = CooldownBars;
			LogInfo($"Short exit: Price={candle.ClosePrice}, Middle Band={middleBand}");
		}
	}
}