Открыть на GitHub

Locker — сетка с фиксацией прибыли

Стратегия повторяет логику советника MetaTrader 4 Locker.mq4. В начале каждого цикла отправляется рыночная покупка, после чего формируется хеджирующая сетка из длинных и коротких позиций. Когда суммарная нереализованная прибыль всех сделок достигает заданной доли от капитала, все позиции закрываются и стартует новый цикл. Если же текущий убыток становится таким же по величине, стратегия через фиксированные интервалы по точкам добавляет спасательные ордера, чередуя покупки и продажи и «запирая» цену между ними.

Параметры

Параметр Описание Значение по умолчанию
NeedProfitRatio Доля капитала, при достижении (или просадке до) которой срабатывает закрытие или добавление ордеров. Значение 0.001 эквивалентно 0,1% от счёта. 0.001
InitialVolume Объём первой рыночной покупки в каждом цикле. 0.5
StepVolume Объём каждого спасательного ордера в фазе усреднения. 0.2
StepPoints Интервал между спасательными ордерами в пунктах MetaTrader. Внутри стратегии пересчитывается в цену через Security.PriceStep. 50
EnableRescue Включает усредняющую сетку при превышении убытком заданного порога. Если отключено, стратегия только ждёт прибыль по первой сделке. true

Алгоритм работы

  1. Старт цикла

    • При первом тиковом событии выставляется рыночная покупка объёмом InitialVolume.
    • Цена входа сохраняется как контрольная точка, а экстремумы для покупок и продаж сбрасываются на это значение.
  2. Фиксация прибыли

    • На каждом тике вычисляется плавающий результат: для лонгов (price - averageBuyPrice) * longVolume, для шортов (averageSellPrice - price) * shortVolume.
    • Когда плавающая прибыль достигает NeedProfitRatio * equity, стратегия отправляет встречные рыночные ордера и закрывает весь портфель, после исполнения начинается новый цикл.
  3. Спасательная сетка

    • Если плавающий результат опускается ниже -NeedProfitRatio * equity и EnableRescue = true, стратегия ждёт движения цены на StepPoints пунктов от контрольной точки (с учётом пересчёта в цену). Обновлённый максимум инициирует дополнительную покупку, минимум — продажу. Объём каждой операции равен StepVolume.
    • После каждой сделки обновляются контрольная точка и крайние значения, поэтому для следующего добавления нужно ещё одно полноценное движение цены.
  4. Сброс цикла

    • Когда через события 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;
			}
		}
	}
}