Ver no GitHub

Estratégia de Canal de Cruzamento Percentual

Visão geral

A Estratégia de Canal de Cruzamento Percentual origina-se do consultor especialista de MetaTrader 5 Percentage_Crossover_Channel_EA. Ela depende de um canal personalizado construído em torno de uma média móvel rápida e reage a toques de banda ou cruzamentos da linha média. Esta implementação do StockSharp segue a mesma lógica enquanto usa a API de alto nível para processar velas concluídas.

Construção do canal

O indicador subjacente constrói um canal dinâmico em torno do preço selecionado (fechamento por padrão):

  1. Calcular o preço base usando o modo Applied Price configurado.
  2. Aplicar uma média móvel simples de 1 período para obter o preço de referência de curto prazo.
  3. Calcular dois limites usando o parâmetro Percent (p. ex., 50 → ±0,5%).
  4. Limitar a linha média anterior dentro dos novos limites para obter o valor médio atual.
  5. As bandas superior e inferior são o valor médio limitado multiplicado pelos fatores ±percentagem.

Esta recursão permite que o canal atrase durante tendências fortes enquanto mantém um envelope apertado quando o preço consolida.

Lógica de trading

Dois modos de sinal diferentes estão disponíveis:

  • Modo de toque de banda (padrão):
    • Entrada comprada quando o mínimo da vela anterior estava acima da banda inferior e a última vela concluída a toca ou perfura.
    • Entrada vendida quando o máximo da vela anterior estava abaixo da banda superior e a última vela concluída a toca ou perfura.
  • Modo de cruzamento da linha média (TradeOnMiddleCross = true):
    • Entrada comprada quando o preço cruza a linha média de cima para baixo.
    • Entrada vendida quando o preço cruza a linha média de baixo para cima.

O indicador ReverseSignals troca as regras compradas e vendidas. A estratégia sempre fecha e reverte posições existentes enviando uma única ordem a mercado cujo volume equivale ao OrderVolume configurado mais o valor absoluto da posição atual.

Gestão de risco

Os níveis protetores opcionais emulam as configurações originais de stop-loss e take-profit do MT5:

  • StopLossPoints – distância em passos de preço subtraída (comprado) ou adicionada (vendido) do preço de entrada estimado.
  • TakeProfitPoints – distância em passos de preço adicionada (comprado) ou subtraída (vendido) do preço de entrada.

Se qualquer parâmetro for zero, a proteção correspondente é desativada. Os stops são avaliados em cada vela finalizada comparando máximos e mínimos da vela com os níveis armazenados. Nenhuma lógica de trailing é aplicada.

Parâmetros

Parâmetro Descrição
CandleType Tipo de dados de vela para assinar (período de 15 minutos por padrão).
Percent Largura do canal em porcentagem do preço (convertido para fatores ±porcentagem/100).
PriceMode Preço aplicado para o canal. Opções: Close, Open, High, Low, Median (H+L)/2, Typical (H+L+C)/3, Weighted (H+L+2C)/4, Average (O+H+L+C)/4.
TradeOnMiddleCross Alternar entre lógica de toque de banda e lógica de cruzamento de linha média.
ReverseSignals Inverter as condições compradas e vendidas.
StopLossPoints Distância do stop protetor expressa em passos de preço do instrumento.
TakeProfitPoints Distância do alvo de lucro expressa em passos de preço do instrumento.
OrderVolume Volume base para entradas a mercado. A estratégia adiciona a posição aberta absoluta para reverter em uma transação.

Notas de implementação

  • As ordens são emitidas apenas após as velas terminarem, o que espelha o consultor especialista do MT5 que agia no início da próxima barra usando os dados da barra anterior.
  • O indicador de canal é recriado dentro da estratégia sem armazenar coleções históricas, contando com variáveis de estado escalares.
  • Stops e alvos de proteção são verificados manualmente para replicar o tratamento de ordens específico da plataforma do MT5.
  • Garantir que o instrumento selecionado exponha um PriceStep válido; caso contrário, as distâncias de stop-loss e take-profit serão ignoradas.
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 strategy converted from MetaTrader 5.
/// </summary>
public class PercentageCrossoverChannelStrategy : Strategy
{
	public enum PercentageChannelPriceModes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Average
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<PercentageChannelPriceModes> _priceMode;
	private readonly StrategyParam<bool> _tradeOnMiddleCross;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _orderVolume;

	// Cached indicator values for the previous two finished candles.
	private decimal? _prevUpper;
	private decimal? _prevMiddle;
	private decimal? _prevLower;
	private decimal? _prevPrevUpper;
	private decimal? _prevPrevMiddle;
	private decimal? _prevPrevLower;

	// Stored price data for signal evaluation.
	private decimal? _prevClose;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _prevPrevClose;
	private decimal? _prevPrevHigh;
	private decimal? _prevPrevLow;

	// Internal state of the channel middle line recursion.
	private decimal _lastMiddle;
	private bool _hasIndicatorState;

	// Protective levels that mimic MT5 stop loss and take profit requests.
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _entryPrice;

	public PercentageCrossoverChannelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for processing", "General");

		_percent = Param(nameof(Percent), 1m)
			.SetDisplay("Percent", "Channel width percent", "Channel")
			
			.SetGreaterThanZero();

		_priceMode = Param(nameof(PriceMode), PercentageChannelPriceModes.Close)
			.SetDisplay("Applied Price", "Price source for channel calculations", "Channel");

		_tradeOnMiddleCross = Param(nameof(TradeOnMiddleCross), false)
			.SetDisplay("Trade Middle Cross", "Use middle line crossovers instead of band touches", "Signals");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short logic", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss (points)", "Protective stop distance in points", "Risk")
			.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit (points)", "Target profit distance in points", "Risk")
			.SetNotNegative();

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetDisplay("Order Volume", "Base volume for market entries", "Trading")
			.SetGreaterThanZero();
	}

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

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

	public PercentageChannelPriceModes PriceMode
	{
		get => _priceMode.Value;
		set => _priceMode.Value = value;
	}

	public bool TradeOnMiddleCross
	{
		get => _tradeOnMiddleCross.Value;
		set => _tradeOnMiddleCross.Value = value;
	}

	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

		_prevUpper = null;
		_prevMiddle = null;
		_prevLower = null;
		_prevPrevUpper = null;
		_prevPrevMiddle = null;
		_prevPrevLower = null;

		_prevClose = null;
		_prevHigh = null;
		_prevLow = null;
		_prevPrevClose = null;
		_prevPrevHigh = null;
		_prevPrevLow = null;

		_lastMiddle = 0m;
		_hasIndicatorState = false;

		ResetProtection();
	}

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

		Volume = OrderVolume;

		// Subscribe to candle updates that will drive the high level logic.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with completed candles to stay consistent with the MT5 implementation.
		if (candle.State != CandleStates.Finished)
			return;

		var exitTriggered = CheckProtection(candle);

		if (!exitTriggered)
			TryEnterPositions(candle);

		UpdateChannelState(candle);
	}

	private void TryEnterPositions(ICandleMessage candle)
	{
		// Wait until the channel has valid values for two completed candles.
		if (!_prevLower.HasValue || !_prevPrevLower.HasValue)
			return;

		if (!_prevClose.HasValue || !_prevPrevClose.HasValue || !_prevHigh.HasValue || !_prevPrevHigh.HasValue || !_prevLow.HasValue || !_prevPrevLow.HasValue)
			return;

		var openLong = false;
		var openShort = false;

		if (TradeOnMiddleCross)
		{
			// Evaluate crossovers of the price and the middle channel line.
			var crossDown = _prevPrevClose.Value > _prevPrevMiddle.Value && _prevClose.Value < _prevMiddle.Value;
			var crossUp = _prevPrevClose.Value < _prevPrevMiddle.Value && _prevClose.Value > _prevMiddle.Value;

			if (!ReverseSignals)
			{
				if (crossDown)
					openLong = true;

				if (crossUp)
					openShort = true;
			}
			else
			{
				if (crossDown)
					openShort = true;

				if (crossUp)
					openLong = true;
			}
		}
		else
		{
			// Default mode trades touches of the outer channel boundaries.
			var touchLower = _prevPrevLow.Value > _prevPrevLower.Value && _prevLow.Value <= _prevLower.Value;
			var touchUpper = _prevPrevHigh.Value < _prevPrevUpper.Value && _prevHigh.Value >= _prevUpper.Value;

			if (!ReverseSignals)
			{
				if (touchLower)
					openLong = true;

				if (touchUpper)
					openShort = true;
			}
			else
			{
				if (touchLower)
					openShort = true;

				if (touchUpper)
					openLong = true;
			}
		}

		if (openLong)
		{
			EnterLong(candle);
		}
		else if (openShort)
		{
			EnterShort(candle);
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		// Combine base order volume with the size required to flatten shorts.
		var volume = OrderVolume + (Position < 0 ? Math.Abs(Position) : 0m);
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_entryPrice = candle.OpenPrice;
		_stopPrice = CalculateStopPrice(Sides.Buy, _entryPrice);
		_takePrice = CalculateTakePrice(Sides.Buy, _entryPrice);
	}

	private void EnterShort(ICandleMessage candle)
	{
		// Combine base order volume with the size required to flatten longs.
		var volume = OrderVolume + (Position > 0 ? Position : 0m);
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_entryPrice = candle.OpenPrice;
		_stopPrice = CalculateStopPrice(Sides.Sell, _entryPrice);
		_takePrice = CalculateTakePrice(Sides.Sell, _entryPrice);
	}

	private bool CheckProtection(ICandleMessage candle)
	{
		// Emulate MT5 protective stop and take profit that were attached to market orders.
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return true;
			}
		}
		else
		{
			ResetProtection();
		}

		return false;
	}

	private void UpdateChannelState(ICandleMessage candle)
	{
		// Recreate the Percentage Crossover Channel middle line recursion.
		var percent = Percent <= 0m ? 0.001m : Percent;
		var plusFactor = 1m + percent / 100m;
		var minusFactor = 1m - percent / 100m;
		var price = GetAppliedPrice(candle);

		decimal currentMiddle;
		if (!_hasIndicatorState)
		{
			currentMiddle = price;
			_hasIndicatorState = true;
		}
		else
		{
			var lowerBound = price * minusFactor;
			var upperBound = price * plusFactor;
			var previousMiddle = _lastMiddle;

			currentMiddle = previousMiddle;

			if (lowerBound > previousMiddle)
				currentMiddle = lowerBound;
			else if (upperBound < previousMiddle)
				currentMiddle = upperBound;
		}

		var currentUpper = currentMiddle * plusFactor;
		var currentLower = currentMiddle * minusFactor;

		if (_prevUpper.HasValue)
		{
			_prevPrevUpper = _prevUpper;
			_prevPrevMiddle = _prevMiddle;
			_prevPrevLower = _prevLower;
			_prevPrevClose = _prevClose;
			_prevPrevHigh = _prevHigh;
			_prevPrevLow = _prevLow;
		}

		_prevUpper = currentUpper;
		_prevMiddle = currentMiddle;
		_prevLower = currentLower;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_lastMiddle = currentMiddle;
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		// Convert the selected price mode into a candle value.
		return PriceMode switch
		{
			PercentageChannelPriceModes.Open => candle.OpenPrice,
			PercentageChannelPriceModes.High => candle.HighPrice,
			PercentageChannelPriceModes.Low => candle.LowPrice,
			PercentageChannelPriceModes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			PercentageChannelPriceModes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			PercentageChannelPriceModes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			PercentageChannelPriceModes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private decimal? CalculateStopPrice(Sides side, decimal entryPrice)
	{
		if (StopLossPoints <= 0)
			return null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return null;

		var offset = StopLossPoints * step;
		return side == Sides.Buy ? entryPrice - offset : entryPrice + offset;
	}

	private decimal? CalculateTakePrice(Sides side, decimal entryPrice)
	{
		if (TakeProfitPoints <= 0)
			return null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return null;

		var offset = TakeProfitPoints * step;
		return side == Sides.Buy ? entryPrice + offset : entryPrice - offset;
	}

	private void ResetProtection()
	{
		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}
}