Ver no GitHub

Fractals Distância Mínima

Visão geral

Fractals Distância Mínima replica o consultor especialista do MetaTrader "Fractals minimum distance" usando a API de estratégia de alto nível do StockSharp. O sistema varre a série de candles configurada em busca de padrões fractais de cinco barras no estilo Bill Williams. Cada vez que um novo fractal confirmado aparece no deslocamento da barra de sinal especificada, a estratégia mede a distância entre os fractais de subida e descida mais recentes. Uma ordem de mercado só é permitida quando essa distância excede o limiar necessário expresso em pips.

A conversão mantém o comportamento original de fechar qualquer exposição oposta imediatamente antes de reverter. Ao contrário da versão MQL, o tamanho da posição é retirado da propriedade Volume da estratégia em vez de realizar cálculos de risco baseados na conta. Nenhuma ordem de stop-loss ou take-profit é enviada, correspondendo ao expert de origem.

Lógica de sinais

  1. Assinar o tipo de candle definido por CandleType e construir buffers deslizantes de máximas e mínimas que sempre contenham a barra localizada SignalBar candles no passado junto com dois vizinhos em cada lado.
  2. Detectar um fractal superior quando a máxima da barra central é estritamente maior que as máximas das duas velas anteriores e das duas seguintes. Detectar um fractal inferior de forma análoga para as mínimas.
  3. Converter o parâmetro DistancePips em uma distância de preço usando o PriceStep do símbolo. Símbolos com três ou cinco dígitos decimais são automaticamente ajustados para tratar cotações de 0.001/0.00001 como um pip.
  4. Quando um fractal superior é confirmado:
    • Armazenar o novo nível superior e fechar as posições compradas existentes.
    • Se tanto o último fractal superior quanto o inferior forem conhecidos e sua diferença absoluta for pelo menos o limiar de distância, enviar uma ordem de venda a mercado usando Volume.
  5. Quando um fractal inferior é confirmado:
    • Armazenar o novo nível inferior e fechar as posições vendidas existentes.
    • Se a condição de distância for satisfeita, enviar uma ordem de compra a mercado usando Volume.

As operações são colocadas apenas após o fechamento do candle que finaliza o fractal, garantindo que barras inacabadas nunca acionem entradas. A estratégia baseia-se em IsFormedAndOnlineAndAllowTrading() para evitar colocar ordens antes que o ambiente esteja pronto.

Parâmetros

Nome Descrição Notas
DistancePips Espaçamento mínimo entre os últimos fractais de subida e descida medido em pips. Convertido internamente para unidades de preço usando o tamanho do tick do instrumento.
SignalBar Número de barras completamente fechadas que devem passar após a barra que hospeda o fractal. O valor efetivo mínimo é 2, correspondendo à confirmação de duas barras usada pelos fractais de Bill Williams.
CandleType Série de dados que alimenta os cálculos. O padrão é o período de um minuto; altere para trabalhar com outras resoluções.
Volume Propriedade padrão da estratégia StockSharp que define o tamanho da operação. Substitui o dimensionamento baseado em risco original do expert MetaTrader.

Gestão de posições e diferenças em relação ao MQL

  • As posições são sempre zeradas antes de reverter a direção, exatamente como o helper ClosePositions de origem fazia.
  • O expert original chamava RefreshRates() e realizava configurações explícitas de slippage. Esses aspectos são delegados à infraestrutura do StockSharp neste port.
  • Ordens de stop-loss e take-profit não faziam parte da lógica MQL e permanecem ausentes aqui.
  • DistancePips usa precisão inteira como a entrada ushort, enquanto SignalBar espelha a entrada uchar do MQL.
  • Como o StockSharp trabalha com posições líquidas, abrir uma ordem na direção oposta vira automaticamente a exposição, correspondendo ao comportamento de netting do MetaTrader.

Dicas de uso

  • Comece com o mesmo deslocamento de barra de sinal (SignalBar = 3) do código original e calibre o limiar de distância de acordo com a volatilidade do instrumento.
  • Aumente SignalBar para aguardar mais candles após o aparecimento de um fractal, o que pode filtrar oscilações rápidas.
  • Combine com gerenciamento de risco externo, como o helper integrado StartProtection(), se um stop de proteção for necessário.
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>
/// Fractals minimum distance breakout strategy converted from MetaTrader.
/// </summary>
public class FractalsMinimumDistanceStrategy : Strategy
{
	private readonly StrategyParam<int> _distancePips;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _prevUpperFractal;
	private decimal? _prevLowerFractal;
	private decimal[] _highs = Array.Empty<decimal>();
	private decimal[] _lows = Array.Empty<decimal>();
	private int _bufferCount;
	private int _windowSize;
	private int _signalOffset;
	private decimal _pipSize;

	public int DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

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

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

	public FractalsMinimumDistanceStrategy()
	{
		_distancePips = Param(nameof(DistancePips), 15)
			.SetDisplay("Distance (pips)", "Minimum allowed gap between the last two fractals", "Risk")
			;

		_signalBar = Param(nameof(SignalBar), 3)
			.SetDisplay("Signal bar offset", "How many closed bars ago the fractal must appear", "General")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle type", "Primary candle series used for signals", "Data")
			;
	}

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

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

		_prevUpperFractal = null;
		_prevLowerFractal = null;
		_highs = Array.Empty<decimal>();
		_lows = Array.Empty<decimal>();
		_bufferCount = 0;
		_windowSize = 0;
		_signalOffset = 0;
		_pipSize = 0m;
	}

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

		InitializeBuffers();
		_pipSize = CalculatePipSize();

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

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

	private void InitializeBuffers()
	{
		_signalOffset = Math.Max(2, SignalBar);
		_windowSize = Math.Max(_signalOffset + 3, 5);
		_highs = new decimal[_windowSize];
		_lows = new decimal[_windowSize];
		_bufferCount = 0;
	}

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

		// Shift the rolling buffers to keep the configured number of historical bars.
		for (var i = 0; i < _windowSize - 1; i++)
		{
			_highs[i] = _highs[i + 1];
			_lows[i] = _lows[i + 1];
		}

		// Append the latest candle extremes.
		_highs[_windowSize - 1] = candle.HighPrice;
		_lows[_windowSize - 1] = candle.LowPrice;

		if (_bufferCount < _windowSize)
			_bufferCount++;

		if (_bufferCount < _windowSize)
			return;

		var centerIndex = _windowSize - 1 - _signalOffset;
		if (centerIndex < 2 || centerIndex > _windowSize - 3)
			return;

		var high = _highs[centerIndex];
		var low = _lows[centerIndex];

		var isUpperFractal =
			high > _highs[centerIndex - 1] &&
			high > _highs[centerIndex - 2] &&
			high > _highs[centerIndex + 1] &&
			high > _highs[centerIndex + 2];

		var isLowerFractal =
			low < _lows[centerIndex - 1] &&
			low < _lows[centerIndex - 2] &&
			low < _lows[centerIndex + 1] &&
			low < _lows[centerIndex + 2];

		var distanceThreshold = DistancePips * _pipSize;

		if (isUpperFractal)
		{
			_prevUpperFractal = high;

			// Close existing long exposure before reversing.
			if (Position > 0)
				SellMarket();

			// Enter a short position if the fractals are far enough apart.
			if (ShouldOpenTrade(distanceThreshold))
				SellMarket();
		}

		if (isLowerFractal)
		{
			_prevLowerFractal = low;

			// Close existing short exposure before reversing.
			if (Position < 0)
				BuyMarket();

			// Enter a long position if the fractals are far enough apart.
			if (ShouldOpenTrade(distanceThreshold))
				BuyMarket();
		}
	}

	private bool ShouldOpenTrade(decimal distanceThreshold)
	{
		if (Volume <= 0)
			return false;

		if (_prevUpperFractal is not decimal upper || _prevLowerFractal is not decimal lower)
			return false;

		var threshold = Math.Abs(distanceThreshold);
		return Math.Abs(upper - lower) >= threshold;
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep;

		if (priceStep is not decimal step || step <= 0m)
			return 1m;

		var decimals = GetDecimalPlaces(step);

		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}
}