View on GitHub

Fractals Minimum Distance

Overview

Fractals Minimum Distance replicates the MetaTrader expert advisor "Fractals minimum distance" using StockSharp's high level strategy API. The system scans the configured candle series for Bill Williams style five-bar fractal patterns. Each time a new confirmed fractal appears at the specified signal bar offset, the strategy measures the gap between the most recent up and down fractals. A market order is allowed only when this distance exceeds the required threshold expressed in pips.

The conversion keeps the original behaviour of closing any opposite exposure immediately before reversing. Unlike the MQL version, the position size is taken from the strategy's Volume property instead of performing account-based risk calculations. No stop-loss or take-profit orders are submitted, matching the source expert.

Signal Logic

  1. Subscribe to the candle type defined by CandleType and build rolling buffers of highs and lows that always contain the bar located SignalBar candles in the past together with two neighbours on each side.
  2. Detect an upper fractal when the high of the centre bar is strictly greater than the highs of the two preceding and the two following candles. Detect a lower fractal analogously for lows.
  3. Convert the DistancePips parameter to a price distance using the symbol's PriceStep. Symbols with three or five decimal digits are automatically adjusted to treat 0.001/0.00001 quotes as one pip.
  4. When an upper fractal is confirmed:
    • Store the new upper level and close existing long positions.
    • If both the latest upper and lower fractals are known and their absolute difference is at least the distance threshold, submit a market sell order using Volume.
  5. When a lower fractal is confirmed:
    • Store the new lower level and close existing short positions.
    • If the distance condition is satisfied, submit a market buy order using Volume.

Trades are placed only after the candle that finalises the fractal is closed, ensuring that unfinished bars never trigger entries. The strategy relies on IsFormedAndOnlineAndAllowTrading() to avoid placing orders before the environment is ready.

Parameters

Name Description Notes
DistancePips Minimum spacing between the last up and down fractals measured in pips. Converted internally to price units using the instrument's tick size.
SignalBar Number of fully closed bars that must pass after the bar hosting the fractal. Minimum effective value is 2, matching the two-bar confirmation used by Bill Williams fractals.
CandleType Data series that feeds the calculations. Default is one-minute time frame; change to work on other resolutions.
Volume Standard StockSharp strategy property defining the trade size. Replace the original risk-based sizing from the MetaTrader expert.

Position Management and Differences vs. MQL

  • Positions are always flattened before reversing direction, exactly as the source ClosePositions helper did.
  • The original expert called RefreshRates() and performed explicit slippage settings. Those aspects are delegated to the StockSharp infrastructure in this port.
  • Stop-loss and take-profit orders were not part of the MQL logic and remain absent here.
  • DistancePips uses integer precision like the ushort input, while SignalBar mirrors the MQL uchar input.
  • Because StockSharp works with net positions, opening an order in the opposite direction automatically flips the exposure, matching the MetaTrader netting behaviour.

Usage Tips

  • Start with the same signal bar offset (SignalBar = 3) from the original code and calibrate the distance threshold according to the instrument's volatility.
  • Increase SignalBar to wait for more candles after a fractal appears, which can filter out rapid oscillations.
  • Combine with external risk management such as the built-in StartProtection() helper if a protective stop is required.
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;
	}
}