Открыть на GitHub

Adaptive Grid MT4 (порт для StockSharp)

Обзор

Стратегия повторяет советник "Adaptive Grid Mt4" на высокоуровневом API StockSharp. Она размещает симметричную сетку из отложенных Buy Stop и Sell Stop вокруг цены закрытия текущей свечи. Расстояния между уровнями вычисляются через показатель Average True Range (ATR), поэтому сетка автоматически подстраивается под волатильность рынка. Каждая заявка имеет ограниченный срок жизни в свечах, что предотвращает захламление книги заявок во флэте.

При исполнении входящей заявки стратегия сразу выставляет соответствующие тейк-профит и стоп-лосс, используя значения ATR в момент генерации сетки. Защитные приказы жёстко привязаны к исходной сделке и остаются активными до исполнения или ручной отмены.

Параметры

Параметр Описание
GridLevels Количество уровней выше и ниже рынка (nGrid).
TimerBars Максимальное число завершённых свечей до отмены отложенной заявки (nBars).
PriceOffsetMultiplier Множитель ATR для стартового смещения от цены (Poffset).
GridStepMultiplier Множитель ATR для шага между уровнями (Pstep).
StopLossMultiplier Множитель ATR для вычисления стоп-лосса (StopLoss).
TakeProfitMultiplier Множитель ATR для вычисления тейк-профита (TakeProfit).
AtrPeriod Период усреднения ATR (аналог фиксированного значения 14 в оригинале).
OrderVolume Объём каждой заявки (Lot).
CandleType Таймфрейм свечей, по которым пересчитывается сетка (Wtf).

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

  1. Подписка на свечи выбранного CandleType и расчёт ATR(14).
  2. На каждой завершённой свече:
    • Увеличить счётчик баров и отменить сеточные заявки, превысившие ограничение TimerBars.
    • Пропустить дальнейшую обработку, если ATR ещё не сформирован, есть активные заявки сетки или открыта позиция.
    • Рассчитать смещение, шаг сетки, стоп-лосс и тейк-профит как ATR * множитель.
    • Выставить по GridLevels пар Buy Stop и Sell Stop вокруг цены закрытия, нормализуя уровни через Security.ShrinkPrice.
  3. При исполнении заявки удалить её из списка сетки и поставить соответствующие защитные приказы:
    • Для длинных позиций — SellStop и SellLimit.
    • Для коротких — BuyStop и BuyLimit.
  4. Метод OnOrderChanged отслеживает завершённые защитные ордера и очищает списки.

Дополнительно

  • Новая сетка строится только после полного отсутствия позиций и активных отложенных заявок, что повторяет поведение функции What() в MQL.
  • В качестве базовой цены используется закрытие свечи вместо последних Bid/Ask тиков, что позволяет оставаться в событийной модели по свечам и всё равно получать симметричную сетку.
  • ATR фиксируется на момент генерации сетки и применятся к защитным приказам, повторяя прикреплённые стопы и тейки в MetaTrader.
  • Python-версия не создавалась по требованию.
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Adaptive grid strategy using ATR-based breakout levels.
/// Simplified from the "Adaptive Grid Mt4" expert advisor to use market orders.
/// </summary>
public class AdaptiveGridMt4Strategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _breakoutMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private AverageTrueRange _atr;
	private decimal? _prevClose;
	private decimal? _prevAtr;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	/// <summary>
	/// ATR averaging period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Breakout threshold in ATR multiples.
	/// </summary>
	public decimal BreakoutMultiplier
	{
		get => _breakoutMultiplier.Value;
		set => _breakoutMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to drive calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public AdaptiveGridMt4Strategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("ATR Period", "Number of candles used for ATR smoothing", "Indicators");

		_breakoutMultiplier = Param(nameof(BreakoutMultiplier), 2.5m)
		.SetGreaterThanZero()
		.SetDisplay("Breakout Multiplier", "ATR multiplier for breakout threshold", "Grid");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
		.SetDisplay("Candle Type", "Candle type used to trigger grid recalculation", "General");
	}

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

		_prevClose = null;
		_prevAtr = null;
		_stopPrice = 0;
		_takeProfitPrice = 0;

		_atr = new AverageTrueRange { Length = AtrPeriod };

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

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

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

		if (!_atr.IsFormed || atrValue <= 0)
		{
			_prevClose = candle.ClosePrice;
			_prevAtr = atrValue;
			return;
		}

		// Check protective stops
		if (Position > 0)
		{
			if (_stopPrice > 0 && candle.LowPrice <= _stopPrice)
			{
				SellMarket(Math.Abs(Position));
				_stopPrice = 0;
				_takeProfitPrice = 0;
			}
			else if (_takeProfitPrice > 0 && candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket(Math.Abs(Position));
				_stopPrice = 0;
				_takeProfitPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice > 0 && candle.HighPrice >= _stopPrice)
			{
				BuyMarket(Math.Abs(Position));
				_stopPrice = 0;
				_takeProfitPrice = 0;
			}
			else if (_takeProfitPrice > 0 && candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket(Math.Abs(Position));
				_stopPrice = 0;
				_takeProfitPrice = 0;
			}
		}

		if (_prevClose is not decimal prevClose || _prevAtr is not decimal prevAtr || prevAtr <= 0)
		{
			_prevClose = candle.ClosePrice;
			_prevAtr = atrValue;
			return;
		}

		var threshold = prevAtr * BreakoutMultiplier;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Breakout up
		if (candle.ClosePrice > prevClose + threshold && Position <= 0)
		{
			BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
			_stopPrice = candle.ClosePrice - atrValue * 3;
			_takeProfitPrice = candle.ClosePrice + atrValue * 4;
		}
		// Breakout down
		else if (candle.ClosePrice < prevClose - threshold && Position >= 0)
		{
			SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
			_stopPrice = candle.ClosePrice + atrValue * 3;
			_takeProfitPrice = candle.ClosePrice - atrValue * 4;
		}

		_prevClose = candle.ClosePrice;
		_prevAtr = atrValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_atr = null;
		_prevClose = null;
		_prevAtr = null;
		_stopPrice = 0;
		_takeProfitPrice = 0;

		base.OnReseted();
	}
}