Ver no GitHub

Estratégia do Sistema de Canal de Cruzamento Percentual

Esta estratégia é um port direto do consultor especialista MetaTrader Exp_PercentageCrossoverChannel_System. Ela rastreia como o preço interage com um "Percentage Crossover Channel" personalizado e reage quando os candles voltam ao interior do canal após uma ruptura anterior. O código foi reescrito com as APIs de alto nível do StockSharp e preserva o fluxo de sinais original.

Lógica de negociação

  1. Construção do indicador

    • O Percentage Crossover Channel constrói uma linha média adaptativa que permanece próxima ao preço, mas não pode se afastar mais rapidamente do que uma porcentagem fixa (Percent).
    • As bandas superior e inferior são derivadas da linha média usando a mesma distância percentual.
    • Cada candle completado recebe cor de acordo com sua relação com o canal de há Shift barras:
      • Cor 3 / 4: fechamento acima da banda superior (corpo de candle de baixa/alta respectivamente).
      • Cor 0 / 1: fechamento abaixo da banda inferior (corpo de baixa/alta respectivamente).
      • Cor 2: o candle terminou dentro do canal.
  2. Regras de entrada e saída

    • Avaliar o último candle do SignalBar e o imediatamente anterior (espelha a chamada CopyBuffer do MQL).
    • Sequência de alta (olderColor > 2): o mercado fechou recentemente acima do canal. Se o candle mais recente voltou ao interior (recentColor < 3) a estratégia:
      • Fecha qualquer short ativo se SellPositionsClose estiver habilitado.
      • Abre uma posição comprada quando não há operações abertas e BuyPositionsOpen está habilitado.
    • Sequência de baixa (olderColor < 2): o mercado fechou recentemente abaixo do canal. Se o último candle retornou ao interior (recentColor > 1) a estratégia:
      • Fecha qualquer long se BuyPositionsClose estiver habilitado.
      • Abre uma posição vendida quando não há operações ativas e SellPositionsOpen está habilitado.
    • A lógica portanto aguarda um rompimento seguido de uma re-entrada no canal antes de se comprometer na direção do rompimento.
  3. Gestão de risco

    • Stop loss e take profit opcionais são expressos em passos de preço e avaliados em máximas/mínimas de candles.
    • Se uma ordem de proteção for acionada, a estratégia sai do mercado e ignora novas entradas para a mesma barra, imitando o comportamento MQL onde stops do broker fecham a operação primeiro.

Parâmetros

Nome Descrição
Percent Largura do canal em percentual. Corresponde ao parâmetro de entrada do indicador MQL.
Shift Número de barras usadas para comparar o rompimento com as bandas históricas.
SignalBar Deslocamento (em barras) para avaliação de sinais. Um valor de 1 significa "barra anterior" como o padrão original do EA.
BuyPositionsOpen / SellPositionsOpen Habilitar ou desabilitar a abertura de operações na direção correspondente.
BuyPositionsClose / SellPositionsClose Habilitar ou desabilitar o fechamento forçado de posições opostas em um novo sinal.
StopLoss Distância do stop loss expressa em múltiplos de Security.PriceStep. Definir como zero para desabilitar.
TakeProfit Distância do take-profit em passos de preço. Definir como zero para desabilitar.
CandleType Período para assinatura de candles. Padrão são barras de quatro horas para refletir PERIOD_H4.

Notas de implementação

  • A lógica do indicador está implementada inline porque o StockSharp não fornece um Percentage Crossover Channel nativo. Os cálculos da linha média, derivação de bandas e atribuições de cores reproduzem o algoritmo-fonte do MQL passo a passo.
  • O gerenciamento de posições segue as funções auxiliares originais (BuyPositionOpen, SellPositionOpen, etc.) fechando operações opostas antes de abrir uma nova e pulando entradas quando uma posição oposta ainda está presente.
  • Gerenciamento de capital, tratamento de desvio e dimensionamento de lotes específico do modo de margem do arquivo include original não são replicados. Os usuários do StockSharp devem configurar o volume da estratégia através das propriedades padrão de Strategy ou do ambiente de hospedagem.
  • Os valores de stop loss / take profit são interpretados como passos de preço porque os parâmetros do MetaTrader são especificados em pontos. Certifique-se de que o instrumento conectado expõe um PriceStep válido.

Dicas de uso

  • Conecte a estratégia a um instrumento com dados confiáveis de quatro horas se desejar comportamento idêntico ao MetaTrader. Ajuste CandleType para experimentar operação intradiária.
  • Como a lógica de entrada requer dois candles completados com informações de cor válidas, permita que a estratégia aqueça com pelo menos Shift + SignalBar + 1 barras de histórico.
  • O canal é sensível ao parâmetro Percent. Valores menores ficam próximos ao preço e aumentam a frequência de trading, enquanto valores maiores se concentram em rompimentos mais fortes.
  • Ao combinar com controles de risco a nível de portfólio, tenha em mente que esta implementação abre no máximo uma posição por vez e alterna entre estados comprado, neutro ou vendido.
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; }
		}
	}
}