Ver en GitHub

Estrategia de Canal Plano (2684)

Esta estrategia es una conversión en C# del asesor experto de MetaTrader 5 Flat Channel (edición barabashkakvn). Detecta períodos de baja volatilidad (un canal "plano") usando el indicador de Desviación Estándar y coloca órdenes stop de ruptura en los límites del canal. Cuando el precio rompe el rango plano, la orden stop correspondiente se activa, mientras que la opuesta se cancela para evitar quedar atrapado en ambos lados del mercado.

Lógica principal

  1. Filtro de volatilidad – la estrategia se suscribe a velas y calcula la Desviación Estándar del precio mediano. Una fase plana se confirma cuando el valor sigue cayendo durante al menos FlatBars velas consecutivas.
  2. Construcción del canal – una vez confirmada la fase plana, se rastrean el máximo más alto y el mínimo más bajo del rango plano. El ancho del canal debe mantenerse entre ChannelMinPips y ChannelMaxPips (convertidos a unidades de precio mediante el tamaño de tick del instrumento).
  3. Órdenes de entrada – mientras el precio opera dentro del canal, la estrategia coloca:
    • Un buy stop en el máximo del canal con stop-loss 2 × ancho del canal por debajo de la entrada y take-profit 1 × ancho del canal por encima.
    • Un sell stop en el mínimo del canal con las distancias simétricas de stop-loss/take-profit.
  4. Vida útil de la orden – las órdenes stop pendientes expiran después de OrderLifetimeSeconds. Cuando transcurre el tiempo, se cancelan y pueden recrearse si las condiciones planas se mantienen.
  5. Gestión de posición – después de que una orden de entrada se ejecuta, la orden stop opuesta se cancela y se registran órdenes de protección nuevas (stop-loss y take-profit). La lógica opcional de punto de equilibrio mueve el stop-loss al precio de entrada una vez que el precio recorre una fracción Fibonacci (FiboTrail) de la distancia hacia el objetivo de take-profit.
  6. Ventana de trading – el filtro UseTradingHours restringe la actividad por día de la semana y por horas específicas del lunes/viernes, emulando los controles de horario del EA original.

Indicadores

  • StandardDeviation (precio mediano, longitud = StdDevPeriod) – detecta la caída de la volatilidad.
  • DonchianChannels (longitud = FlatBars) – proporciona los límites iniciales de máximo/mínimo para el canal plano.

Riesgo y gestión de capital

  • FixedVolume define el tamaño del lote cuando UseMoneyManagement está deshabilitado.
  • Cuando UseMoneyManagement está habilitado, el tamaño de la posición se estima a partir de RiskPercent del valor actual del portafolio dividido por la distancia del stop-loss expresada en dinero usando PriceStep y StepPrice.
  • Después de una operación perdedora, la siguiente posición usa FixedVolume × 4, replicando el comportamiento de recuperación del EA original.

Parámetros

Parámetro Descripción
UseTradingHours Habilitar o deshabilitar el filtro de horario por día de la semana/hora.
TradeTuesday, TradeWednesday, TradeThursday Permitir el trading en días individuales entre semana (el lunes y el viernes siempre están permitidos pero controlados por los límites horarios).
MondayStartHour, FridayStopHour Hora de inicio el lunes y hora de corte el viernes (reloj de 24h).
UseMoneyManagement, RiskPercent, FixedVolume Opciones de gestión de capital descritas anteriormente.
OrderLifetimeSeconds Tiempo de vencimiento para las órdenes de entrada pendientes (0 = sin vencimiento).
StdDevPeriod, FlatBars Configuraciones del indicador que controlan la detección de la fase plana.
ChannelMinPips, ChannelMaxPips Ancho de canal permitido expresado en pips (convertido usando el tamaño de tick del instrumento).
UseBreakeven, FiboTrail Habilitar la lógica de punto de equilibrio y establecer el multiplicador Fibonacci usado para activar el ajuste del stop.
CandleType Tipo de datos de velas o marco temporal usado para los cálculos.

Notas

  • La estrategia espera instrumentos que expongan PriceStep y StepPrice para que los umbrales basados en pips puedan convertirse a precios reales.
  • Las órdenes pendientes se recrean sólo cuando la volatilidad continúa cayendo. Si la volatilidad sube, el estado plano se reinicia y todas las órdenes de entrada se cancelan.
  • Las órdenes protectoras de stop y take-profit se cancelan automáticamente cuando la posición se cierra.

Descargo de responsabilidad

Este ejemplo se proporciona sólo con fines educativos. El rendimiento pasado de la estrategia original no garantiza resultados futuros. Prueba y ajusta los parámetros exhaustivamente antes de desplegar en mercados reales.

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>
/// Flat channel breakout strategy converted from the MetaTrader 5 version.
/// Detects consolidation via falling standard deviation, then trades breakouts of the channel.
/// </summary>
public class FlatChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _stdDevPeriod;
	private readonly StrategyParam<int> _flatBars;
	private readonly StrategyParam<decimal> _channelMinPips;
	private readonly StrategyParam<decimal> _channelMaxPips;
	private readonly StrategyParam<DataType> _candleType;

	private StandardDeviation _stdDev = null!;
	private DonchianChannels _donchian = null!;

	private decimal _previousStdDev;
	private int _flatBarCount;
	private decimal _channelHigh;
	private decimal _channelLow;

	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal _entryPrice;
	private decimal _longStop;
	private decimal _longTake;
	private decimal _shortStop;
	private decimal _shortTake;

	/// <summary>
	/// Standard deviation indicator period.
	/// </summary>
	public int StdDevPeriod
	{
		get => _stdDevPeriod.Value;
		set => _stdDevPeriod.Value = value;
	}

	/// <summary>
	/// Minimum number of bars with falling volatility required to form a flat channel.
	/// </summary>
	public int FlatBars
	{
		get => _flatBars.Value;
		set => _flatBars.Value = value;
	}

	/// <summary>
	/// Minimum channel width expressed in pips.
	/// </summary>
	public decimal ChannelMinPips
	{
		get => _channelMinPips.Value;
		set => _channelMinPips.Value = value;
	}

	/// <summary>
	/// Maximum channel width expressed in pips.
	/// </summary>
	public decimal ChannelMaxPips
	{
		get => _channelMaxPips.Value;
		set => _channelMaxPips.Value = value;
	}

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

	public FlatChannelStrategy()
	{
		_stdDevPeriod = Param(nameof(StdDevPeriod), 37)
			.SetDisplay("StdDev Period", "Standard deviation indicator period", "Indicators")
			.SetGreaterThanZero();

		_flatBars = Param(nameof(FlatBars), 2)
			.SetDisplay("Flat Bars", "Minimum bars in flat state", "Indicators")
			.SetGreaterThanZero();

		_channelMinPips = Param(nameof(ChannelMinPips), 10m)
			.SetDisplay("Channel Min Pips", "Minimum channel width in pips", "Indicators")
			.SetGreaterThanZero();

		_channelMaxPips = Param(nameof(ChannelMaxPips), 100000m)
			.SetDisplay("Channel Max Pips", "Maximum channel width in pips", "Indicators")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type", "General");
	}

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

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

		_previousStdDev = 0m;
		_flatBarCount = 0;
		_channelHigh = 0m;
		_channelLow = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
	}

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

		_stdDev = new StandardDeviation { Length = StdDevPeriod };
		_donchian = new DonchianChannels { Length = FlatBars };

		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(_donchian, ProcessCandle)
			.Start();

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

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

		var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;
		var stdDevValue = _stdDev.Process(new DecimalIndicatorValue(_stdDev, medianPrice, candle.CloseTime) { IsFinal = true }).ToDecimal();

		if (!_stdDev.IsFormed || channelValue is not DonchianChannelsValue donchianValue)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		if (donchianValue.UpperBand is not decimal upper || donchianValue.LowerBand is not decimal lower)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		// Update flat state based on StdDev direction.
		UpdateStdDevState(stdDevValue, upper, lower, candle);

		// Check simulated pending entries.
		CheckPendingEntries(candle);

		// Manage existing positions with SL/TP.
		ManagePosition(candle);

		// If flat and no position, set up pending breakout entries.
		if (Position == 0 && _flatBarCount >= FlatBars && _channelHigh > _channelLow)
		{
			var channelWidth = _channelHigh - _channelLow;
			var priceStep = Security?.PriceStep ?? 0.01m;
			if (priceStep <= 0m) priceStep = 0.01m;
			var minWidth = ChannelMinPips * priceStep;
			var maxWidth = ChannelMaxPips * priceStep;

			if (channelWidth >= minWidth && channelWidth <= maxWidth)
			{
				// Set pending breakout entries at channel boundaries.
				_pendingBuyPrice = _channelHigh;
				_pendingSellPrice = _channelLow;
				_longStop = _channelHigh - channelWidth * 2m;
				_longTake = _channelHigh + channelWidth;
				_shortStop = _channelLow + channelWidth * 2m;
				_shortTake = _channelLow - channelWidth;
			}
		}

		_previousStdDev = stdDevValue;
	}

	private void UpdateStdDevState(decimal stdDevValue, decimal upper, decimal lower, ICandleMessage candle)
	{
		if (_previousStdDev == 0m)
		{
			_previousStdDev = stdDevValue;
			return;
		}

		if (stdDevValue < _previousStdDev)
		{
			_flatBarCount++;

			if (_flatBarCount == FlatBars)
			{
				_channelHigh = upper;
				_channelLow = lower;
			}
			else if (_flatBarCount > FlatBars)
			{
				if (candle.HighPrice > _channelHigh)
					_channelHigh = candle.HighPrice;
				if (candle.LowPrice < _channelLow)
					_channelLow = candle.LowPrice;
			}
		}
		else if (stdDevValue > _previousStdDev)
		{
			_flatBarCount = 0;
			_channelHigh = 0m;
			_channelLow = 0m;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
		else if (_flatBarCount >= FlatBars && _channelHigh <= _channelLow)
		{
			_channelHigh = upper;
			_channelLow = lower;
		}
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (Position != 0)
			return;

		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
		{
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
		{
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop > 0m && candle.LowPrice <= _longStop)
			{
				SellMarket();
				ResetPositionState();
				return;
			}
			if (_longTake > 0m && candle.HighPrice >= _longTake)
			{
				SellMarket();
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			if (_shortStop > 0m && candle.HighPrice >= _shortStop)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}
			if (_shortTake > 0m && candle.LowPrice <= _shortTake)
			{
				BuyMarket();
				ResetPositionState();
			}
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
	}
}