Auf GitHub ansehen

Fractals at Close Prices Strategy

Overview

This strategy is a StockSharp port of the MetaTrader 5 expert advisor "Fractals at Close prices" by Vladimir Karputov. It analyses five consecutive closing prices to detect Bill Williams style fractals that are built strictly on closes instead of highs or lows. The most recent two bullish and bearish fractals are compared to determine the active trend. When the latest bullish fractal prints above the previous one, the strategy opens a long position. When the latest bearish fractal forms below the previous one, it opens a short position. Opposite positions are always closed before entering a new trade, so the strategy stays in at most one direction at a time.

Trades are only allowed between the configurable start and end hour. If the current hour falls outside this window, all open positions are closed immediately, mirroring the behaviour of the original EA. The time filter supports intraday windows (start < end), overnight sessions that cross midnight (start > end) and full-day trading (start == end).

Indicator logic

  • Every finished candle is appended to a rolling five-element queue of closing prices.
  • Once five values are available, the middle close (two candles back) is evaluated:
    • A bullish fractal is registered if the middle close is strictly greater than the two older closes and greater-or-equal to the two newer closes.
    • A bearish fractal is registered if the middle close is strictly lower than the two older closes and lower-or-equal to the two newer closes.
  • The latest and previous bullish fractals, as well as the latest and previous bearish fractals, are stored for later comparison.
  • A bullish trend is detected when the latest bullish fractal is higher than the previous one. A bearish trend is detected when the latest bearish fractal is lower than the previous one.

Trading rules

  1. Long entries
    • Close any active short position at market.
    • If no long position is open, buy OrderVolume at market on the close that confirmed the bullish fractal sequence.
  2. Short entries
    • Close any active long position at market.
    • If no short position is open, sell OrderVolume at market when a bearish fractal sequence is confirmed.
  3. Session control
    • Before applying signals, the strategy verifies that candle.OpenTime.Hour is inside the trading window. If not, CloseAllPositions is called and the bar is ignored.

Risk management

  • Stop-loss and take-profit distances are expressed in pips. The implementation reproduces the MT5 approach: the symbol point is multiplied by ten when the instrument has 3 or 5 decimals. The resulting pip value is then multiplied by the configured distances.
  • When entering a position, the initial stop-loss and take-profit levels are stored internally. Because StockSharp does not automatically manage MT5 style protective orders, the strategy monitors finished candles and exits at market when their price range touches the stored level.
  • Trailing stops follow the original EA rules. A new stop is calculated as close ± TrailingStop once the profit exceeds TrailingStop + TrailingStep. The trailing stop is only advanced if the move from the previous stop is at least TrailingStep.
  • When trading hours end, all positions are closed regardless of trailing status. This replicates the EA calling CloseAllPositions outside the allowed session.

Parameters

Name Description Default
OrderVolume Volume used for each market order. 0.1
StartHour Hour (0-23) when trading becomes active. If equal to EndHour, the strategy runs all day. 10
EndHour Hour (0-23) when trading stops accepting new signals. 22
StopLossPips Stop-loss distance expressed in pips. 0 disables the stop. 30
TakeProfitPips Take-profit distance expressed in pips. 0 disables the take. 50
TrailingStopPips Base trailing stop distance in pips. 0 disables trailing. 15
TrailingStepPips Additional profit (in pips) required before the trailing stop is advanced. 5
CandleType Candle data type subscribed by the strategy. The default is 1-hour time-frame candles. 1 hour TimeFrame

Implementation notes

  • The strategy uses SubscribeCandles with the high-level API and does not register indicators manually, following the project guidelines.
  • Protective exits (stop, take-profit, trailing stop) are executed by sending market orders after a candle finishes, because StockSharp does not automatically manage MT5 protective orders.
  • Session filtering, fractal detection, and trailing logic strictly follow the EA's structure, including closing all positions when the hour filter is not satisfied.
  • The pip scaling logic mirrors the MT5 implementation by multiplying the symbol point by ten on 3- or 5-decimal instruments, ensuring equivalent price distances.

Usage tips

  1. Attach the strategy to a symbol and set OrderVolume to your preferred lot size.
  2. Choose a candle type that matches the timeframe used in MetaTrader 5 (the original EA works on any timeframe).
  3. Adjust the trading window to your broker's session or desired hours.
  4. Tune the pip-based distances to reflect instrument volatility. Larger TrailingStepPips reduces trailing frequency, while smaller values make the stop follow price more closely.
  5. Monitor logs for entries and exits; the strategy draws trades on the optional chart area for quick visual validation.
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 converted from the MT5 "Fractals at Close prices" expert advisor.
/// Detects bullish and bearish fractal sequences built on close prices and trades trend reversals.
/// Includes configurable trading hours and manual risk management with trailing stops.
/// </summary>
public class FractalsAtClosePricesStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeWindow = new(6);

	private decimal? _lastUpperFractal;
	private decimal? _previousUpperFractal;
	private decimal? _lastLowerFractal;
	private decimal? _previousLowerFractal;

	private decimal _pipValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;

	private decimal? _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Trading volume used for every market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can start opening positions.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy stops opening positions.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Stop-loss size expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit size expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum price improvement required before moving the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="FractalsAtClosePricesStrategy"/> parameters.
	/// </summary>
	public FractalsAtClosePricesStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for entries", "General")
		;

		_startHour = Param(nameof(StartHour), 0)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Hour when trading can start (0-23)", "Trading Hours");

		_endHour = Param(nameof(EndHour), 0)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Hour when trading stops (0-23)", "Trading Hours");

		_stopLossPips = Param(nameof(StopLossPips), 200)
		.SetRange(0, 1000)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 400)
		.SetRange(0, 1000)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
		;

		_trailingStopPips = Param(nameof(TrailingStopPips), 15)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Stop (pips)", "Base distance for the trailing stop", "Risk Management")
		;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk Management")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles processed by the strategy", "General");
	}

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

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

		_closeWindow.Clear();
		_lastUpperFractal = null;
		_previousUpperFractal = null;
		_lastLowerFractal = null;
		_previousLowerFractal = null;

		_pipValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;

		_entryPrice = null;
		ResetRiskLevels();
	}

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

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

		_pipValue = priceStep;
		if (decimals == 3 || decimals == 5)
		{
			// MT5 version multiplies point value by 10 when the symbol uses 3 or 5 decimals.
			_pipValue *= 10m;
		}

		_stopLossDistance = StopLossPips == 0 ? 0m : StopLossPips * _pipValue;
		_takeProfitDistance = TakeProfitPips == 0 ? 0m : TakeProfitPips * _pipValue;
		_trailingStopDistance = TrailingStopPips == 0 ? 0m : TrailingStopPips * _pipValue;
		_trailingStepDistance = TrailingStepPips == 0 ? 0m : TrailingStepPips * _pipValue;

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

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

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

		UpdateFractals(candle);

		if (!IsWithinTradingHours(candle.OpenTime))
		{
			CloseAllPositions();
			return;
		}

		ApplyRiskManagement(candle);

		// no bound indicators, skip IsFormedAndOnlineAndAllowTrading()

		ExecuteEntries(candle);
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		// Maintain a rolling window of the five most recent closes.
		_closeWindow.Add(candle.ClosePrice);
		while (_closeWindow.Count > 5)
			_closeWindow.RemoveAt(0);

		if (_closeWindow.Count < 5)
		{
			return;
		}

		var window = _closeWindow;
		var center = window[2];

		var isUpper = center > window[0]
		&& center > window[1]
		&& center >= window[3]
		&& center >= window[4];

		if (isUpper)
		{
			_previousUpperFractal = _lastUpperFractal;
			_lastUpperFractal = center;
		}

		var isLower = center < window[0]
		&& center < window[1]
		&& center <= window[3]
		&& center <= window[4];

		if (isLower)
		{
			_previousLowerFractal = _lastLowerFractal;
			_lastLowerFractal = center;
		}
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;

		if (StartHour == EndHour)
		{
			// Trade the entire day when start and end hours are equal.
			return true;
		}

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

		return hour >= StartHour || hour < EndHour;
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				// Close the long position if the stop-loss level is breached.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				// Close the long position when the take-profit level is hit.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				// Cover the short position if the stop-loss level is breached.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				// Cover the short position when the take-profit level is hit.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = candle.ClosePrice - entry;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice - _trailingStopDistance;
		if (_longStop is decimal currentStop && currentStop >= candle.ClosePrice - (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_longStop = targetStop;
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = entry - candle.ClosePrice;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice + _trailingStopDistance;
		if (_shortStop is decimal currentStop && currentStop <= candle.ClosePrice + (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_shortStop = targetStop;
	}

	private void ExecuteEntries(ICandleMessage candle)
	{
		// Only trade when flat to avoid too frequent reversals.
		if (Position != 0)
			return;

		var bullishTrend = _lastLowerFractal is decimal lastLow
		&& _previousLowerFractal is decimal prevLow
		&& prevLow < lastLow;

		if (bullishTrend && OrderVolume > 0m)
		{
			BuyMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_longStop = _stopLossDistance > 0m ? candle.ClosePrice - _stopLossDistance : null;
			_longTake = _takeProfitDistance > 0m ? candle.ClosePrice + _takeProfitDistance : null;
			_shortStop = null;
			_shortTake = null;
			return;
		}

		var bearishTrend = _lastUpperFractal is decimal lastUp
		&& _previousUpperFractal is decimal prevUp
		&& prevUp > lastUp;

		if (bearishTrend && OrderVolume > 0m)
		{
			SellMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_shortStop = _stopLossDistance > 0m ? candle.ClosePrice + _stopLossDistance : null;
			_shortTake = _takeProfitDistance > 0m ? candle.ClosePrice - _takeProfitDistance : null;
			_longStop = null;
			_longTake = null;
		}
	}

	private void CloseAllPositions()
	{
		if (Position > 0)
		{
			SellMarket(Position);
		}
		else if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
		}

		ResetRiskLevels();
	}

	private void CloseLongPosition()
	{
		if (Position > 0)
		{
			SellMarket(Position);
			ResetRiskLevels();
		}
	}

	private void CloseShortPosition()
	{
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetRiskLevels();
		}
	}

	private void ResetRiskLevels()
	{
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = null;
	}
}