Locker
Grid-based hedging strategy that alternates long and short market orders to lock floating losses and capture a small percentage profit on the account balance.
Trading logic
- Opens the first long position with the configured starting volume as soon as the first candle closes.
- Tracks every subsequent entry and keeps an internal ledger of buy and sell legs to estimate combined unrealized and realized profit.
- If the number of active legs reaches eight, the strategy closes the earliest available buy/sell pair to keep exposure under control before doing anything else on that candle.
- When the combined profit rises above the target percentage of the portfolio value, it exits all remaining positions and resets the internal state.
- When the combined profit drops below the negative target, it measures the distance between the latest entry price and the current market price. If price has moved upward by the configured step it adds a new short leg; if price has moved downward by the same distance it adds a new long leg.
- Every close uses market orders in the opposite direction of the recorded entry so the hedge is neutralized immediately.
Parameters
- Profit % – percentage of the current portfolio value that should be locked in before flattening the book.
- Start Volume – quantity used for the very first long entry that seeds the grid.
- Step Volume – quantity submitted for every hedging order once the loss threshold is breached.
- Step Points – number of price steps between grid levels; multiplied by the instrument's price step to calculate the actual price distance.
- Enable Automation – master switch that pauses all trading logic when disabled.
- Candle Type – candle series used to trigger the decision logic on every finished bar.
The conversion replicates the original MetaTrader expert logic while adapting order placement to the StockSharp high-level API and storing detailed trade state inside the strategy so that profit calculation matches the MQL version.
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;
public class LockerStrategy : Strategy
{
private readonly struct PositionEntry
{
public PositionEntry(Sides side, decimal price, decimal volume)
{
Side = side;
Price = price;
Volume = volume;
}
public Sides Side { get; }
public decimal Price { get; }
public decimal Volume { get; }
}
private readonly StrategyParam<decimal> _profitTargetPercent;
private readonly StrategyParam<decimal> _startVolume;
private readonly StrategyParam<decimal> _stepVolume;
private readonly StrategyParam<decimal> _stepPoints;
private readonly StrategyParam<bool> _enableAutomation;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxOpenPositions;
private readonly List<PositionEntry> _entries = new();
private decimal _realizedPnL;
private decimal _lastEntryPrice;
private Sides? _lastEntrySide;
private int _cooldown;
public decimal ProfitTargetPercent { get => _profitTargetPercent.Value; set => _profitTargetPercent.Value = value; }
public decimal StartVolume { get => _startVolume.Value; set => _startVolume.Value = value; }
public decimal StepVolume { get => _stepVolume.Value; set => _stepVolume.Value = value; }
public decimal StepPoints { get => _stepPoints.Value; set => _stepPoints.Value = value; }
public bool EnableAutomation { get => _enableAutomation.Value; set => _enableAutomation.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int MaxOpenPositions { get => _maxOpenPositions.Value; set => _maxOpenPositions.Value = value; }
public LockerStrategy()
{
_profitTargetPercent = Param(nameof(ProfitTargetPercent), 0.001m)
.SetGreaterThanZero()
.SetDisplay("Profit %", "Target profit percent of balance", "General")
;
_startVolume = Param(nameof(StartVolume), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Start Volume", "Initial trade volume", "General")
;
_stepVolume = Param(nameof(StepVolume), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
;
_stepPoints = Param(nameof(StepPoints), 15000m)
.SetGreaterThanZero()
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
;
_enableAutomation = Param(nameof(EnableAutomation), true)
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for processing", "Data");
_maxOpenPositions = Param(nameof(MaxOpenPositions), 2)
.SetGreaterThanZero()
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_entries.Clear();
_realizedPnL = 0m;
_lastEntryPrice = 0m;
_lastEntrySide = null;
_cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType).Bind(Process).Start();
}
private void Process(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!EnableAutomation)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var closePrice = candle.ClosePrice;
// Use the candle close as a proxy for bid/ask because we operate on finished bars.
var bid = closePrice;
var ask = closePrice;
var currentProfit = _realizedPnL + CalculateUnrealizedProfit(bid, ask);
var openCount = _entries.Count;
if (openCount == 0)
{
// Start the grid with an initial buy order.
OpenPosition(Sides.Buy, StartVolume, ask);
return;
}
if (openCount >= MaxOpenPositions && TryClosePair(bid, ask))
{
// Reduce exposure when too many hedged orders are active.
return;
}
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
if (portfolioValue <= 0m)
portfolioValue = 1000000m;
var targetProfit = portfolioValue * ProfitTargetPercent;
if (targetProfit > 0m && currentProfit >= targetProfit)
{
// Target reached, flatten the book.
CloseAllPositions(bid, ask);
_cooldown = 20;
return;
}
if (targetProfit <= 0m)
return;
if (currentProfit <= -targetProfit)
{
var lastPrice = _lastEntryPrice;
if (lastPrice == 0m)
return;
var stepDistance = GetStepDistance();
if (stepDistance <= 0m)
return;
// Add a hedging order whenever price travels far enough from the latest entry.
if (ask > lastPrice + stepDistance)
OpenPosition(Sides.Sell, StepVolume, ask);
else if (bid < lastPrice - stepDistance)
OpenPosition(Sides.Buy, StepVolume, bid);
}
}
private decimal CalculateUnrealizedProfit(decimal bid, decimal ask)
{
var profit = 0m;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? 1m : -1m;
profit += (exitPrice - entry.Price) * direction * entry.Volume;
}
return profit;
}
private bool TryClosePair(decimal bid, decimal ask)
{
var buyIndex = -1;
var sellIndex = -1;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
if (entry.Side == Sides.Buy && buyIndex == -1)
buyIndex = i;
else if (entry.Side == Sides.Sell && sellIndex == -1)
sellIndex = i;
if (buyIndex != -1 && sellIndex != -1)
break;
}
if (buyIndex == -1 || sellIndex == -1)
return false;
if (buyIndex > sellIndex)
{
CloseEntry(buyIndex, bid, ask);
CloseEntry(sellIndex, bid, ask);
}
else
{
CloseEntry(sellIndex, bid, ask);
CloseEntry(buyIndex, bid, ask);
}
UpdateLastEntry();
return true;
}
private void CloseAllPositions(decimal bid, decimal ask)
{
while (_entries.Count > 0)
{
CloseEntry(_entries.Count - 1, bid, ask);
}
UpdateLastEntry();
}
private void CloseEntry(int index, decimal bid, decimal ask)
{
if (index < 0 || index >= _entries.Count)
return;
var entry = _entries[index];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? Sides.Sell : Sides.Buy;
// Send the offsetting market order to neutralize the entry.
if (direction == Sides.Sell)
SellMarket();
else
BuyMarket();
var pnl = (exitPrice - entry.Price) * (entry.Side == Sides.Buy ? 1m : -1m) * entry.Volume;
_realizedPnL += pnl;
try { _entries.RemoveAt(index); } catch { }
}
private void OpenPosition(Sides side, decimal volume, decimal price)
{
if (volume <= 0m)
return;
if (side == Sides.Buy)
BuyMarket();
else
SellMarket();
_entries.Add(new PositionEntry(side, price, volume));
_lastEntryPrice = price;
_lastEntrySide = side;
}
private decimal GetStepDistance()
{
var priceStep = Security?.PriceStep ?? 0m;
return priceStep > 0m ? StepPoints * priceStep : StepPoints;
}
private void UpdateLastEntry()
{
if (_entries.Count == 0)
{
_lastEntryPrice = 0m;
_lastEntrySide = null;
return;
}
var entry = _entries[_entries.Count - 1];
_lastEntryPrice = entry.Price;
_lastEntrySide = entry.Side;
}
}
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 locker_strategy(Strategy):
"""Hedging grid locker: opens initial position, hedges on drawdown, closes at profit target."""
def __init__(self):
super(locker_strategy, self).__init__()
self._profit_target_percent = self.Param("ProfitTargetPercent", 0.001) \
.SetGreaterThanZero() \
.SetDisplay("Profit %", "Target profit percent of balance", "General")
self._start_volume = self.Param("StartVolume", 0.5) \
.SetGreaterThanZero() \
.SetDisplay("Start Volume", "Initial trade volume", "General")
self._step_volume = self.Param("StepVolume", 0.2) \
.SetGreaterThanZero() \
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
self._step_points = self.Param("StepPoints", 15000.0) \
.SetGreaterThanZero() \
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
self._enable_automation = self.Param("EnableAutomation", True) \
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles for processing", "Data")
self._max_open_positions = self.Param("MaxOpenPositions", 2) \
.SetGreaterThanZero() \
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
# entries: list of (side, price, volume) tuples; side='buy' or 'sell'
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
@property
def ProfitTargetPercent(self):
return float(self._profit_target_percent.Value)
@property
def StartVolume(self):
return float(self._start_volume.Value)
@property
def StepVolume(self):
return float(self._step_volume.Value)
@property
def StepPoints(self):
return float(self._step_points.Value)
@property
def EnableAutomation(self):
return self._enable_automation.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def MaxOpenPositions(self):
return int(self._max_open_positions.Value)
def OnStarted2(self, time):
super(locker_strategy, self).OnStarted2(time)
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
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
if not self.EnableAutomation:
return
if self._cooldown > 0:
self._cooldown -= 1
return
close_price = float(candle.ClosePrice)
bid = close_price
ask = close_price
current_profit = self._realized_pnl + self._calc_unrealized(bid, ask)
open_count = len(self._entries)
if open_count == 0:
self._open_position('buy', self.StartVolume, ask)
return
if open_count >= self.MaxOpenPositions and self._try_close_pair(bid, ask):
return
portfolio_value = 1000000.0
if self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
pv = float(self.Portfolio.CurrentValue)
if pv > 0:
portfolio_value = pv
target_profit = portfolio_value * self.ProfitTargetPercent
if target_profit > 0 and current_profit >= target_profit:
self._close_all(bid, ask)
self._cooldown = 20
return
if target_profit <= 0:
return
if current_profit <= -target_profit:
last_price = self._last_entry_price
if last_price == 0:
return
step_distance = self._get_step_distance()
if step_distance <= 0:
return
if ask > last_price + step_distance:
self._open_position('sell', self.StepVolume, ask)
elif bid < last_price - step_distance:
self._open_position('buy', self.StepVolume, bid)
def _calc_unrealized(self, bid, ask):
profit = 0.0
for side, price, volume in self._entries:
exit_price = bid if side == 'buy' else ask
direction = 1.0 if side == 'buy' else -1.0
profit += (exit_price - price) * direction * volume
return profit
def _try_close_pair(self, bid, ask):
buy_index = -1
sell_index = -1
for i in range(len(self._entries)):
side = self._entries[i][0]
if side == 'buy' and buy_index == -1:
buy_index = i
elif side == 'sell' and sell_index == -1:
sell_index = i
if buy_index != -1 and sell_index != -1:
break
if buy_index == -1 or sell_index == -1:
return False
if buy_index > sell_index:
self._close_entry(buy_index, bid, ask)
self._close_entry(sell_index, bid, ask)
else:
self._close_entry(sell_index, bid, ask)
self._close_entry(buy_index, bid, ask)
self._update_last_entry()
return True
def _close_all(self, bid, ask):
while len(self._entries) > 0:
self._close_entry(len(self._entries) - 1, bid, ask)
self._update_last_entry()
def _close_entry(self, index, bid, ask):
if index < 0 or index >= len(self._entries):
return
side, price, volume = self._entries[index]
exit_price = bid if side == 'buy' else ask
if side == 'buy':
self.SellMarket()
else:
self.BuyMarket()
direction = 1.0 if side == 'buy' else -1.0
pnl = (exit_price - price) * direction * volume
self._realized_pnl += pnl
self._entries.pop(index)
def _open_position(self, side, volume, price):
if volume <= 0:
return
if side == 'buy':
self.BuyMarket()
else:
self.SellMarket()
self._entries.append((side, price, volume))
self._last_entry_price = price
self._last_entry_side = side
def _get_step_distance(self):
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
if price_step > 0:
return self.StepPoints * price_step
return self.StepPoints
def _update_last_entry(self):
if len(self._entries) == 0:
self._last_entry_price = 0.0
self._last_entry_side = None
return
side, price, volume = self._entries[-1]
self._last_entry_price = price
self._last_entry_side = side
def OnReseted(self):
super(locker_strategy, self).OnReseted()
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
def CreateClone(self):
return locker_strategy()