Ver no GitHub

Estratégia SendClose

Visão geral

SendClose é uma estratégia de rompimento baseada em fractais que recria o comportamento do consultor especialista original do MT5. O algoritmo constrói continuamente linhas dinâmicas de suporte e resistência ligando pivôs fractais alternantes e reage no momento em que o preço revisita esses níveis projetados. O port do StockSharp mantém as mecânicas centrais intactas: as linhas de tendência são geradas a partir de sequências alternantes de fractais para cima/para baixo, os rompimentos acionam entradas de mercado, e linhas de deslocamento separadas são usadas para forçar a liquidação de posições.

Fluxo de detecção de fractais

  1. Janela de cinco velas – a estratégia mantém um buffer rolante das últimas cinco velas concluídas. Assim que a janela estiver cheia, avalia a vela do meio em relação aos dois vizinhos mais antigos e dois mais novos.
  2. Regra de fractal ascendente – a vela central forma um fractal ascendente quando sua máxima é maior do que as máximas das duas velas mais novas e estritamente maior do que as máximas das duas velas mais antigas. Isso corresponde à lógica iFractals do MT5 (>= no lado mais novo, > no lado mais antigo).
  3. Regra de fractal descendente – similarmente, a vela central é um fractal descendente se sua mínima for menor ou igual em comparação com as velas mais novas e estritamente menor do que as duas velas mais antigas.
  4. Fila de fractais – cada fractal recém-confirmado é inserido em uma fila FIFO de seis elementos ordenada do mais recente para o mais antigo. Esta fila é posteriormente escaneada para encontrar os padrões alternantes necessários.

Construção de linhas de tendência

  • Linha de venda – o algoritmo procura a sequência mais recente fractal ascendente → fractal descendente → fractal ascendente. A linha é traçada pelo primeiro e último fractal ascendente, conectando efetivamente dois topos de oscilação separados por um fundo de oscilação.
  • Linha de compra – simetricamente, procura uma cadeia fractal descendente → fractal ascendente → fractal descendente e conecta os fundos de oscilação circundantes.
  • Projeção – os pontos finais armazenados (tempo e preço) são usados para interpolar ou extrapolar o valor da linha para qualquer timestamp posterior. Quando o mercado alcança a projeção no fechamento da vela atual, uma decisão de negociação é tomada.
  • Linhas de fechamento – dois níveis auxiliares são calculados deslocando a linha de venda para cima e a linha de compra para baixo por LineOffsetSteps * PriceStep. Eles atuam como gatilhos de saída forçada assim como as linhas Close1/Close2 originais.

Lógica de trading

  • Condições de entrada
    • Vender quando o preço toca a linha de venda e não há exposição comprada conflitante. A exposição vendida existente pode ser aumentada até o limite MaxPositions ser atingido.
    • Comprar quando o preço toca a linha de compra e não há exposição vendida conflitante. A exposição comprada existente pode ser aumentada até o mesmo limite.
  • Condições de saída
    • O preço tocando qualquer linha de fechamento fecha imediatamente a posição aberta, emulando o comportamento do MT5 onde tocar Close1/Close2 emite uma saída completa.
    • Os sinais de entrada tentam achatar posições opostas antes de colocar a nova ordem, refletindo a adaptação de cobertura para neteo dentro do StockSharp.
  • Detecção de toque – a precisão de tick do MT5 é aproximada com dados de velas. Um nível é considerado "tocado" quando está entre a máxima e a mínima da vela.

Parâmetros

Nome Descrição
EnableSellLine Habilita ou desabilita ordens baseadas na linha fractal superior (de venda).
EnableBuyLine Habilita ou desabilita ordens baseadas na linha fractal inferior (de compra).
EnableCloseSellLine Ativa o nível Close1 que fecha posições quando o preço sobe acima da linha de venda mais o deslocamento.
EnableCloseBuyLine Ativa o nível Close2 que fecha posições quando o preço cai abaixo da linha de compra menos o deslocamento.
MaxPositions Número máximo de lotes que podem permanecer abertos em uma direção. Entradas adicionais além deste limite são ignoradas.
OrderVolume Volume de cada ordem de mercado. O valor deve corresponder ao tamanho do contrato do instrumento.
LineOffsetSteps Deslocamento, medido em passos de preço, usado ao calcular os níveis Close1/Close2. O padrão 15 replica o deslocamento 15*Point() do MT5.
CandleType Série de velas usada para análise. Escolha um período de tempo que corresponda ao gráfico que planeja negociar (ex., M15, H1).

Notas de implementação

  • A estratégia executa em velas concluídas para respeitar o EA original, que dependia de barras MT5 confirmadas antes de avaliar os fractais.
  • A igualdade em nível de tick com bid/ask é aproximada com intervalos de velas. Se maior precisão for necessária, alimentar dados de tick em vez de velas.
  • O parâmetro MaxPositions opera sobre a posição líquida do StockSharp. Portanto é adequado para contas de neteo; contas de cobertura ainda podem simular escalonamento aumentando MaxPositions.
  • As linhas de fechamento são avaliadas antes das entradas. Se tanto uma saída quanto uma entrada forem acionadas na mesma vela, a saída tem prioridade, evitando ordens conflitantes.

Diretrizes de uso

  1. Configure o símbolo e o período de tempo desejados no seu terminal StockSharp e certifique-se de que o instrumento forneça informações de PriceStep. A lógica de deslocamento depende disso.
  2. Ajuste CandleType para corresponder ao período de tempo que deseja analisar. O padrão é 30 minutos, que oferece um equilíbrio entre ruído e capacidade de resposta.
  3. Defina OrderVolume como o tamanho da posição que deseja enviar por operação. Para futuros, use contagens de contratos; para CFDs de câmbio, use tamanhos de lote.
  4. Ajuste LineOffsetSteps para se alinhar com a volatilidade do instrumento. Deslocamentos maiores requerem um movimento mais forte para acionar as saídas Close1/Close2.
  5. Monitore o número de lotes abertos ao aumentar MaxPositions. A estratégia não excederá este limite, mas ainda pode piramidear posições em mercados de tendência.

Diferenças da versão MT5

  • O StockSharp opera com posições líquidas, portanto o código achata a exposição oposta antes de abrir uma nova operação em vez de manter tickets de compra/venda simultâneos.
  • Os objetos de gráfico não são desenhados automaticamente. Se precisar de visualização no gráfico, conecte um módulo de gráfico e plote os valores de linha gerados manualmente.
  • A detecção de toque baseada em velas pode disparar ligeiramente mais tarde do que as verificações de tick do MT5, especialmente em mercados rápidos com velas amplas.

Gerenciamento de risco

A estratégia coloca ordens de mercado sem stop-losses integrados. Sempre complemente-a com controles de risco externos como stops de capital, filtros de horário de negociação ou supervisão manual. Faça backtesting extensivo no instrumento e período de tempo alvo antes de implantar ao vivo.

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>
/// SendClose strategy replicates fractal breakout lines with close-based exits.
/// This class recreates the MT5 SendClose expert using StockSharp high level API.
/// </summary>
public class SendCloseStrategy : Strategy
{
	private enum FractalTypes
	{
		Up,
		Down
	}

	private readonly struct FractalPoint
	{
		public FractalPoint(FractalTypes type, DateTimeOffset time, decimal price)
		{
			Type = type;
			Time = time;
			Price = price;
		}

		public FractalTypes Type { get; }
		public DateTimeOffset Time { get; }
		public decimal Price { get; }
	}

	private readonly struct FractalLine
	{
		public FractalLine(FractalPoint recent, FractalPoint older)
		{
			if (recent.Time < older.Time)
			{
				Recent = older;
				Older = recent;
			}
			else
			{
				Recent = recent;
				Older = older;
			}
		}

		public FractalPoint Recent { get; }
		public FractalPoint Older { get; }

		public decimal GetPrice(DateTimeOffset time)
		{
			var totalSeconds = (decimal)(Recent.Time - Older.Time).TotalSeconds;
			if (totalSeconds == 0m)
				return Recent.Price;

			var offsetSeconds = (decimal)(time - Older.Time).TotalSeconds;
			return Older.Price + (Recent.Price - Older.Price) * (offsetSeconds / totalSeconds);
		}
	}

	private readonly StrategyParam<bool> _enableSellLine;
	private readonly StrategyParam<bool> _enableBuyLine;
	private readonly StrategyParam<bool> _enableCloseSellLine;
	private readonly StrategyParam<bool> _enableCloseBuyLine;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _lineOffsetSteps;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _h0;
	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;

	private decimal _l0;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;

	private DateTimeOffset _t0;
	private DateTimeOffset _t1;
	private DateTimeOffset _t2;
	private DateTimeOffset _t3;
	private DateTimeOffset _t4;

	private int _bufferCount;

	private FractalPoint? _fractal0;
	private FractalPoint? _fractal1;
	private FractalPoint? _fractal2;
	private FractalPoint? _fractal3;
	private FractalPoint? _fractal4;
	private FractalPoint? _fractal5;

	private FractalLine? _sellLine;
	private FractalLine? _buyLine;

	/// <summary>
	/// Enable sell breakout line.
	/// </summary>
	public bool EnableSellLine
	{
		get => _enableSellLine.Value;
		set => _enableSellLine.Value = value;
	}

	/// <summary>
	/// Enable buy breakout line.
	/// </summary>
	public bool EnableBuyLine
	{
		get => _enableBuyLine.Value;
		set => _enableBuyLine.Value = value;
	}

	/// <summary>
	/// Enable upper close line (based on sell trend line).
	/// </summary>
	public bool EnableCloseSellLine
	{
		get => _enableCloseSellLine.Value;
		set => _enableCloseSellLine.Value = value;
	}

	/// <summary>
	/// Enable lower close line (based on buy trend line).
	/// </summary>
	public bool EnableCloseBuyLine
	{
		get => _enableCloseBuyLine.Value;
		set => _enableCloseBuyLine.Value = value;
	}

	/// <summary>
	/// Maximum number of lots that can remain open.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Order volume per entry.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Offset in price steps for close lines.
	/// </summary>
	public int LineOffsetSteps
	{
		get => _lineOffsetSteps.Value;
		set => _lineOffsetSteps.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="SendCloseStrategy"/>.
	/// </summary>
	public SendCloseStrategy()
	{
		_enableSellLine = Param(nameof(EnableSellLine), true)
			.SetDisplay("Sell Line", "Enable sell fractal breakout line", "General");

		_enableBuyLine = Param(nameof(EnableBuyLine), true)
			.SetDisplay("Buy Line", "Enable buy fractal breakout line", "General");

		_enableCloseSellLine = Param(nameof(EnableCloseSellLine), true)
			.SetDisplay("Close Line 1", "Enable closing line above sell trend", "General");

		_enableCloseBuyLine = Param(nameof(EnableCloseBuyLine), true)
			.SetDisplay("Close Line 2", "Enable closing line below buy trend", "General");

		_maxPositions = Param(nameof(MaxPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of simultaneous lots", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.10m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per signal", "Risk");

		_lineOffsetSteps = Param(nameof(LineOffsetSteps), 60)
			.SetGreaterThanZero()
			.SetDisplay("Offset Steps", "Offset in price steps for close levels", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

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

		// Clear buffers that hold recent highs, lows, and times.
		_h0 = _h1 = _h2 = _h3 = _h4 = 0m;
		_l0 = _l1 = _l2 = _l3 = _l4 = 0m;
		_t0 = _t1 = _t2 = _t3 = _t4 = default;
		_bufferCount = 0;

		// Reset stored fractal points and active lines.
		_fractal0 = _fractal1 = _fractal2 = _fractal3 = _fractal4 = _fractal5 = null;
		_sellLine = null;
		_buyLine = null;
	}

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

		// Subscribe to candle data and process each completed candle.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

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

		// Update internal buffers and detect new fractal points.
		UpdateBuffers(candle);
		UpdateFractalLines();

		// Ensure trading is allowed before evaluating signals.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var offset = GetOffset();
		var shouldClose = false;

		// Check closing logic derived from the upper fractal trend line.
		if (EnableCloseSellLine && _sellLine is { } sellLine)
		{
			var closePrice = GetLinePrice(sellLine, candle.CloseTime) + offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Check closing logic derived from the lower fractal trend line.
		if (EnableCloseBuyLine && _buyLine is { } buyLine)
		{
			var closePrice = GetLinePrice(buyLine, candle.CloseTime) - offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Close any open position if price reached one of the close lines.
		if (shouldClose && Position != 0m)
		{
			if (Position > 0) SellMarket(); else BuyMarket();
			return;
		}

		// Entry logic for sell breakout.
		if (EnableSellLine && _sellLine is { } sellEntryLine)
		{
			var sellPrice = GetLinePrice(sellEntryLine, candle.CloseTime);
			if (IsTouched(sellPrice, candle))
			{
				if (Position > 0m)
				{
					// Flatten long positions before attempting to go short.
					SellMarket();
				}
				else if (CanIncreaseShort())
				{
					SellMarket(OrderVolume);
				}
			}
		}

		// Entry logic for buy breakout.
		if (EnableBuyLine && _buyLine is { } buyEntryLine)
		{
			var buyPrice = GetLinePrice(buyEntryLine, candle.CloseTime);
			if (IsTouched(buyPrice, candle))
			{
				if (Position < 0m)
				{
					// Flatten short positions before attempting to go long.
					BuyMarket();
				}
				else if (CanIncreaseLong())
				{
					BuyMarket(OrderVolume);
				}
			}
		}
	}

	private void UpdateBuffers(ICandleMessage candle)
	{
		// Shift buffers to keep the latest five candles for fractal detection.
		_h4 = _h3;
		_h3 = _h2;
		_h2 = _h1;
		_h1 = _h0;
		_h0 = candle.HighPrice;

		_l4 = _l3;
		_l3 = _l2;
		_l2 = _l1;
		_l1 = _l0;
		_l0 = candle.LowPrice;

		_t4 = _t3;
		_t3 = _t2;
		_t2 = _t1;
		_t1 = _t0;
		_t0 = candle.OpenTime;

		if (_bufferCount < 5)
		{
			_bufferCount++;
			return;
		}

		// Identify new fractal points once enough candles are available.
		if (IsUpFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Up, _t2, _h2));

		if (IsDownFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Down, _t2, _l2));
	}

	private void UpdateFractalLines()
	{
		// Build the sell line using the most recent up-down-up pattern.
		if (TryBuildLine(FractalTypes.Up, out var sellLine))
			_sellLine = sellLine;

		// Build the buy line using the most recent down-up-down pattern.
		if (TryBuildLine(FractalTypes.Down, out var buyLine))
			_buyLine = buyLine;
	}

	private bool IsUpFractal()
	{
		return _h2 >= _h3 && _h2 > _h4 && _h2 >= _h1 && _h2 > _h0;
	}

	private bool IsDownFractal()
	{
		return _l2 <= _l3 && _l2 < _l4 && _l2 <= _l1 && _l2 < _l0;
	}

	private void RegisterFractal(FractalPoint point)
	{
		// Skip duplicates that can appear on flat sequences.
		if (_fractal0 is { } latest && latest.Time == point.Time && latest.Type == point.Type)
			return;

		_fractal5 = _fractal4;
		_fractal4 = _fractal3;
		_fractal3 = _fractal2;
		_fractal2 = _fractal1;
		_fractal1 = _fractal0;
		_fractal0 = point;
	}

	private bool TryBuildLine(FractalTypes target, out FractalLine line)
	{
		line = default;
		FractalPoint? latest = null;
		FractalPoint? middle = null;
		FractalPoint? oldest = null;

		foreach (var item in EnumerateFractals())
		{
			if (item is not { } point)
				continue;

			if (latest is null)
			{
				if (point.Type == target)
					latest = point;
				continue;
			}

			if (middle is null)
			{
				if (point.Type != target)
					middle = point;
				continue;
			}

			if (point.Type == target)
			{
				oldest = point;
				break;
			}
		}

		if (latest is not { } latestPoint || middle is null || oldest is not { } oldestPoint)
			return false;

		if (latestPoint.Time == oldestPoint.Time)
			return false;

		line = new FractalLine(latestPoint, oldestPoint);
		return true;
	}

	private IEnumerable<FractalPoint?> EnumerateFractals()
	{
		yield return _fractal0;
		yield return _fractal1;
		yield return _fractal2;
		yield return _fractal3;
		yield return _fractal4;
		yield return _fractal5;
	}

	private bool CanIncreaseShort()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private bool CanIncreaseLong()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private decimal GetOffset()
	{
		var step = Security?.PriceStep ?? 1m;
		return step * LineOffsetSteps;
	}

	private static bool IsTouched(decimal price, ICandleMessage candle)
	{
		return price <= candle.HighPrice && price >= candle.LowPrice;
	}

	private static decimal GetLinePrice(FractalLine line, DateTimeOffset time)
	{
		return line.GetPrice(time);
	}
}