GitHub で見る

Adaptive Grid MT4 (StockSharp Port)

Overview

The strategy recreates the "Adaptive Grid Mt4" expert advisor for StockSharp's high level API. It drops a symmetric grid of buy stop and sell stop orders around the current candle close. Grid distances are derived from the Average True Range (ATR) and are therefore adaptive to market volatility. Each pending order expires after a configurable number of candles, keeping the order book tidy in sideways markets.

When an entry order is filled the strategy immediately registers the matching take-profit and stop-loss orders at prices computed from the ATR snapshot that produced the grid. Protective orders are one-to-one with the filled entry and persist until executed or manually cancelled.

Parameters

Parameter Description
GridLevels Number of stop orders above and below the market. Equivalent to the nGrid input of the EA.
TimerBars Number of finished candles after which any pending entry is cancelled (MT4 nBars).
PriceOffsetMultiplier ATR multiplier applied to the initial offset from the current price (Poffset).
GridStepMultiplier ATR multiplier used for the spacing between consecutive grid levels (Pstep).
StopLossMultiplier ATR multiplier defining the distance of the stop-loss attached to each order (StopLoss).
TakeProfitMultiplier ATR multiplier defining the distance of the take-profit (TakeProfit).
AtrPeriod ATR averaging period. Mirrors the hard-coded value of 14 from the script.
OrderVolume Volume used for all pending orders (MT4 Lot).
CandleType Time frame that drives grid recalculation (Wtf).

Trading Logic

  1. Subscribe to candles of the configured CandleType and feed an ATR(14).
  2. On each finished candle:
    • Advance the internal bar counter and cancel pending grid orders that exceeded TimerBars.
    • Skip further processing if the ATR is not formed, any grid order is still active, or the strategy already holds a position.
    • Compute the breakout offset, grid spacing, stop-loss and take-profit distances as ATR * multiplier values.
    • Place GridLevels pairs of buy stop and sell stop orders around the candle close, normalising prices with Security.ShrinkPrice to honour the instrument tick size.
  3. When an entry fills, remove it from the tracked grid list and spawn the corresponding protective orders:
    • Long entries receive a SellStop stop-loss and a SellLimit take-profit.
    • Short entries receive a BuyStop stop-loss and a BuyLimit take-profit.
  4. Protective orders are monitored via OnOrderChanged so that completed or cancelled entries are removed from the tracking lists.

Notes

  • The grid is only rebuilt when there are no open positions and all existing grid orders expired, matching the What() logic of the original EA.
  • Prices are calculated from the candle close instead of the raw Bid/Ask tick. This keeps the implementation candle-driven while producing the same symmetric layout around the market.
  • The ATR snapshot used for the grid is also used for protective orders to mimic MetaTrader's per-ticket stop and take-profit values.
  • There is no Python translation yet, matching the request.
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();
	}
}