Ver en GitHub

Estrategia de Operación en Canal

Estrategia contraria de canal que desvanece los extremos del canal Donchian cuando el ancho de banda permanece sin cambios. El sistema compara el último máximo/mínimo contra los límites previos del canal y un pivote calculado desde el cierre anterior para decidir si desvanece el movimiento. Los stops protectores dependen de la distancia ATR y un trailing stop opcional mantiene las ganancias una vez que el precio avanza a favor de la posición.

Detalles

  • Criterios de entrada:
    • Corto: banda superior del canal sin cambios y el último máximo de la vela tocó la banda superior o el cierre previo está entre el pivote y la banda superior.
    • Largo: banda inferior del canal sin cambios y el último mínimo de la vela tocó la banda inferior o el cierre previo está entre el pivote y la banda inferior.
  • Largo/Corto: Ambos.
  • Criterios de salida:
    • Cerrar largo si la banda superior está plana y el precio la toca, o si se activa el stop ATR o el trailing stop.
    • Cerrar corto si la banda inferior está plana y el precio la toca, o si se activa el stop ATR o el trailing stop.
  • Stops:
    • Stop inicial para largos en support - ATR y para cortos en resistance + ATR.
    • El trailing stop se mueve detrás del mejor precio una vez que la ganancia supera la distancia TrailingStopPips (convertida en pasos de precio).
  • Valores predeterminados:
    • ChannelPeriod = 20 (lookback de Donchian)
    • AtrPeriod = 4 (suavizado ATR)
    • Volume = 1 contrato/lote
    • TrailingStopPips = 30 pasos de precio
    • CandleType = marco temporal de 1 hora
  • Filtros:
    • Categoría: Canal / Reversión a la media
    • Dirección: Largo y Corto
    • Indicadores: Donchian Channel, ATR
    • Stops: Stop fijo ATR + trailing stop
    • Complejidad: Intermedio
    • Marco temporal: Intradía
    • Estacionalidad: No
    • Redes neuronales: No
    • Divergencia: No
    • Nivel de riesgo: Medio

Notas

  • El pivote es igual a (banda superior + banda inferior + cierre previo) / 3, coincidiendo con la implementación MQL original.
  • La estrategia mantiene solo una posición neta y cambia de dirección solo después de que el trade anterior esté completamente cerrado.
  • La distancia del trailing se especifica en pasos de precio ("pips"); se multiplica por el PriceStep del instrumento para obtener el offset de precio real.
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;
	}
}