The strategy replicates the MetaTrader 4 expert advisor Locker.mq4. It starts every cycle with a market buy and then manages a hedged grid of buy and sell orders. Whenever the combined unrealized profit of all open trades reaches a fixed fraction of the account equity, every position is closed and a fresh cycle begins. If the floating loss exceeds the same fraction in the negative direction, the strategy progressively adds rescue orders at fixed point intervals, locking price swings with alternating long and short entries.
Parameters
Parameter
Description
Default
NeedProfitRatio
Fraction of portfolio equity that must be earned (or lost) before closing/adding orders. 0.001 corresponds to 0.1% of the account.
0.001
InitialVolume
Volume of the very first market buy order at the beginning of every cycle.
0.5
StepVolume
Volume for each rescue order that is added while the strategy is in a drawdown phase.
0.2
StepPoints
Distance in MetaTrader points between rescue orders. Converted internally into price by using Security.PriceStep (pip) information.
50
EnableRescue
Enables the averaging grid when the floating loss breaches the negative threshold. If disabled the strategy only performs the initial trade and waits for profit.
true
Trading Logic
Cycle start
On the first incoming trade quote a market buy is sent with InitialVolume.
The entry price becomes the reference checkpoint, and both the highest buy and lowest sell trackers are reset to that price.
Profit locking
At every tick the strategy sums the unrealized P&L from all long and short legs. Long legs contribute (price - averageBuyPrice) * longVolume, while short legs add (averageSellPrice - price) * shortVolume.
Once the floating profit reaches NeedProfitRatio * equity, all positions are flattened via opposite market orders. A new cycle starts after fills are confirmed.
Rescue grid
When unrealized profit drops below -NeedProfitRatio * equity and EnableRescue is true, the system waits for price to move StepPoints (converted to price distance). Each new high above the last checkpoint issues another market buy, whereas each new low schedules a market sell. Volumes always equal StepVolume.
Checkpoint and directional extremes are updated after every rescue order so that the next addition requires another full step in price.
Cycle reset
After both long and short inventories drop to zero (confirmed through own trade notifications) the checkpoint and extremes are reset to the latest trade price and the strategy is ready to seed a new cycle with the initial buy.
Implementation Notes
Uses SubscribeTrades().Bind(ProcessTrade) to work with tick-by-tick prices, mirroring the original MQL EA that reacted to the current bid/ask.
Converts MetaTrader "points" to StockSharp prices via a pip size derived from Security.PriceStep. Symbols quoted with 3 or 5 decimals receive the standard x10 adjustment.
Tracks long and short inventories separately inside OnOwnTradeReceived, enabling hedged exposure exactly like the MT4 version (buy and sell positions can coexist).
Portfolio equity is estimated from Portfolio.CurrentValue with fallbacks to CurrentBalance or BeginValue. The first positive reading is cached so that the profit threshold remains stable even if the provider stops reporting the value.
Every market order volume goes through an AlignVolume helper that honors Security.VolumeStep, VolumeMin, and VolumeMax restrictions.
Usage Tips
Ensure that the instrument metadata supplies a correct PriceStep; otherwise the point-to-price conversion will be inaccurate and the grid distances will not match MetaTrader behaviour.
Because the rescue logic mirrors a martingale-style averaging, choose StepVolume carefully and monitor risk. Increasing both StepPoints and StepVolume reduces the number of open trades but amplifies exposure.
Set EnableRescue to false to replicate a conservative variant that simply waits for the first position to hit the profit target without ever averaging down.
Back-testing on Forex symbols should be performed with tick data to match the EA’s original granularity.
Differences from the MQL Expert
The original script attempted to close perfectly offsetting order pairs when more than eight trades were active. That block never executed because of a ticket-filter bug, and it has been omitted.
StepLot recalculation based on pre-existing orders at initialization is not replicated; volumes are controlled entirely through the parameters exposed in StockSharp.
Order comments, alert pop-ups, and manual stop flags from the EA are not present—the StockSharp version focuses purely on autonomous trading logic.
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()