Esta estratégia é um port para StockSharp do consultor especialista de MetaTrader 5 "Fractals at Close prices" de Vladimir Karputov. Ela analisa cinco preços de fechamento consecutivos para detectar fractais no estilo de Bill Williams construídos estritamente sobre fechamentos em vez de máximos ou mínimos. Os dois fractais de alta e de baixa mais recentes são comparados para determinar a tendência ativa. Quando o último fractal de alta aparece acima do anterior, a estratégia abre uma posição comprada. Quando o último fractal de baixa se forma abaixo do anterior, abre uma posição vendida. Posições opostas são sempre fechadas antes de entrar em uma nova negociação, portanto a estratégia permanece em no máximo uma direção por vez.
As negociações são permitidas apenas entre a hora de início e a hora de término configuráveis. Se a hora atual cair fora dessa janela, todas as posições abertas são fechadas imediatamente, replicando o comportamento do EA original. O filtro de tempo suporta janelas intradiárias (início < fim), sessões noturnas que cruzam a meia-noite (início > fim) e trading durante todo o dia (início == fim).
Lógica do indicador
Cada vela finalizada é adicionada a uma fila deslizante de cinco elementos de preços de fechamento.
Uma vez disponíveis cinco valores, o fechamento intermediário (duas velas atrás) é avaliado:
Um fractal de alta é registrado se o fechamento intermediário for estritamente maior que os dois fechamentos mais antigos e maior ou igual aos dois fechamentos mais novos.
Um fractal de baixa é registrado se o fechamento intermediário for estritamente menor que os dois fechamentos mais antigos e menor ou igual aos dois fechamentos mais novos.
Os fractais de alta e de baixa mais recentes e anteriores são armazenados para comparação posterior.
Uma tendência de alta é detectada quando o último fractal de alta é mais alto que o anterior. Uma tendência de baixa é detectada quando o último fractal de baixa é mais baixo que o anterior.
Regras de trading
Entradas compradas
Fechar qualquer posição vendida ativa a mercado.
Se não houver posição comprada aberta, comprar OrderVolume a mercado no fechamento que confirmou a sequência de fractal de alta.
Entradas vendidas
Fechar qualquer posição comprada ativa a mercado.
Se não houver posição vendida aberta, vender OrderVolume a mercado quando uma sequência de fractal de baixa for confirmada.
Controle de sessão
Antes de aplicar sinais, a estratégia verifica se candle.OpenTime.Hour está dentro da janela de trading. Caso contrário, CloseAllPositions é chamado e a barra é ignorada.
Gestão de risco
As distâncias de stop-loss e take-profit são expressas em pips. A implementação reproduz a abordagem MT5: o ponto do símbolo é multiplicado por dez quando o instrumento tem 3 ou 5 decimais. O valor pip resultante é então multiplicado pelas distâncias configuradas.
Ao entrar em uma posição, os níveis iniciais de stop-loss e take-profit são armazenados internamente. Como o StockSharp não gerencia automaticamente ordens protetoras no estilo MT5, a estratégia monitora velas finalizadas e sai a mercado quando seu intervalo de preços toca o nível armazenado.
Os trailing stops seguem as regras originais do EA. Um novo stop é calculado como close ± TrailingStop assim que o lucro superar TrailingStop + TrailingStep. O trailing stop só avança se o movimento do stop anterior for de pelo menos TrailingStep.
Quando o horário de trading termina, todas as posições são fechadas independentemente do status do trailing. Isso replica o EA chamando CloseAllPositions fora da sessão permitida.
Parâmetros
Nome
Descrição
Padrão
OrderVolume
Volume usado para cada ordem a mercado.
0.1
StartHour
Hora (0-23) em que o trading se torna ativo. Se igual a EndHour, a estratégia opera o dia todo.
10
EndHour
Hora (0-23) em que o trading para de aceitar novos sinais.
22
StopLossPips
Distância do stop-loss expressa em pips. 0 desativa o stop.
30
TakeProfitPips
Distância do take-profit expressa em pips. 0 desativa o take.
50
TrailingStopPips
Distância base do trailing stop em pips. 0 desativa o trailing.
15
TrailingStepPips
Lucro adicional (em pips) necessário antes que o trailing stop avance.
5
CandleType
Tipo de dados de velas assinado pela estratégia. O padrão é velas de período de 1 hora.
1 hour TimeFrame
Notas de implementação
A estratégia usa SubscribeCandles com a API de alto nível e não registra indicadores manualmente, seguindo as diretrizes do projeto.
Saídas protetoras (stop, take-profit, trailing stop) são executadas enviando ordens a mercado após uma vela terminar, pois o StockSharp não gerencia automaticamente as ordens protetoras do MT5.
Filtragem de sessão, detecção de fractais e lógica de trailing seguem estritamente a estrutura do EA, incluindo o fechamento de todas as posições quando o filtro de hora não é satisfeito.
A lógica de escala de pips espelha a implementação MT5 multiplicando o ponto do símbolo por dez em instrumentos de 3 ou 5 decimais, garantindo distâncias de preço equivalentes.
Dicas de uso
Anexar a estratégia a um símbolo e definir OrderVolume para o tamanho de lote preferido.
Escolher um tipo de vela que corresponda ao período usado no MetaTrader 5 (o EA original funciona em qualquer período).
Ajustar a janela de trading para a sessão do corretor ou horas desejadas.
Ajustar as distâncias baseadas em pips para refletir a volatilidade do instrumento. TrailingStepPips maior reduz a frequência do trailing, enquanto valores menores fazem o stop seguir o preço mais de perto.
Monitorar registros para entradas e saídas; a estratégia desenha negociações na área de gráfico opcional para validação visual rápida.
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()