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
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.
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.
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
Attach the strategy to a symbol and set OrderVolume to your preferred lot size.
Choose a candle type that matches the timeframe used in MetaTrader 5 (the original EA works on any timeframe).
Adjust the trading window to your broker's session or desired hours.
Tune the pip-based distances to reflect instrument volatility. Larger TrailingStepPips reduces trailing frequency, while smaller values make the stop follow price more closely.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class fractals_at_close_prices_strategy(Strategy):
def __init__(self):
super(fractals_at_close_prices_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 0)
self._end_hour = self.Param("EndHour", 0)
self._stop_loss_pips = self.Param("StopLossPips", 200)
self._take_profit_pips = self.Param("TakeProfitPips", 400)
self._trailing_stop_pips = self.Param("TrailingStopPips", 15)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._pip_value = 0.0
self._sl_dist = 0.0
self._tp_dist = 0.0
self._trail_dist = 0.0
self._trail_step = 0.0
self._entry_price = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
def OnStarted2(self, time):
super(fractals_at_close_prices_strategy, self).OnStarted2(time)
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
decimals = sec.Decimals if sec is not None and sec.Decimals is not None else 0
self._pip_value = price_step
if decimals == 3 or decimals == 5:
self._pip_value *= 10.0
self._sl_dist = self.StopLossPips * self._pip_value if self.StopLossPips != 0 else 0.0
self._tp_dist = self.TakeProfitPips * self._pip_value if self.TakeProfitPips != 0 else 0.0
self._trail_dist = self.TrailingStopPips * self._pip_value if self.TrailingStopPips != 0 else 0.0
self._trail_step = self.TrailingStepPips * self._pip_value if self.TrailingStepPips != 0 else 0.0
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._entry_price = None
self._reset_risk_levels()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_fractals(candle)
if not self._is_within_trading_hours(candle.OpenTime):
self._close_all()
return
self._apply_risk_management(candle)
self._execute_entries(candle)
def _update_fractals(self, candle):
self._close_window.append(float(candle.ClosePrice))
while len(self._close_window) > 5:
self._close_window.pop(0)
if len(self._close_window) < 5:
return
w = self._close_window
center = w[2]
is_upper = (center > w[0] and center > w[1] and
center >= w[3] and center >= w[4])
if is_upper:
self._prev_upper_fractal = self._last_upper_fractal
self._last_upper_fractal = center
is_lower = (center < w[0] and center < w[1] and
center <= w[3] and center <= w[4])
if is_lower:
self._prev_lower_fractal = self._last_lower_fractal
self._last_lower_fractal = center
def _is_within_trading_hours(self, time):
hour = time.Hour
if self.StartHour == self.EndHour:
return True
if self.StartHour < self.EndHour:
return hour >= self.StartHour and hour < self.EndHour
return hour >= self.StartHour or hour < self.EndHour
def _apply_risk_management(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position > 0:
if self._long_stop is not None and low <= self._long_stop:
self.SellMarket()
self._reset_risk_levels()
return
if self._long_take is not None and high >= self._long_take:
self.SellMarket()
self._reset_risk_levels()
return
self._update_long_trailing(candle)
elif self.Position < 0:
if self._short_stop is not None and high >= self._short_stop:
self.BuyMarket()
self._reset_risk_levels()
return
if self._short_take is not None and low <= self._short_take:
self.BuyMarket()
self._reset_risk_levels()
return
self._update_short_trailing(candle)
def _update_long_trailing(self, candle):
if self._trail_dist <= 0 or self._entry_price is None:
return
close = float(candle.ClosePrice)
profit_dist = close - self._entry_price
if profit_dist <= self._trail_dist + self._trail_step:
return
target_stop = close - self._trail_dist
if self._long_stop is not None and self._long_stop >= close - (self._trail_dist + self._trail_step):
return
self._long_stop = target_stop
def _update_short_trailing(self, candle):
if self._trail_dist <= 0 or self._entry_price is None:
return
close = float(candle.ClosePrice)
profit_dist = self._entry_price - close
if profit_dist <= self._trail_dist + self._trail_step:
return
target_stop = close + self._trail_dist
if self._short_stop is not None and self._short_stop <= close + (self._trail_dist + self._trail_step):
return
self._short_stop = target_stop
def _execute_entries(self, candle):
if self.Position != 0:
return
close = float(candle.ClosePrice)
bullish_trend = (self._last_lower_fractal is not None and
self._prev_lower_fractal is not None and
self._prev_lower_fractal < self._last_lower_fractal)
if bullish_trend:
self.BuyMarket()
self._entry_price = close
self._long_stop = close - self._sl_dist if self._sl_dist > 0 else None
self._long_take = close + self._tp_dist if self._tp_dist > 0 else None
self._short_stop = None
self._short_take = None
return
bearish_trend = (self._last_upper_fractal is not None and
self._prev_upper_fractal is not None and
self._prev_upper_fractal > self._last_upper_fractal)
if bearish_trend:
self.SellMarket()
self._entry_price = close
self._short_stop = close + self._sl_dist if self._sl_dist > 0 else None
self._short_take = close - self._tp_dist if self._tp_dist > 0 else None
self._long_stop = None
self._long_take = None
def _close_all(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_risk_levels()
def _reset_risk_levels(self):
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._entry_price = None
def OnReseted(self):
super(fractals_at_close_prices_strategy, self).OnReseted()
self._close_window = []
self._last_upper_fractal = None
self._prev_upper_fractal = None
self._last_lower_fractal = None
self._prev_lower_fractal = None
self._pip_value = 0.0
self._sl_dist = 0.0
self._tp_dist = 0.0
self._trail_dist = 0.0
self._trail_step = 0.0
self._entry_price = None
self._reset_risk_levels()
def CreateClone(self):
return fractals_at_close_prices_strategy()