GitHub で見る

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;
	}
}