Эта стратегия представляет собой порт советника MetaTrader 5 «Fractals at Close prices» автора Владимира Карпутова на платформу StockSharp. Она анализирует последовательность из пяти подряд закрытий и строит фракталы Билла Вильямса именно по ценам закрытия, без использования максимумов и минимумов. Для определения направления тренда сравниваются два последних бычьих и два последних медвежьих фрактала. Если новый бычий фрактал появился выше предыдущего, открывается длинная позиция. Если новый медвежий фрактал сформировался ниже предыдущего, открывается короткая позиция. Перед входом противоположная позиция закрывается, поэтому стратегия всегда находится только в одном направлении.
Сделки разрешены лишь в заданном часовом диапазоне. Если текущий час выходит за рамки интервала, все позиции закрываются немедленно — так же поступает оригинальный эксперт. Фильтр времени поддерживает внутридневную торговлю (start < end), переход через полночь (start > end) и круглосуточный режим (start == end).
Логика индикатора
Каждая завершённая свеча добавляется в скользящее окно из пяти последних закрытий.
Как только окно заполнено, анализируется средняя цена (две свечи назад):
Бычий фрактал фиксируется, если средняя цена строго выше двух более старых закрытий и не ниже двух более новых.
Медвежий фрактал фиксируется, если средняя цена строго ниже двух более старых закрытий и не выше двух более новых.
Сохраняются последние и предпоследние значения бычьих и медвежьих фракталов для последующего сравнения.
Если новый бычий фрактал выше предыдущего, определяется восходящий тренд. Если новый медвежий фрактал ниже предыдущего, считается, что тренд нисходящий.
Торговые правила
Лонг
Закрыть все активные короткие позиции по рынку.
Если длинной позиции нет, купить OrderVolume по рынку на закрытии, подтвердившем бычью последовательность фракталов.
Шорт
Закрыть все активные длинные позиции по рынку.
Если короткой позиции нет, продать OrderVolume по рынку при подтверждении медвежьей последовательности фракталов.
Контроль сессии
Перед обработкой сигналов проверяется, попадает ли candle.OpenTime.Hour в разрешённый диапазон. Если нет, вызывается CloseAllPositions, и текущая свеча игнорируется.
Управление рисками
Стоп-лосс и тейк-профит задаются в пунктах. Реализована логика MT5: значение шага цены умножается на десять, если инструмент имеет 3 или 5 знаков после запятой, и полученный «пункт» домножается на выбранные расстояния.
При входе уровни стоп-лосса и тейк-профита сохраняются во внутренних переменных. Поскольку StockSharp не сопровождает защитные ордера MT5 автоматически, стратегия отслеживает завершённые свечи и закрывает позицию по рынку, когда диапазон цены касается соответствующего уровня.
Трейлинг-стоп повторяет правила советника: новый стоп равен close ± TrailingStop, когда прибыль превышает TrailingStop + TrailingStep. Стоп смещается только если прирост относительно предыдущего уровня не меньше TrailingStep.
По окончании торгового окна все позиции закрываются независимо от состояния трейлинга, полностью копируя поведение оригинала.
Параметры
Имя
Описание
Значение по умолчанию
OrderVolume
Объём каждой рыночной сделки.
0.1
StartHour
Час (0–23), начиная с которого разрешена торговля. При равенстве с EndHour торговля ведётся весь день.
10
EndHour
Час (0–23), после которого сигналы не отрабатываются.
22
StopLossPips
Расстояние стоп-лосса в пунктах. Значение 0 отключает стоп.
30
TakeProfitPips
Расстояние тейк-профита в пунктах. Значение 0 отключает тейк.
50
TrailingStopPips
Базовое расстояние трейлинг-стопа в пунктах. Значение 0 отключает трейлинг.
15
TrailingStepPips
Дополнительный профит в пунктах, необходимый для переноса трейлинг-стопа.
5
CandleType
Тип свечей, на которые подписывается стратегия. По умолчанию — часовые свечи.
1 hour TimeFrame
Особенности реализации
Используется высокоуровневый API SubscribeCandles; индикаторы вручную не регистрируются, что соответствует требованиям репозитория.
Защитные выходы (стоп, тейк, трейлинг) исполняются рыночными ордерами после закрытия свечи, поскольку StockSharp не управляет ими автоматически, как MT5.
Фильтр торговых часов, поиск фракталов и логика трейлинга повторяют структуру советника, включая принудительное закрытие позиций вне торгового окна.
Масштабирование пунктов полностью совпадает с MT5 благодаря умножению шага цены на десять для инструментов с 3 или 5 знаками.
Практические рекомендации
Привяжите стратегию к нужному инструменту и задайте подходящий OrderVolume.
Выберите тип свечей, соответствующий таймфрейму, на котором запускался советник в MetaTrader 5.
Настройте временное окно торговли под сессию брокера или желаемые часы работы.
Подберите расстояния в пунктах под волатильность инструмента. Большие TrailingStepPips замедляют перенос стопа, меньшие заставляют его следовать за ценой плотнее.
Отслеживайте сообщения в логах и визуализацию сделок на графике, чтобы контролировать работу стратегии.
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()