Открыть на GitHub

Стратегия Amstell Grid Manager

Высокоуровневый порт эксперта MetaTrader "exp_Amstell-SL", реализующий двухсторонний усредняющий сеточный алгоритм. Стратегия хранит последние цены исполнения по каждой стороне и докупает/допродаёт позицию при отклонении цены на заданное расстояние, закрывая весь блок ордеров после достижения фиксированного тейк-профита или стоп-лосса. Реализация использует подписку на свечи и вспомогательные методы высокоуровневого API StockSharp, поэтому её можно подключить к любому источнику свечных данных по одному инструменту.

Перенос адаптирован к неттинговой модели StockSharp: длинная и короткая сетки управляются раздельно, но не удерживаются одновременно. Пока совокупная позиция не отрицательная, активна длинная сетка; после полного закрытия лонга управление передаётся короткой стороне.

Как работает стратегия

Поток данных и исполнение

  • Подписывается на свечи типа CandleType (по умолчанию тайм-фрейм 1 минута) и обрабатывает только завершённые свечи.
  • Пересчитывает точки (pips) из PriceStep. Если шаг цены имеет 3 или 5 знаков после запятой, он дополнительно умножается на 10 — аналогично оригинальной проверке MetaTrader для инструментов с 3/5-значной точкой.
  • Все сделки выполняются рыночными ордерами через BuyMarket/SellMarket, отложенные заявки не используются.

Управление длинной сеткой

  • Открывает первичный лонг объёмом OrderVolume, когда нет существующего длинного экспозиционного блока и не выполняется принудительное закрытие шорта.
  • Сохраняет последнюю цену покупки и средневзвешенную цену входа для текущего длинного блока.
  • Добавляет очередной лонг тем же объёмом, если цена закрытия опустилась как минимум на BuyDistancePips (в пересчёте на цену) ниже последнего исполнения.

Управление короткой сеткой

  • Активируется только после полного закрытия длинного блока, когда чистая позиция не положительная.
  • Открывает первый шорт, если нет активных коротких позиций; последующие шорты добавляются при росте цены на BuyDistancePips * SellDistanceMultiplier выше последнего короткого исполнения.
  • Аналогично хранит последнюю цену продажи и средневзвешенную цену входа короткого блока.

Правила закрытия

  • Для каждого направления рассчитывает нереализованную прибыль относительно средневзвешенной цены входа.
  • Закрывает весь длинный блок рыночной продажей при достижении TakeProfitPips пунктов прибыли либо StopLossPips пунктов просадки.
  • Закрывает весь короткий блок рыночной покупкой при тех же расстояниях по прибыли/убытку.
  • После ликвидации обнуляет накопленные цены и объёмы, чтобы следующий блок стартовал «с чистого листа».

Отличия от оригинального эксперта MQL

  • Логика основана на закрытиях свечей, а не на тиковых котировках.
  • Лонг и шорт выполняются последовательно из-за неттинговой схемы StockSharp.
  • Стопы и тейки проверяются относительно средневзвешенной цены блока, а не для каждой заявки отдельно.

Параметры

Параметр Значение по умолчанию Диапазон оптимизации Описание
OrderVolume 0.01 0.010.10 (шаг 0.01) Объём каждой сеточной заявки. Должен быть положительным.
TakeProfitPips 30 10150 (шаг 10) Цель по прибыли для текущего блока в пунктах.
StopLossPips 30 10150 (шаг 10) Максимально допустимое противодвижение для блока.
BuyDistancePips 10 560 (шаг 5) Минимальное отклонение вниз перед добором лонга. Должно быть меньше тейка и стопа.
SellDistanceMultiplier 10 215 (шаг 1) Во сколько раз расстояние для шорта превышает длинное расстояние.
CandleType тайм-фрейм 1 минута Тип свечей, по которым выполняются расчёты.

Особенности реализации

  • При запуске проверяется условие BuyDistancePips < TakeProfitPips и BuyDistancePips < StopLossPips; при нарушении генерируется исключение, как и в исходном советнике.
  • Размер пункта вычисляется по PriceStep; при нетипичном шаге цены стоит пересмотреть параметры.
  • Метод OnReseted полностью очищает внутренние состояния, чтобы повторный запуск начинался без остаточных данных.
  • Код соответствует стилевым требованиям репозитория: используется только высокоуровневый API, без явной окраски графика и без ручной регистрации индикаторов.
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;

/// <summary>
/// Amstell averaging grid strategy that opens new entries when price drifts away
/// from the last fill and closes exposure once profit or loss thresholds are reached.
/// </summary>
public class AmstellGridManagerStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _buyDistancePips;
	private readonly StrategyParam<decimal> _sellDistanceMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _longVolume;
	private decimal _shortVolume;
	private decimal? _averageLongPrice;
	private decimal? _averageShortPrice;
	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private decimal _pipValue;
	private decimal _takeProfitOffset;
	private decimal _stopLossOffset;
	private decimal _buyDistanceOffset;
	private decimal _sellDistanceOffset;
	private bool _closingLong;
	private bool _closingShort;

	/// <summary>
	/// Quantity per market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Profit target in pips for each grid leg.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in pips for each grid leg.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Price distance in pips required to add another long position.
	/// </summary>
	public int BuyDistancePips
	{
		get => _buyDistancePips.Value;
		set => _buyDistancePips.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the long distance when stacking short entries.
	/// </summary>
	public decimal SellDistanceMultiplier
	{
		get => _sellDistanceMultiplier.Value;
		set => _sellDistanceMultiplier.Value = value;
	}

	/// <summary>
	/// Candle data type used for decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public AmstellGridManagerStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
			
			.SetOptimize(0.01m, 0.1m, 0.01m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stopLossPips = Param(nameof(StopLossPips), 30)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
			
			.SetOptimize(10, 150, 10);

		_buyDistancePips = Param(nameof(BuyDistancePips), 10)
			.SetGreaterThanZero()
			.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
			
			.SetOptimize(5, 60, 5);

		_sellDistanceMultiplier = Param(nameof(SellDistanceMultiplier), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance when adding shorts", "Entries")
			
			.SetOptimize(2m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for processing", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_longVolume = 0m;
		_shortVolume = 0m;
		_averageLongPrice = null;
		_averageShortPrice = null;
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_pipValue = 0m;
		_takeProfitOffset = 0m;
		_stopLossOffset = 0m;
		_buyDistanceOffset = 0m;
		_sellDistanceOffset = 0m;
		_closingLong = false;
		_closingShort = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		if (BuyDistancePips >= TakeProfitPips || BuyDistancePips >= StopLossPips)
			throw new InvalidOperationException("Buy distance must be less than take profit and stop loss distances.");

		UpdatePriceOffsets();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// no indicators bound via .Bind()

		var close = candle.ClosePrice;

		if (!_closingLong && _longVolume > 0m && _averageLongPrice is decimal longAvg)
		{
			var profit = close - longAvg;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				SellMarket();
				_closingLong = true;
				return;
			}
		}

		if (!_closingShort && _shortVolume > 0m && _averageShortPrice is decimal shortAvg)
		{
			var profit = shortAvg - close;
			if (profit >= _takeProfitOffset || -profit >= _stopLossOffset)
			{
				BuyMarket();
				_closingShort = true;
				return;
			}
		}

		var openedLong = false;

		if (!_closingLong && Position >= 0m)
		{
			if (_longVolume <= 0m)
			{
				BuyMarket();
				openedLong = true;
			}
			else if (_lastBuyPrice is decimal lastBuy && lastBuy - close >= _buyDistanceOffset)
			{
				BuyMarket();
				openedLong = true;
			}
		}

		if (openedLong)
			return;

		if (!_closingShort && Position <= 0m)
		{
			if (_shortVolume <= 0m)
			{
				SellMarket();
			}
			else if (_lastSellPrice is decimal lastSell && close - lastSell >= _sellDistanceOffset)
			{
				SellMarket();
			}
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order == null)
			return;

		var tradeVolume = trade.Trade.Volume;
		var price = trade.Trade.Price;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_shortVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _shortVolume);
				_shortVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_shortVolume <= 0m)
				{
					_shortVolume = 0m;
					_averageShortPrice = null;
					_lastSellPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _longVolume + tradeVolume;
				var totalCost = (_averageLongPrice ?? 0m) * _longVolume + price * tradeVolume;
				_longVolume = newVolume;
				_averageLongPrice = totalCost / newVolume;
				_lastBuyPrice = price;
				_closingLong = false;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_longVolume > 0m)
			{
				var closingVolume = Math.Min(tradeVolume, _longVolume);
				_longVolume -= closingVolume;
				tradeVolume -= closingVolume;
				if (_longVolume <= 0m)
				{
					_longVolume = 0m;
					_averageLongPrice = null;
					_lastBuyPrice = null;
				}
			}

			if (tradeVolume > 0m)
			{
				var newVolume = _shortVolume + tradeVolume;
				var totalCost = (_averageShortPrice ?? 0m) * _shortVolume + price * tradeVolume;
				_shortVolume = newVolume;
				_averageShortPrice = totalCost / newVolume;
				_lastSellPrice = price;
				_closingShort = false;
			}
		}

		if (_longVolume <= 0m && Position <= 0m)
			_closingLong = false;

		if (_shortVolume <= 0m && Position >= 0m)
			_closingShort = false;

		if (Position == 0m)
		{
			_longVolume = 0m;
			_shortVolume = 0m;
			_averageLongPrice = null;
			_averageShortPrice = null;
			_lastBuyPrice = null;
			_lastSellPrice = null;
			_closingLong = false;
			_closingShort = false;
		}
	}

	private void UpdatePriceOffsets()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var decimals = GetDecimalPlaces(step);
		_pipValue = decimals == 3 || decimals == 5 ? step * 10m : step;

		_takeProfitOffset = TakeProfitPips * _pipValue;
		_stopLossOffset = StopLossPips * _pipValue;
		_buyDistanceOffset = BuyDistancePips * _pipValue;
		_sellDistanceOffset = _buyDistanceOffset * SellDistanceMultiplier;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0x7F;
	}
}