Стратегия повторяет логику советника MetaTrader 4 Locker.mq4. В начале каждого цикла отправляется рыночная покупка, после чего формируется хеджирующая сетка из длинных и коротких позиций. Когда суммарная нереализованная прибыль всех сделок достигает заданной доли от капитала, все позиции закрываются и стартует новый цикл. Если же текущий убыток становится таким же по величине, стратегия через фиксированные интервалы по точкам добавляет спасательные ордера, чередуя покупки и продажи и «запирая» цену между ними.
Параметры
Параметр
Описание
Значение по умолчанию
NeedProfitRatio
Доля капитала, при достижении (или просадке до) которой срабатывает закрытие или добавление ордеров. Значение 0.001 эквивалентно 0,1% от счёта.
0.001
InitialVolume
Объём первой рыночной покупки в каждом цикле.
0.5
StepVolume
Объём каждого спасательного ордера в фазе усреднения.
0.2
StepPoints
Интервал между спасательными ордерами в пунктах MetaTrader. Внутри стратегии пересчитывается в цену через Security.PriceStep.
50
EnableRescue
Включает усредняющую сетку при превышении убытком заданного порога. Если отключено, стратегия только ждёт прибыль по первой сделке.
true
Алгоритм работы
Старт цикла
При первом тиковом событии выставляется рыночная покупка объёмом InitialVolume.
Цена входа сохраняется как контрольная точка, а экстремумы для покупок и продаж сбрасываются на это значение.
Фиксация прибыли
На каждом тике вычисляется плавающий результат: для лонгов (price - averageBuyPrice) * longVolume, для шортов (averageSellPrice - price) * shortVolume.
Когда плавающая прибыль достигает NeedProfitRatio * equity, стратегия отправляет встречные рыночные ордера и закрывает весь портфель, после исполнения начинается новый цикл.
Спасательная сетка
Если плавающий результат опускается ниже -NeedProfitRatio * equity и EnableRescue = true, стратегия ждёт движения цены на StepPoints пунктов от контрольной точки (с учётом пересчёта в цену). Обновлённый максимум инициирует дополнительную покупку, минимум — продажу. Объём каждой операции равен StepVolume.
После каждой сделки обновляются контрольная точка и крайние значения, поэтому для следующего добавления нужно ещё одно полноценное движение цены.
Сброс цикла
Когда через события OnOwnTradeReceived становится ясно, что и длинные, и короткие позиции обнулены, флаги ожидания снимаются, контрольная точка и экстремумы устанавливаются в цену последней сделки, и стратегия готова снова открыть стартовую покупку.
Особенности реализации
Используется SubscribeTrades().Bind(ProcessTrade) для получения тиков, что соответствует оригинальному советнику, работавшему по текущим Bid/Ask.
Пункты MetaTrader переводятся в цену на базе Security.PriceStep; для инструментов с 3 или 5 знаками после запятой применяется десятикратный множитель, как в MT4.
В OnOwnTradeReceived отдельно ведутся объёмы и средние цены по лонгам и шортам, что позволяет держать хеджевые позиции одновременно в обе стороны.
Значение капитала берётся из Portfolio.CurrentValue с резервами на CurrentBalance и BeginValue. Первая положительная оценка кешируется, чтобы порог прибыли оставался стабильным.
Перед отправкой все объёмы проходят через AlignVolume, соблюдая ограничения VolumeStep, VolumeMin и VolumeMax инструмента.
Рекомендации по применению
Убедитесь, что у инструмента корректно задан PriceStep, иначе пересчёт пунктов нарушится и сетка не совпадёт с поведением в MetaTrader.
Усредняющая сетка по сути реализует мартингейл. Подбирайте StepVolume и StepPoints с учётом допустимой просадки: большие значения сокращают число ордеров, но увеличивают нагрузку на депозит.
Переключите EnableRescue в false, если нужен консервативный сценарий — открыли и держим до достижения цели без дополнительных ордеров.
Для достоверного тестирования на форекс-символах используйте тиковые данные.
Отличия от оригинального советника
Блок, который в исходнике пытался закрывать полностью взаимозачётные сделки при количестве ордеров ≥ 8, исключён: из-за ошибки с выбором тикетов он всё равно не работал.
Пересчёт StepLot по историческим позициям при инициализации не переносился, объёмы целиком контролируются параметрами StockSharp.
В версии StockSharp нет комментариев к ордерам, всплывающих окон и ручного флага остановки — реализована только автономная логика торговли.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Grid strategy that opens positions at regular price intervals.
/// Uses ATR to determine grid spacing and reverses direction on profit targets.
/// </summary>
public class LockerHedgingGridStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _gridMultiplier;
private decimal _gridLevel;
private decimal _entryPrice;
private bool _initialized;
public LockerHedgingGridStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for analysis.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "Period for ATR calculation.", "Indicators");
_gridMultiplier = Param(nameof(GridMultiplier), 1.5m)
.SetDisplay("Grid Multiplier", "ATR multiplier for grid spacing.", "Grid");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal GridMultiplier
{
get => _gridMultiplier.Value;
set => _gridMultiplier.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_gridLevel = 0;
_entryPrice = 0;
_initialized = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_gridLevel = 0;
_entryPrice = 0;
_initialized = false;
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (atrValue <= 0)
return;
var close = candle.ClosePrice;
var gridStep = atrValue * GridMultiplier;
if (!_initialized)
{
_gridLevel = close;
_initialized = true;
return;
}
// Grid logic: trade when price moves a full grid step
if (Position == 0)
{
if (close >= _gridLevel + gridStep)
{
// Price moved up a grid step - buy
_entryPrice = close;
_gridLevel = close;
BuyMarket();
}
else if (close <= _gridLevel - gridStep)
{
// Price moved down a grid step - sell
_entryPrice = close;
_gridLevel = close;
SellMarket();
}
}
else if (Position > 0)
{
if (close >= _entryPrice + gridStep)
{
// Take profit
SellMarket();
_gridLevel = close;
}
else if (close <= _entryPrice - gridStep * 2)
{
// Stop-loss at 2x grid step
SellMarket();
_gridLevel = close;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - gridStep)
{
// Take profit
BuyMarket();
_gridLevel = close;
}
else if (close >= _entryPrice + gridStep * 2)
{
// Stop-loss at 2x grid step
BuyMarket();
_gridLevel = close;
}
}
}
}
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.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class locker_hedging_grid_strategy(Strategy):
"""
Grid strategy using ATR for grid spacing.
Opens positions at grid steps, exits at TP/SL distances.
"""
def __init__(self):
super(locker_hedging_grid_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "Period for ATR", "Indicators")
self._grid_multiplier = self.Param("GridMultiplier", 1.5) \
.SetDisplay("Grid Multiplier", "ATR multiplier for grid spacing", "Grid")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe for analysis", "General")
self._grid_level = 0.0
self._entry_price = 0.0
self._initialized = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(locker_hedging_grid_strategy, self).OnReseted()
self._grid_level = 0.0
self._entry_price = 0.0
self._initialized = False
def OnStarted2(self, time):
super(locker_hedging_grid_strategy, self).OnStarted2(time)
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
atr = float(atr_val)
if atr <= 0:
return
close = float(candle.ClosePrice)
grid_step = atr * self._grid_multiplier.Value
if not self._initialized:
self._grid_level = close
self._initialized = True
return
if self.Position == 0:
if close >= self._grid_level + grid_step:
self._entry_price = close
self._grid_level = close
self.BuyMarket()
elif close <= self._grid_level - grid_step:
self._entry_price = close
self._grid_level = close
self.SellMarket()
elif self.Position > 0:
if close >= self._entry_price + grid_step:
self.SellMarket()
self._grid_level = close
elif close <= self._entry_price - grid_step * 2:
self.SellMarket()
self._grid_level = close
elif self.Position < 0:
if close <= self._entry_price - grid_step:
self.BuyMarket()
self._grid_level = close
elif close >= self._entry_price + grid_step * 2:
self.BuyMarket()
self._grid_level = close
def CreateClone(self):
return locker_hedging_grid_strategy()