Стратегия Fractals Minimum Distance переносит советник MetaTrader «Fractals minimum distance» на высокоуровневый API StockSharp. Алгоритм подписывается на выбранный поток свечей и после закрытия каждой новой свечи проверяет, сформировался ли на баре с отступом SignalBar подтверждённый пятисвечный фрактал Билла Вильямса. Сделка разрешается только тогда, когда расстояние между последними верхним и нижним фракталами превышает заданный минимум DistancePips.
Как и оригинал, порт сначала закрывает позицию противоположного направления, а затем разворачивается. Размер лота теперь задаётся свойством стратегии Volume, поэтому расчёт риска от свободной маржи, присутствующий в MQL-версии, исключён. Стоп-лоссы и тейк-профиты не выставляются, что полностью соответствует исходному коду.
Логика сигналов
Подписаться на свечи типа CandleType и поддерживать скользящее окно максимумов и минимумов, в котором всегда присутствует бар с указанным отступом и два соседних бара с каждой стороны.
Верхний фрактал считается подтверждённым, если максимум центрального бара больше максимумов двух предыдущих и двух последующих свечей. Аналогично определяется нижний фрактал по минимумам.
Значение DistancePips переводится в ценовое расстояние через PriceStep. Для инструментов с тремя или пятью знаками после запятой автоматически применяется множитель 10, чтобы 0.001 и 0.00001 интерпретировались как один пункт.
При появлении верхнего фрактала:
Сохранить новое значение и закрыть все длинные позиции.
Если оба последних фрактала известны и их разница не меньше порога, отправить рыночную заявку на продажу объёмом Volume.
При появлении нижнего фрактала:
Сохранить уровень и закрыть короткие позиции.
При выполнении условия дистанции отправить рыночную заявку на покупку объёмом Volume.
Обработка выполняется только после получения свечи в состоянии Finished, поэтому незавершённые бары не приводят к входам. Перед подачей заявок дополнительно проверяется IsFormedAndOnlineAndAllowTrading(), чтобы убедиться в готовности окружения.
Параметры
Параметр
Описание
Примечание
DistancePips
Минимальное расстояние между последними верхним и нижним фракталами в пунктах.
Внутри переводится в абсолютное ценовое значение с учётом шага цены инструмента.
SignalBar
Количество закрытых свечей между текущим моментом и баром, на котором должен находиться фрактал.
Эффективный минимум — 2, что соответствует определению фрактала у Вильямса.
CandleType
Тип свечей, используемых для расчётов.
По умолчанию минутные свечи, но можно выбрать любой другой таймфрейм.
Volume
Стандартный объём сделок стратегии.
Замещает расчёт риска и объёма из MQL-реализации.
Отличия от MQL-версии
Закрытие противоположных позиций реализовано теми же рыночными ордерами, что и функция ClosePositions в исходнике.
Ограничения по риску (SL/TP) отсутствуют, как и в оригинальном коде.
Параметры DistancePips и SignalBar сохраняют целочисленные значения, соответствующие типам ushort и uchar из MQL.
StockSharp использует неттинговую модель, поэтому противоположная заявка автоматически переворачивает позицию, что повторяет поведение MetaTrader на неттинговых счетах.
Рекомендации по применению
Начинайте с стандартного значения SignalBar = 3, затем адаптируйте DistancePips под волатильность конкретного инструмента.
Увеличение SignalBar позволяет дождаться большего числа подтверждающих свечей и снизить количество ложных сигналов.
При необходимости защитить позицию комбинируйте стратегию с сервисами управления риском StockSharp, например StartProtection().
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;
}
}
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_minimum_distance_strategy(Strategy):
"""Fractals minimum distance breakout strategy."""
def __init__(self):
super(fractals_minimum_distance_strategy, self).__init__()
self._distance_pips = self.Param("DistancePips", 15) \
.SetDisplay("Distance (pips)", "Minimum allowed gap between fractals", "Risk")
self._signal_bar = self.Param("SignalBar", 3) \
.SetDisplay("Signal bar offset", "How many closed bars ago the fractal must appear", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle type", "Primary candle series used for signals", "Data")
self._prev_upper = None
self._prev_lower = None
self._highs = []
self._lows = []
self._buffer_count = 0
self._window_size = 0
self._signal_offset = 0
self._pip_size = 0.0
@property
def DistancePips(self):
return self._distance_pips.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 1.0
step = float(sec.PriceStep)
# count decimals
digits = 0
temp = step
while temp > 0 and temp < 1 and digits < 10:
temp *= 10
digits += 1
if digits == 3 or digits == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(fractals_minimum_distance_strategy, self).OnStarted2(time)
self._signal_offset = max(2, self.SignalBar)
self._window_size = max(self._signal_offset + 3, 5)
self._highs = [0.0] * self._window_size
self._lows = [0.0] * self._window_size
self._buffer_count = 0
self._pip_size = self._calc_pip_size()
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 or self._window_size == 0:
return
ws = self._window_size
for i in range(ws - 1):
self._highs[i] = self._highs[i + 1]
self._lows[i] = self._lows[i + 1]
self._highs[ws - 1] = float(candle.HighPrice)
self._lows[ws - 1] = float(candle.LowPrice)
if self._buffer_count < ws:
self._buffer_count += 1
if self._buffer_count < ws:
return
ci = ws - 1 - self._signal_offset
if ci < 2 or ci > ws - 3:
return
high = self._highs[ci]
low = self._lows[ci]
is_upper = (high > self._highs[ci - 1] and high > self._highs[ci - 2] and
high > self._highs[ci + 1] and high > self._highs[ci + 2])
is_lower = (low < self._lows[ci - 1] and low < self._lows[ci - 2] and
low < self._lows[ci + 1] and low < self._lows[ci + 2])
dist_threshold = self.DistancePips * self._pip_size
if is_upper:
self._prev_upper = high
if self.Position > 0:
self.SellMarket()
if self._should_open(dist_threshold):
self.SellMarket()
if is_lower:
self._prev_lower = low
if self.Position < 0:
self.BuyMarket()
if self._should_open(dist_threshold):
self.BuyMarket()
def _should_open(self, threshold):
if self.Volume <= 0:
return False
if self._prev_upper is None or self._prev_lower is None:
return False
return abs(self._prev_upper - self._prev_lower) >= abs(threshold)
def OnReseted(self):
super(fractals_minimum_distance_strategy, self).OnReseted()
self._prev_upper = None
self._prev_lower = None
self._highs = []
self._lows = []
self._buffer_count = 0
self._window_size = 0
self._signal_offset = 0
self._pip_size = 0.0
def CreateClone(self):
return fractals_minimum_distance_strategy()