Ver en GitHub

Estrategia del Sistema de Canal de Cruce Porcentual

Esta estrategia es un port directo del asesor experto de MetaTrader Exp_PercentageCrossoverChannel_System. Rastrea cómo el precio interactúa con un "Percentage Crossover Channel" personalizado y reacciona cuando los candles vuelven al interior del canal después de haber roto previamente. El código fue reescrito con las APIs de alto nivel de StockSharp y preserva el flujo de señales original.

Lógica de trading

  1. Construcción del indicador

    • El Percentage Crossover Channel construye una línea media adaptativa que permanece cerca del precio pero no puede alejarse más rápido que un porcentaje fijo (Percent).
    • Las bandas superior e inferior se derivan de la línea media usando la misma distancia porcentual.
    • Cada candle completado recibe color según su relación con el canal de hace Shift barras:
      • Color 3 / 4: cierre por encima de la banda superior (cuerpo de candle bajista/alcista respectivamente).
      • Color 0 / 1: cierre por debajo de la banda inferior (cuerpo bajista/alcista respectivamente).
      • Color 2: el candle terminó dentro del canal.
  2. Reglas de entrada y salida

    • Evaluar el último candle de SignalBar y el inmediatamente anterior (espeja la llamada CopyBuffer de MQL).
    • Secuencia alcista (olderColor > 2): el mercado recientemente cerró por encima del canal. Si el candle más reciente volvió al interior (recentColor < 3) la estrategia:
      • Cierra cualquier corto activo si SellPositionsClose está habilitado.
      • Abre una posición larga cuando no hay operaciones abiertas y BuyPositionsOpen está habilitado.
    • Secuencia bajista (olderColor < 2): el mercado recientemente cerró por debajo del canal. Si el último candle retornó al interior (recentColor > 1) la estrategia:
      • Cierra cualquier largo si BuyPositionsClose está habilitado.
      • Abre una posición corta cuando no hay operaciones activas y SellPositionsOpen está habilitado.
    • La lógica por tanto espera una ruptura seguida de una re-entrada al canal antes de comprometerse en la dirección de la ruptura.
  3. Gestión del riesgo

    • El stop loss y take profit opcionales se expresan en pasos de precio y se evalúan en los máximos/mínimos de los candles.
    • Si se activa una orden de protección la estrategia sale del mercado e ignora nuevas entradas para la misma barra, imitando el comportamiento de MQL donde los stops del broker cierran la operación primero.

Parámetros

Nombre Descripción
Percent Ancho del canal en porcentaje. Coincide con el parámetro de entrada del indicador MQL.
Shift Número de barras usadas para comparar la ruptura con las bandas históricas.
SignalBar Desplazamiento (en barras) para la evaluación de señales. Un valor de 1 significa "barra anterior" como el valor predeterminado del EA original.
BuyPositionsOpen / SellPositionsOpen Habilitar o deshabilitar la apertura de operaciones en la dirección correspondiente.
BuyPositionsClose / SellPositionsClose Habilitar o deshabilitar el cierre forzado de posiciones opuestas ante una nueva señal.
StopLoss Distancia del stop loss expresada en múltiplos de Security.PriceStep. Poner en cero para deshabilitar.
TakeProfit Distancia del take-profit en pasos de precio. Poner en cero para deshabilitar.
CandleType Marco temporal para la suscripción de candles. Por defecto barras de cuatro horas para reflejar PERIOD_H4.

Notas de implementación

  • La lógica del indicador está implementada inline porque StockSharp no proporciona un Percentage Crossover Channel nativo. Los cálculos de la línea media, derivación de bandas y asignaciones de color reproducen el algoritmo fuente de MQL paso a paso.
  • La gestión de posiciones sigue las funciones auxiliares originales (BuyPositionOpen, SellPositionOpen, etc.) cerrando operaciones opuestas antes de abrir una nueva y omitiendo entradas cuando hay una posición opuesta activa.
  • La gestión del dinero, el manejo de desviaciones y el dimensionamiento de lotes específico del modo de margen del archivo include original no están replicados. Los usuarios de StockSharp deben configurar el volumen de la estrategia a través de las propiedades estándar de Strategy o del entorno de hospedaje.
  • Los valores de stop loss / take profit se interpretan como pasos de precio porque los parámetros de MetaTrader se especifican en puntos. Asegúrese de que el instrumento conectado exponga un PriceStep válido.

Consejos de uso

  • Conecte la estrategia a un instrumento con datos confiables de cuatro horas si desea un comportamiento idéntico a MetaTrader. Ajuste CandleType para experimentar con operación intradía.
  • Dado que la lógica de entrada requiere dos candles completados con información de color válida, permita que la estrategia se caliente con al menos Shift + SignalBar + 1 barras de historial.
  • El canal es sensible al parámetro Percent. Valores más pequeños se ajustan estrechamente al precio y aumentan la frecuencia de trading, mientras que valores más grandes se enfocan en rupturas más fuertes.
  • Al combinar con controles de riesgo a nivel de cartera, tenga en cuenta que esta implementación abre como máximo una posición a la vez y alterna entre estados largo, plano o corto.
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>
/// Percentage Crossover Channel breakout system translated from MQL.
/// </summary>
public class PercentageCrossoverChannelSystemStrategy : Strategy
{
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _buyOpen;
	private readonly StrategyParam<bool> _sellOpen;
	private readonly StrategyParam<bool> _buyClose;
	private readonly StrategyParam<bool> _sellClose;
	private readonly StrategyParam<int> _stopLoss;
	private readonly StrategyParam<int> _takeProfit;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _colorHistory = new();
	private readonly List<decimal> _upperHistory = new();
	private readonly List<decimal> _lowerHistory = new();

	private decimal _previousMiddle;
	private bool _hasMiddle;
	private decimal? _entryPrice;

	public decimal Percent
	{
		get => _percent.Value;
		set => _percent.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	public bool BuyPositionsOpen
	{
		get => _buyOpen.Value;
		set => _buyOpen.Value = value;
	}

	public bool SellPositionsOpen
	{
		get => _sellOpen.Value;
		set => _sellOpen.Value = value;
	}

	public bool BuyPositionsClose
	{
		get => _buyClose.Value;
		set => _buyClose.Value = value;
	}

	public bool SellPositionsClose
	{
		get => _sellClose.Value;
		set => _sellClose.Value = value;
	}

	public int StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	public int TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PercentageCrossoverChannelSystemStrategy()
	{
		_percent = Param(nameof(Percent), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Channel Percent", "Percentage width of the channel", "Indicator");

		_shift = Param(nameof(Shift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift", "Number of bars used for crossover comparison", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Bars back to evaluate indicator colors", "Trading Rules");

		_buyOpen = Param(nameof(BuyPositionsOpen), true)
			.SetDisplay("Enable Long Entries", "Allow long position openings", "Trading Rules");

		_sellOpen = Param(nameof(SellPositionsOpen), true)
			.SetDisplay("Enable Short Entries", "Allow short position openings", "Trading Rules");

		_buyClose = Param(nameof(BuyPositionsClose), true)
			.SetDisplay("Allow Long Exits", "Permit closing long trades on bearish signals", "Trading Rules");

		_sellClose = Param(nameof(SellPositionsClose), true)
			.SetDisplay("Allow Short Exits", "Permit closing short trades on bullish signals", "Trading Rules");

		_stopLoss = Param(nameof(StopLoss), 1000)
			.SetDisplay("Stop Loss (steps)", "Protective stop loss distance in price steps", "Risk Management");

		_takeProfit = Param(nameof(TakeProfit), 2000)
			.SetDisplay("Take Profit (steps)", "Target profit distance in price steps", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for analysis", "General");
	}

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

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

		_colorHistory.Clear();
		_upperHistory.Clear();
		_lowerHistory.Clear();
		_hasMiddle = false;
		_previousMiddle = 0m;
		_entryPrice = null;
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore interim updates; we only react on closed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Evaluate protective orders before generating new signals.
		var stopTriggered = HandleRisk(candle);

		// Mirror the MQL signal logic using cached indicator colors.
		if (_colorHistory.Count > SignalBar)
		{
			// Equivalent to CopyBuffer(..., SignalBar, 2, ...) from the EA.
			var recentIndex = _colorHistory.Count - SignalBar;
			var olderIndex = recentIndex - 1;

			if (olderIndex >= 0)
			{
				var recentColor = _colorHistory[recentIndex];
				var olderColor = _colorHistory[olderIndex];

				var shouldCloseShort = SellPositionsClose && olderColor > 2;
				var shouldCloseLong = BuyPositionsClose && olderColor < 2;
				var shouldOpenBuy = BuyPositionsOpen && olderColor > 2 && recentColor < 3;
				var shouldOpenSell = SellPositionsOpen && olderColor < 2 && recentColor > 1;

				// Close existing positions according to the original toggles.
				if (shouldCloseLong && Position > 0)
				{
					SellMarket();
					_entryPrice = null;
				}

				if (shouldCloseShort && Position < 0)
				{
					BuyMarket();
					_entryPrice = null;
				}

				// Enter only when we are flat to match the EA behaviour.
				if (!stopTriggered && Position == 0)
				{
					if (shouldOpenBuy)
					{
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
					else if (shouldOpenSell)
					{
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
		}

		// Update indicator state after trading decisions are made.
		var color = CalculateColor(candle);

		_colorHistory.Add(color);
		TrimHistory();
	}

	private bool HandleRisk(ICandleMessage candle)
	{
		// Exit early if there is no stored entry price.
		if (_entryPrice is null)
			return false;

		// Price step is required to translate MQL points into absolute prices.
		if (Security?.PriceStep is not decimal step || step <= 0)
			return false;

		var triggered = false;

		if (Position > 0)
		{
			// Long position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value - StopLoss * step;
				if (candle.LowPrice <= stopLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value + TakeProfit * step;
				if (candle.HighPrice >= takeLevel)
				{
					SellMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}
		else if (Position < 0)
		{
			// Short position risk checks.
			if (StopLoss > 0)
			{
				var stopLevel = _entryPrice.Value + StopLoss * step;
				if (candle.HighPrice >= stopLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}

			if (!triggered && TakeProfit > 0)
			{
				var takeLevel = _entryPrice.Value - TakeProfit * step;
				if (candle.LowPrice <= takeLevel)
				{
					BuyMarket();
					_entryPrice = null;
					triggered = true;
				}
			}
		}

		// Reset cached entry price once we are flat.
		if (Position == 0)
			_entryPrice = null;

		return triggered;
	}

	private int CalculateColor(ICandleMessage candle)
	{
		// Recreate the Percentage Crossover Channel midline and colour logic.
		var percentFactor = Percent / 100m;
		var plusVar = 1m + percentFactor;
		var minusVar = 1m - percentFactor;
		var close = candle.ClosePrice;

		// Initialise the midline on the very first candle.
		if (!_hasMiddle)
		{
			_previousMiddle = close;
			_hasMiddle = true;
		}

		var middle = _previousMiddle;
		var lowerCandidate = close * minusVar;
		var upperCandidate = close * plusVar;

		// Adjust the midline exactly as in the original indicator.
		if (lowerCandidate > _previousMiddle)
		{
			middle = lowerCandidate;
		}
		else if (upperCandidate < _previousMiddle)
		{
			middle = upperCandidate;
		}

		var upper = middle + middle * percentFactor;
		var lower = middle - middle * percentFactor;

		_previousMiddle = middle;

		var color = 2;

		// Determine candle colour relative to past channel values.
		if (_upperHistory.Count >= Shift)
		{
			var referenceIndex = _upperHistory.Count - Shift;
			var referenceUpper = _upperHistory[referenceIndex];
			var referenceLower = _lowerHistory[referenceIndex];

			if (close > referenceUpper)
			{
				color = candle.OpenPrice <= close ? 4 : 3;
			}
			else if (close < referenceLower)
			{
				color = candle.OpenPrice > close ? 0 : 1;
			}
		}

		// Persist channel history for future signal checks.
		_upperHistory.Add(upper);
		_lowerHistory.Add(lower);

		return color;
	}

	private void TrimHistory()
	{
		// Keep only as much history as needed for Shift and SignalBar lookbacks.
		var maxCapacity = Math.Max(Shift + SignalBar + 5, 16);
		if (_colorHistory.Count <= maxCapacity)
			return;

		var removeCount = _colorHistory.Count - maxCapacity;
		for (var i = 0; i < removeCount; i++)
		{
			try
			{
				_colorHistory.RemoveAt(0);
				_upperHistory.RemoveAt(0);
				_lowerHistory.RemoveAt(0);
			}
			catch { break; }
		}
	}
}