Ver no GitHub

Estratégia de Cruzamento Percentual

A estratégia replica o comportamento do expert original do MetaTrader Exp_PercentageCrossover. Ela opera na direção do indicador Percentage Crossover, que traça uma linha de preço trailing que só pode se mover dentro de uma banda percentual fixa em torno do fechamento atual. A inclinação desta linha define o estado do mercado e desencadeia negociações.

Conceito

  1. Em cada vela completada o indicador mantém o valor anterior da linha.
  2. Uma atualização de alta ocorre quando o fechamento empurra a linha trailing acima de seu valor anterior em pelo menos percent por cento do preço.
  3. Uma atualização de baixa ocorre quando o fechamento puxa a linha trailing abaixo de seu valor anterior pelo mesmo percentual.
  4. Se o fechamento permanecer dentro da banda, a linha permanece plana e retém sua última cor.

A cor da linha é interpretada da mesma forma que no MetaTrader:

  • Índice de cor 0 (azul/violeta) – a linha está subindo (contexto de alta).
  • Índice de cor 1 (laranja) – a linha está caindo (contexto de baixa).

Regras de negociação

Entradas compradas

  • Habilitadas apenas quando BuyPosOpen = true.
  • Avalia a barra selecionada por SignalBar (1 significa a última barra fechada).
  • Abre uma posição comprada quando essa barra muda da cor 1 para a cor 0.

Entradas vendidas

  • Habilitadas apenas quando SellPosOpen = true.
  • Avalia a mesma barra SignalBar.
  • Abre uma posição vendida quando a barra muda da cor 0 para a cor 1.

Gestão de posições

  • Se BuyPosClose = true, qualquer posição comprada aberta é fechada sempre que a barra atual (após aplicar o deslocamento SignalBar) for de cor 1.
  • Se SellPosClose = true, qualquer posição vendida aberta é fechada sempre que essa barra for de cor 0.
  • Quando UseTimeFilter = true e o horário atual estiver fora da janela de negociação configurada, a estratégia sai imediatamente da posição ativa e ignora novos sinais até que o mercado entre novamente na janela.
  • As ordens são enviadas com BuyMarket() e SellMarket(). A quantidade real vem da propriedade Volume da estratégia.

Parâmetros

Parâmetro Descrição Padrão
Percent Banda percentual para a linha trailing. Valores mais altos fazem a linha reagir mais lentamente. 1
SignalBar Qual barra fechada é analisada (1 = última fechada). Deve permanecer positivo. 1
BuyPosOpen / SellPosOpen Habilitar entradas compradas ou vendidas respectivamente. true
BuyPosClose / SellPosClose Habilitar a lógica de fechamento para posições compradas ou vendidas. true
UseTimeFilter Ativar a janela de negociação. true
StartHour / StartMinute Hora e minuto que abrem a janela de negociação quando o filtro está ativo. 0 / 0
EndHour / EndMinute Hora e minuto que fecham a janela de negociação. 23 / 59
CandleType Período das velas usadas para o indicador e os sinais. 4h

Notas

  • O filtro de tempo segue rigorosamente o Expert Advisor original. Quando a hora de início é maior que a hora de fim, a lógica cria uma janela noturna, mas ainda exige que os minutos sejam maiores ou iguais a StartMinute para que a sessão se torne ativa.
  • SignalBar é avaliado apenas em velas finalizadas. Defina-o como 1 para espelhar a configuração padrão do MetaTrader.
  • A estratégia não impõe níveis de stop-loss ou take-profit. O controle de risco deve ser gerenciado externamente ou ajustando o percentual e a janela de negociaçã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>
/// Strategy based on the Percentage Crossover indicator.
/// </summary>
public class PercentageCrossoverStrategy : Strategy
{
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _endMinute;
	private readonly StrategyParam<decimal> _percent;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;

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

	private decimal? _previousMiddle;
	private int? _lastColor;

	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	public int EndMinute
	{
		get => _endMinute.Value;
		set => _endMinute.Value = value;
	}

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

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

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

	public PercentageCrossoverStrategy()
	{
		_buyPosOpen = Param(nameof(BuyPosOpen), true)
			.SetDisplay("Enable Buy Entries", "Allow opening long positions", "General");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
			.SetDisplay("Enable Sell Entries", "Allow opening short positions", "General");

		_buyPosClose = Param(nameof(BuyPosClose), true)
			.SetDisplay("Enable Buy Exits", "Allow closing long positions", "General");

		_sellPosClose = Param(nameof(SellPosClose), true)
			.SetDisplay("Enable Sell Exits", "Allow closing short positions", "General");

		_useTimeFilter = Param(nameof(UseTimeFilter), true)
			.SetDisplay("Use Time Filter", "Restrict trading to specific hours", "Time Filter");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Trading window start hour", "Time Filter");

		_startMinute = Param(nameof(StartMinute), 0)
			.SetDisplay("Start Minute", "Trading window start minute", "Time Filter");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Trading window end hour", "Time Filter");

		_endMinute = Param(nameof(EndMinute), 59)
			.SetDisplay("End Minute", "Trading window end minute", "Time Filter");

		_percent = Param(nameof(Percent), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Percent", "Percentage offset for the indicator", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Bar", "Closed bars to look back for the signal", "Indicator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for signal candles", "Data");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;
	}

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

		_colorHistory.Clear();
		_previousMiddle = null;
		_lastColor = null;

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

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

		var close = candle.ClosePrice;
		var percentFactor = Percent / 100m;

		if (_previousMiddle is null)
		{
			_previousMiddle = close;
			_lastColor = 0;
			_colorHistory.Clear();
			_colorHistory.Add(0);
			return;
		}

		var previousMiddle = _previousMiddle.Value;
		var lowerBoundary = close * (1 - percentFactor);
		var upperBoundary = close * (1 + percentFactor);

		var middle = previousMiddle;

		if (lowerBoundary > previousMiddle)
			middle = lowerBoundary;
		else if (upperBoundary < previousMiddle)
			middle = upperBoundary;

		var color = _lastColor ?? 0;

		if (middle > previousMiddle)
			color = 0;
		else if (middle < previousMiddle)
			color = 1;

		_previousMiddle = middle;
		_lastColor = color;

		_colorHistory.Add(color);
		var maxSize = Math.Max(SignalBar + 2, 4);
		while (_colorHistory.Count > maxSize)
		{
			try { _colorHistory.RemoveAt(0); }
			catch { break; }
		}

		var currentIndex = _colorHistory.Count - SignalBar;
		if (currentIndex <= 0)
			return;

		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var currentColor = _colorHistory[currentIndex];
		var previousColor = _colorHistory[previousIndex];

		var buyOpen = BuyPosOpen && currentColor == 0 && previousColor == 1;
		var sellOpen = SellPosOpen && currentColor == 1 && previousColor == 0;
		var buyClose = BuyPosClose && currentColor == 1;
		var sellClose = SellPosClose && currentColor == 0;

		var inTradingWindow = !UseTimeFilter || IsTradingTime(candle.CloseTime);

		if (UseTimeFilter && !inTradingWindow)
		{
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();

			return;
		}

		if (buyClose && Position > 0)
			SellMarket();

		if (sellClose && Position < 0)
			BuyMarket();

		if (!inTradingWindow)
			return;

		if (buyOpen && Position <= 0)
			BuyMarket();
		else if (sellOpen && Position >= 0)
			SellMarket();
	}

	private bool IsTradingTime(DateTimeOffset time)
	{
		var hour = time.Hour;
		var minute = time.Minute;

		if (StartHour < EndHour)
		{
			if (hour == StartHour && minute >= StartMinute)
				return true;

			if (hour > StartHour && hour < EndHour)
				return true;

			if (hour > StartHour && hour == EndHour && minute < EndMinute)
				return true;

			return false;
		}

		if (StartHour == EndHour)
		{
			return hour == StartHour && minute >= StartMinute && minute < EndMinute;
		}

		if (hour >= StartHour && minute >= StartMinute)
			return true;

		if (hour < EndHour)
			return true;

		if (hour == EndHour && minute < EndMinute)
			return true;

		return false;
	}
}