Ver no GitHub

Estratégia de Trading Noturno em Range Plano

A Estratégia de Trading Noturno em Range Plano reproduz o assessor especialista clássico MQL5 que procura ranges noturnos estreitos em velas H1 do EURUSD. Foca na hora que rodeia a mudança do dia de trading, aguardando que o preço volte às bordas de um canal de consolidação estreito e apostando na continuação do rompimento. A versão StockSharp mantém as idéias originais intactas enquanto depende de assinaturas de velas de alto nível, vínculos de indicadores e objetos de parâmetros para melhor configurabilidade.

Visão geral

  • Mercado e timeframe: Projetado para EURUSD no timeframe H1, mas qualquer instrumento com um passo de preço claramente definido pode ser usado.
  • Janela de sessão: As entradas são permitidas somente durante uma janela de duas horas que começa no OpenHour configurado e termina em OpenHour + 1 (hora da bolsa).
  • Filtro de range: O intervalo alto-baixo das últimas três velas completadas deve permanecer entre DiffMinPips e DiffMaxPips (convertidos em unidades de preço).
  • Viés: Somente comprado ou somente vendido dependendo de onde o último fechamento se situa dentro do range que se qualifica.

Lógica de trading

  1. Calcular os limites do range

    • A estratégia vincula aos indicadores incorporados Highest e Lowest (comprimento = 3) para obter o máximo mais alto e o mínimo mais baixo ao longo das últimas três velas.
    • A distância entre essas fronteiras é o range de trabalho usado para todas as verificações subsequentes.
  2. Condições de entrada

    • Configuração comprada: Durante a sessão ativa, se o preço de fechamento está acima do mínimo do range mas ainda dentro do quarto inferior (lowest + range/4), a estratégia abre uma posição comprada com um stop protetor inicial em lowest - range/3.
    • Configuração vendida: Simetricamente, se o fechamento está abaixo do máximo do range mas ainda dentro do quarto superior (highest - range/4), uma posição vendida é aberta com um stop em highest + range/3.
  3. Gestão de saídas

    • Stop-Loss: Os stops são simulados internamente e ativam uma saída de mercado quando a próxima vela viola o limite armazenado.
    • Take-Profit: Quando TakeProfitPips > 0, um nível adicional fixo de take-profit (em pips) é criado relativo ao preço de entrada.
    • Trailing Stop: Quando tanto TrailingStopPips quanto TrailingStepPips são positivos, o stop é ajustado somente após o preço avançar TrailingStop + TrailingStep pips a favor da operação. Ajustes subsequentes requerem um progresso adicional de TrailingStepPips para refletir o comportamento de trailing escalonado original.
  4. Controle de re-entrada

    • O algoritmo sempre aguarda a posição atual estar completamente fechada antes de buscar um novo sinal, mantendo o sistema plano entre operações como no assessor especialista de referência.

Parâmetros

Parâmetro Descrição Padrão
CandleType Série de velas para assinar (padrão H1). Velas de 1 hora
TakeProfitPips Distância de take-profit opcional em pips. 50
TrailingStopPips Distância entre preço e trailing stop em pips (0 desativa trailing). 15
TrailingStepPips Pips adicionais necessários antes de cada atualização do trailing stop. 5
DiffMinPips Range mínimo permitido de três velas (pips). 18
DiffMaxPips Range máximo permitido de três velas (pips). 28
OpenHour Hora de início da sessão em hora da bolsa (entradas permitidas até OpenHour + 1). 0

Indicadores

  • Highest(Length = 3) para monitorar a parte superior do range recente.
  • Lowest(Length = 3) para monitorar a parte inferior do range recente.

Notas de implementação

  • A conversão de pips se adapta automaticamente a instrumentos com 3 ou 5 casas decimais multiplicando o passo de preço relatado por 10, exatamente como a implementação original do MQ5.
  • Como o StockSharp opera em velas completadas neste exemplo, as condições de entrada intra-vela são aproximadas usando o preço de fechamento. Isso mantém a lógica determinista enquanto permanece fiel ao intento do código fonte.
  • Todos os parâmetros de risco são expostos por meio de objetos StrategyParam<T>, tornando-os visíveis na UI e prontos para otimização ou experimentos em lote.
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>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

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

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

		if (priceStep <= 0m)
			priceStep = 0.0001m;

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_highest, _lowest, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}