Ver no GitHub

Estratégia de Cruzamento de Canais com Envelope

Visão geral

A estratégia de Cruzamento de Canais com Envelope é um port direto do consultor especialista MetaTrader "Channels". O sistema opera velas horárias e monitora uma média móvel exponencial (EMA) rápida de dois períodos em relação a três envelopes baseados em EMA (desvios de 0.3%, 0.7% e 1.0%) calculados a partir de uma EMA lenta de 220 períodos. Rompimentos da EMA rápida através desses envelopes geram entradas direcionais, enquanto um filtro de tempo opcional restringe o trading a horas específicas.

Lógica de trading

  1. Pilha de indicadores
    • EMA rápida (comprimento 2) calculada sobre preços de fechamento de vela.
    • EMA rápida (comprimento 2) calculada sobre preços de abertura de vela.
    • EMA lenta (comprimento 220) calculada sobre preços de fechamento de vela.
    • Três níveis de envelope derivados da EMA lenta com desvios de 0.3%, 0.7% e 1.0%.
  2. Configuração comprada
    • Ativada quando a EMA rápida de fechamento cruza acima do envelope inferior de 1.0% ou 0.7%, permanece abaixo do envelope inferior de 0.3% por duas barras consecutivas, cruza acima da EMA lenta, ou rompe os envelopes superiores de 0.3% ou 0.7%. Qualquer uma dessas condições pode disparar uma entrada comprada quando não há posição aberta.
  3. Configuração vendida
    • Ativada quando a EMA rápida de abertura cruza abaixo de qualquer um dos envelopes superiores, cai abaixo da EMA lenta, ou perfura os envelopes inferiores de cima. Qualquer uma dessas condições pode disparar uma entrada vendida quando não há posição aberta.
  4. Gerenciamento de risco
    • Níveis fixos de stop-loss e take-profit (por lado) são expressos em pips e convertidos para distância de preço usando o tamanho de tick do instrumento. Se as entradas forem definidas como zero, o nível respectivo não é aplicado.
    • Trailing stops independentes para posições compradas e vendidas movem o stop de proteção para mais perto do preço de mercado quando o lucro excede a distância de trailing mais um incremento de passo configurável.
  5. Filtro de tempo
    • Quando habilitado, a estratégia processa entradas apenas durante o intervalo de horas inclusivo configurado. As posições ainda são gerenciadas quando o filtro está ativo.

Parâmetros

Parâmetro Descrição
OrderVolume Tamanho da ordem usado para entradas de mercado (lotes ou contratos dependendo do instrumento).
UseTradeHours Habilita o filtro de tempo para entradas.
FromHour / ToHour Horas de início e fim inclusivas para a janela de trading (suporta intervalos noturnos).
StopLossBuyPips / StopLossSellPips Distância do stop-loss para operações compradas/vendidas expressa em pips.
TakeProfitBuyPips / TakeProfitSellPips Distância do take-profit para operações compradas/vendidas expressa em pips.
TrailingStopBuyPips / TrailingStopSellPips Distância do trailing stop em pips para operações compradas/vendidas.
TrailingStepPips Incremento mínimo (em pips) necessário para mover um trailing stop.
CandleType Série de velas usada para cálculos (padrão é período de 1 hora).

Gerenciamento de posições

  • Na entrada, a estratégia armazena o preço de execução, calcula os alvos de stop-loss e take-profit em unidades de preço absoluto e redefine os níveis de trailing.
  • Enquanto uma posição comprada estiver aberta, o stop-loss é seguido para cima sempre que o lucro exceder TrailingStopBuyPips + TrailingStepPips. A estratégia sai no stop-loss ou take-profit, o que for atingido primeiro.
  • Enquanto uma posição vendida estiver aberta, o stop-loss é seguido para baixo usando os parâmetros de trailing do lado vendido e as saídas são executadas simetricamente.

Notas

  • O tamanho do pip é derivado do tamanho do tick do instrumento. Para instrumentos de três ou cinco decimais, o pip é multiplicado por dez para emular a lógica do MetaTrader.
  • A estratégia trabalha com uma única posição por vez. Uma nova entrada só é colocada depois que a posição existente foi fechada.
  • Habilite StartProtection na classe base para proteção contra posições abertas inesperadas após reinicializações (já chamado na implementação).
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>
/// Channels envelope crossover strategy converted from the MetaTrader Channels expert advisor.
/// The strategy monitors EMA based envelopes on hourly candles and trades breakouts of the fast EMA through the bands.
/// </summary>
public class ChannelsEnvelopeCrossStrategy : Strategy
{
	private readonly StrategyParam<decimal> _envelope003;
	private readonly StrategyParam<decimal> _envelope007;
	private readonly StrategyParam<decimal> _envelope010;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _useTradeHours;
	private readonly StrategyParam<int> _fromHour;
	private readonly StrategyParam<int> _toHour;
	private readonly StrategyParam<int> _stopLossBuyPips;
	private readonly StrategyParam<int> _stopLossSellPips;
	private readonly StrategyParam<int> _takeProfitBuyPips;
	private readonly StrategyParam<int> _takeProfitSellPips;
	private readonly StrategyParam<int> _trailingStopBuyPips;
	private readonly StrategyParam<int> _trailingStopSellPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _emaFastClose;
	private ExponentialMovingAverage _emaFastOpen;
	private ExponentialMovingAverage _emaSlow;

	private bool _hasPreviousValues;
	private decimal _prevFastClose;
	private decimal _prevFastOpen;
	private decimal _prevSlow;
	private decimal _prevEnvLower03;
	private decimal _prevEnvUpper03;
	private decimal _prevEnvLower07;
	private decimal _prevEnvUpper07;
	private decimal _prevEnvLower10;
	private decimal _prevEnvUpper10;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Order volume used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Enable trading only within the configured time window.
	/// </summary>
	public bool UseTradeHours
	{
		get => _useTradeHours.Value;
		set => _useTradeHours.Value = value;
	}

	/// <summary>
	/// Start hour of the trading window (inclusive).
	/// </summary>
	public int FromHour
	{
		get => _fromHour.Value;
		set => _fromHour.Value = value;
	}

	/// <summary>
	/// End hour of the trading window (inclusive).
	/// </summary>
	public int ToHour
	{
		get => _toHour.Value;
		set => _toHour.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for long positions expressed in pips.
	/// </summary>
	public int StopLossBuyPips
	{
		get => _stopLossBuyPips.Value;
		set => _stopLossBuyPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for short positions expressed in pips.
	/// </summary>
	public int StopLossSellPips
	{
		get => _stopLossSellPips.Value;
		set => _stopLossSellPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance for long positions expressed in pips.
	/// </summary>
	public int TakeProfitBuyPips
	{
		get => _takeProfitBuyPips.Value;
		set => _takeProfitBuyPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance for short positions expressed in pips.
	/// </summary>
	public int TakeProfitSellPips
	{
		get => _takeProfitSellPips.Value;
		set => _takeProfitSellPips.Value = value;
	}

	/// <summary>
	/// Trailing-stop size for long positions expressed in pips.
	/// </summary>
	public int TrailingStopBuyPips
	{
		get => _trailingStopBuyPips.Value;
		set => _trailingStopBuyPips.Value = value;
	}

	/// <summary>
	/// Trailing-stop size for short positions expressed in pips.
	/// </summary>
	public int TrailingStopSellPips
	{
		get => _trailingStopSellPips.Value;
		set => _trailingStopSellPips.Value = value;
	}

	/// <summary>
	/// Minimum increment for trailing adjustments expressed in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Percentage width for the 0.3% envelope band.
	/// </summary>
	public decimal Envelope003
	{
		get => _envelope003.Value;
		set => _envelope003.Value = value;
	}

	/// <summary>
	/// Percentage width for the 0.7% envelope band.
	/// </summary>
	public decimal Envelope007
	{
		get => _envelope007.Value;
		set => _envelope007.Value = value;
	}

	/// <summary>
	/// Percentage width for the 1.0% envelope band.
	/// </summary>
	public decimal Envelope010
	{
		get => _envelope010.Value;
		set => _envelope010.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="ChannelsEnvelopeCrossStrategy"/>.
	/// </summary>
	public ChannelsEnvelopeCrossStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Order volume in lots", "Trading");

		_useTradeHours = Param(nameof(UseTradeHours), false)
		.SetDisplay("Use Trade Hours", "Restrict trading to specified hours", "Trading");

		_fromHour = Param(nameof(FromHour), 0)
		.SetDisplay("From Hour", "Start hour for trading window", "Trading");

		_toHour = Param(nameof(ToHour), 23)
		.SetDisplay("To Hour", "End hour for trading window", "Trading");

		_stopLossBuyPips = Param(nameof(StopLossBuyPips), 0)
		.SetDisplay("SL BUY (pips)", "Stop loss distance for long positions", "Risk");

		_stopLossSellPips = Param(nameof(StopLossSellPips), 0)
		.SetDisplay("SL SELL (pips)", "Stop loss distance for short positions", "Risk");

		_takeProfitBuyPips = Param(nameof(TakeProfitBuyPips), 0)
		.SetDisplay("TP BUY (pips)", "Take profit distance for long positions", "Risk");

		_takeProfitSellPips = Param(nameof(TakeProfitSellPips), 0)
		.SetDisplay("TP SELL (pips)", "Take profit distance for short positions", "Risk");

		_trailingStopBuyPips = Param(nameof(TrailingStopBuyPips), 30)
		.SetDisplay("Trail BUY (pips)", "Trailing stop for long positions", "Risk");

		_trailingStopSellPips = Param(nameof(TrailingStopSellPips), 30)
		.SetDisplay("Trail SELL (pips)", "Trailing stop for short positions", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 1)
		.SetDisplay("Trailing Step (pips)", "Minimum increment for trailing stop", "Risk");

		_envelope003 = Param(nameof(Envelope003), 0.3m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 0.3%", "Width of the 0.3% envelope", "Indicators");

		_envelope007 = Param(nameof(Envelope007), 0.7m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 0.7%", "Width of the 0.7% envelope", "Indicators");

		_envelope010 = Param(nameof(Envelope010), 1.0m / 100m)
			.SetGreaterThanZero()
			.SetDisplay("Envelope 1.0%", "Width of the 1.0% envelope", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for calculations", "General");
	}

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

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

		_hasPreviousValues = false;
		_prevFastClose = 0m;
		_prevFastOpen = 0m;
		_prevSlow = 0m;
		_prevEnvLower03 = 0m;
		_prevEnvUpper03 = 0m;
		_prevEnvLower07 = 0m;
		_prevEnvUpper07 = 0m;
		_prevEnvLower10 = 0m;
		_prevEnvUpper10 = 0m;

		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;

		_emaFastClose?.Reset();
		_emaFastOpen?.Reset();
		_emaSlow?.Reset();
	}

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

		_emaFastClose = new ExponentialMovingAverage { Length = 2 };
		_emaFastOpen = new ExponentialMovingAverage { Length = 2 };
		_emaSlow = new ExponentialMovingAverage { Length = 220 };

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

	private void ProcessCandle(ICandleMessage candle)
	{
	if (UseTradeHours && !IsWithinTradeHours(candle.OpenTime))
	return;

	if (candle.State != CandleStates.Finished)
	return;

	var fastCloseValue = _emaFastClose.Process(new DecimalIndicatorValue(_emaFastClose, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
	var fastOpenValue = _emaFastOpen.Process(new DecimalIndicatorValue(_emaFastOpen, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
	var slowValue = _emaSlow.Process(new DecimalIndicatorValue(_emaSlow, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

	var fastClose = fastCloseValue.GetValue<decimal>();
	var fastOpen = fastOpenValue.GetValue<decimal>();
	var slow = slowValue.GetValue<decimal>();

	var envLower03 = slow * (1m - Envelope003);
	var envUpper03 = slow * (1m + Envelope003);
	var envLower07 = slow * (1m - Envelope007);
	var envUpper07 = slow * (1m + Envelope007);
	var envLower10 = slow * (1m - Envelope010);
	var envUpper10 = slow * (1m + Envelope010);

	if (!_emaSlow.IsFormed || !_emaFastClose.IsFormed || !_emaFastOpen.IsFormed)
	{
	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	return;
	}

	if (!_hasPreviousValues)
	{
	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	_hasPreviousValues = true;
	return;
	}

	var buySignal =
	(fastClose > envLower10 && _prevFastClose <= _prevEnvLower10) ||
	(fastClose > envLower07 && _prevFastClose <= _prevEnvLower07) ||
	(fastClose < envLower03 && _prevFastClose < _prevEnvLower03) ||
	(fastClose > slow && _prevFastClose <= _prevSlow) ||
	(fastClose > envUpper03 && _prevFastClose <= _prevEnvUpper03) ||
	(fastClose > envUpper07 && _prevFastClose <= _prevEnvUpper07);

	var sellSignal =
	(fastOpen < envUpper10 && _prevFastOpen >= _prevEnvUpper10) ||
	(fastOpen < envUpper07 && _prevFastOpen >= _prevEnvUpper07) ||
	(fastOpen < envUpper03 && _prevFastOpen >= _prevEnvUpper03) ||
	(fastOpen < slow && _prevFastOpen >= _prevSlow) ||
	(fastOpen < envLower03 && _prevFastOpen >= _prevEnvLower03) ||
	(fastOpen < envLower07 && _prevFastOpen >= _prevEnvLower07);

	if (Position > 0)
	{
	ManageLongPosition(candle);
	}
	else if (Position < 0)
	{
	ManageShortPosition(candle);
	}

	if (Position == 0)
	{
	if (buySignal)
	{
	BuyMarket(OrderVolume);
	SetEntryState(true, candle.ClosePrice);
	}
	else if (sellSignal)
	{
	SellMarket(OrderVolume);
	SetEntryState(false, candle.ClosePrice);
	}
	}

	UpdatePreviousValues(fastClose, fastOpen, slow, envLower03, envUpper03, envLower07, envUpper07, envLower10, envUpper10);
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
	if (_entryPrice is null)
	return;

	var pip = GetPipSize();
	var trailingDistance = TrailingStopBuyPips * pip;
	var trailingStep = TrailingStepPips * pip;

	var profit = candle.ClosePrice - _entryPrice.Value;

	if (TrailingStopBuyPips > 0 && profit > trailingDistance + trailingStep)
	{
	var threshold = candle.ClosePrice - (trailingDistance + trailingStep);
	if (!_stopLossPrice.HasValue || _stopLossPrice.Value < threshold)
	_stopLossPrice = candle.ClosePrice - trailingDistance;
	}

	var exitVolume = Position;

	if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
	{
	SellMarket(exitVolume);
	ResetPositionState();
	return;
	}

	if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
	{
	SellMarket(exitVolume);
	ResetPositionState();
	}
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
	if (_entryPrice is null)
	return;

	var pip = GetPipSize();
	var trailingDistance = TrailingStopSellPips * pip;
	var trailingStep = TrailingStepPips * pip;

	var profit = _entryPrice.Value - candle.ClosePrice;

	if (TrailingStopSellPips > 0 && profit > trailingDistance + trailingStep)
	{
	var threshold = candle.ClosePrice + (trailingDistance + trailingStep);
	if (!_stopLossPrice.HasValue || _stopLossPrice.Value > threshold)
	_stopLossPrice = candle.ClosePrice + trailingDistance;
	}

	var exitVolume = -Position;

	if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
	{
	BuyMarket(exitVolume);
	ResetPositionState();
	return;
	}

	if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
	{
	BuyMarket(exitVolume);
	ResetPositionState();
	}
	}

	private void SetEntryState(bool isLong, decimal entryPrice)
	{
	_entryPrice = entryPrice;

	var pip = GetPipSize();

	_stopLossPrice = isLong && StopLossBuyPips > 0
	? entryPrice - StopLossBuyPips * pip
	: !isLong && StopLossSellPips > 0
	? entryPrice + StopLossSellPips * pip
	: null;

	_takeProfitPrice = isLong && TakeProfitBuyPips > 0
	? entryPrice + TakeProfitBuyPips * pip
	: !isLong && TakeProfitSellPips > 0
	? entryPrice - TakeProfitSellPips * pip
	: null;
	}

	private void ResetPositionState()
	{
	_entryPrice = null;
	_stopLossPrice = null;
	_takeProfitPrice = null;
	}

	private void UpdatePreviousValues(decimal fastClose, decimal fastOpen, decimal slow, decimal envLower03, decimal envUpper03, decimal envLower07, decimal envUpper07, decimal envLower10, decimal envUpper10)
	{
	_prevFastClose = fastClose;
	_prevFastOpen = fastOpen;
	_prevSlow = slow;
	_prevEnvLower03 = envLower03;
	_prevEnvUpper03 = envUpper03;
	_prevEnvLower07 = envLower07;
	_prevEnvUpper07 = envUpper07;
	_prevEnvLower10 = envLower10;
	_prevEnvUpper10 = envUpper10;
	}

	private bool IsWithinTradeHours(DateTimeOffset time)
	{
	var hour = time.Hour;

	if (FromHour == ToHour)
	return hour == FromHour;

	if (FromHour < ToHour)
	return hour >= FromHour && hour <= ToHour;

	return hour >= FromHour || hour <= ToHour;
	}

	private decimal GetPipSize()
	{
	var step = Security?.PriceStep ?? 0.0001m;

	if (Security?.Decimals is int decimals && (decimals == 3 || decimals == 5))
	return step * 10m;

	return step;
	}
}