Открыть на GitHub

Стратегия Exp Amstell

Обзор

Exp Amstell — это сеточная торговая система, портированная из исходного советника MetaTrader 4 exp_Amstell.mq4. Она непрерывно размещает рыночные заявки на покупку и продажу, как только цена отходит от последней сделки на заданное количество пунктов. Каждая позиция ведётся отдельно: когда рынок проходит дистанцию тейк-профита, стратегия отправляет встречную заявку и фиксирует прибыль именно по этой ступени сетки.

В отличие от трендовых алгоритмов Exp Amstell всегда остаётся активной. Она не ожидает сигналов от индикаторов и вместо этого наращивает позиции в обе стороны при колебаниях рынка. Поэтому ключевую роль играют параметры дистанций и размер каждой заявки.

Логика торговли

  • Обработка по тикам. Стратегия подписывается на Level1 и реагирует на каждое изменение лучших цен Bid/Ask — аналогично функции start() в MQL.
  • Независимые стеки лонгов и шортов. Покупка разрешена, если нет открытых длинных сделок или если Ask опустился минимум на расстояние повторного входа относительно последней покупки. Продажи используют зеркальное условие по Bid.
  • Тейк-профит для каждой ступени. Открытые слои отслеживаются по отдельности. Когда Bid (для лонгов) или Ask (для шортов) проходит заданное число пунктов, закрывается только соответствующий слой, остальные остаются нетронутыми.
  • Эмуляция FIFO. Исполненные сделки записываются в порядке поступления, что имитирует тикетную систему MetaTrader и гарантирует списание объёма с самых ранних позиций при частичном закрытии.
  • Учёт неттинга. В StockSharp позиции неттингуются. Если новая покупка перекрывает активный шорт, стратегия сначала уменьшает объём шортового стека и лишь затем добавляет остаток как новый лонг.

Параметры

Имя Тип Значение по умолчанию Описание
TradeVolume decimal 0.1 Объём каждой рыночной заявки при открытии нового слоя сетки.
TakeProfitPoints int 30 Расстояние в пунктах MetaTrader, которое должна пройти цена для закрытия отдельного слоя.
ReentryDistancePoints int 10 Минимальная дистанция от последней сделки перед добавлением следующей заявки в ту же сторону.

Стратегия автоматически переводит пункты в реальные шаги цены через PriceStep. Для трёх- и пятизнаковых котировок применяется метатрейдеровский множитель, чтобы 1 пункт соответствовал 0.0001 (или 0.01 для инструментов типа JPY).

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

  • Достаточно данных Level1; свечи не используются. Метод GetWorkingSecurities() запрашивает (Security, DataType.Level1).
  • В OnStarted вызывается StartProtection(), чтобы платформа закрыла оставшиеся позиции при аварийной остановке стратегии.
  • Все комментарии в исходнике написаны на английском языке согласно правилам репозитория.
  • Из-за неттинга StockSharp порт не может одновременно удерживать разнонаправленные позиции. Если сделки противоположных направлений приходят подряд, новая заявка сначала закроет текущую экспозицию, а затем откроет свежий слой.

Практические рекомендации

  1. Тщательно подбирайте дистанции. Малые значения делают сетку плотнее и повышают нагрузку в волатильных режимах. Большие значения снижают частоту сделок, но увеличивают просадку на слой.
  2. Осторожно выбирайте объём. Сеточные системы быстро наращивают позицию. Начинайте с консервативных величин и тестируйте стратегию в Designer или Backtester.
  3. Добавьте внешние ограничения риска. В оригинальном советнике нет глобального стоп-лосса. Используйте портфельные защиты, чтобы ограничить хвостовые риски.
  4. Следите за качеством исполнения. Алгоритм предполагает исполнение по лучшим ценам Bid/Ask. Проскальзывание напрямую влияет на достижение тейк-профита.

Источник

Преобразовано из MQL/9027/exp_Amstell.mq4.

using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Exp Amstell: Grid-style strategy that scales into positions
/// on ATR-based price movements and closes on profit targets.
/// </summary>
public class ExpAmstellStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<int> _emaLength;

	private decimal _entryPrice;
	private decimal _prevEma;
	private int _gridCount;

	public ExpAmstellStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for grid distance.", "Indicators");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "EMA period for trend.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

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

		_entryPrice = 0;
		_prevEma = 0;
		_gridCount = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_entryPrice = 0;
		_prevEma = 0;
		_gridCount = 0;

		var atr = new AverageTrueRange { Length = AtrLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(atr, ema, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

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

		if (atrVal <= 0 || _prevEma == 0)
		{
			_prevEma = emaVal;
			return;
		}

		var close = candle.ClosePrice;

		// Grid exit: take profit at 1.5 ATR or stop at 3 ATR
		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 1.5m)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close <= _entryPrice - atrVal * 3m)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close <= _entryPrice - atrVal)
			{
				// Scale in: add to position on pullback
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				BuyMarket();
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 1.5m)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close >= _entryPrice + atrVal * 3m)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close >= _entryPrice + atrVal)
			{
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				SellMarket();
			}
		}

		// Entry: EMA direction determines initial direction
		if (Position == 0)
		{
			if (close > emaVal && emaVal > _prevEma)
			{
				_entryPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (close < emaVal && emaVal < _prevEma)
			{
				_entryPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}