GitHub で見る

Amstell Grid Manager Strategy

High-level port of the MetaTrader expert "exp_Amstell-SL" that runs a bi-directional averaging grid. The strategy keeps track of the most recent fill price on each side and issues additional market orders when price drifts far enough away, while liquidating the open batch once a fixed take-profit or stop-loss distance is reached. The implementation uses StockSharp's candle subscriptions and high-level order helpers, so it can be plugged into any environment that provides candle data for a single security.

The translated logic is slightly adapted for StockSharp's netted portfolio model: long and short grids are still managed separately, but they are not held at the same time. The long grid is active while the net position is non-negative, and the short grid takes over only after all long exposure has been flattened.

How it works

Market data and execution flow

  • Subscribes to the configured CandleType (default: 1 minute time-frame candles) and processes only finished candles.
  • Calculates pip-based offsets from the security's PriceStep. If the step has 3 or 5 decimal places, it is multiplied by 10 to mimic MetaTrader's 3/5 digit pip adjustment.
  • All trades are placed through BuyMarket/SellMarket helpers; no pending orders are used.

Long-side management

  • Opens the first long position (OrderVolume) as soon as there is no existing long exposure and the strategy is not in the middle of closing shorts.
  • Tracks the most recent long fill price and the volume-weighted average entry price for the active long batch.
  • Places additional long orders of size OrderVolume whenever the closing price has fallen by at least BuyDistancePips (converted to price units) below the last long fill.

Short-side management

  • Once the long batch is fully closed and the net position is non-positive, the strategy allows short entries.
  • Places the initial short order when there is no short exposure; further shorts are opened after the price rises by BuyDistancePips * SellDistanceMultiplier above the previous short fill.
  • Maintains the most recent short fill price and the volume-weighted average entry price for the active short batch.

Exit rules

  • For each direction, computes unrealised profit relative to the average fill.
  • Closes the entire long batch with a market sell when profit reaches TakeProfitPips pips or the drawdown reaches StopLossPips pips.
  • Closes the entire short batch with a market buy when profit reaches TakeProfitPips pips or the adverse move reaches StopLossPips pips.
  • After liquidation, all cached prices and volumes are reset so a new grid can start on the next candle.

Differences versus the original MQL expert

  • The StockSharp version operates on candle closes instead of individual ticks.
  • Long and short grids are executed sequentially rather than simultaneously, matching StockSharp's default netting mode.
  • All protective distances are checked against the averaged entry price instead of each ticket individually, which mirrors the aggregate net position behaviour.

Parameters

Parameter Default Optimization range Description
OrderVolume 0.01 0.010.10 (step 0.01) Quantity submitted with every grid order. Must be positive.
TakeProfitPips 30 10150 (step 10) Profit target for the active batch expressed in pips.
StopLossPips 30 10150 (step 10) Maximum adverse move before abandoning the batch.
BuyDistancePips 10 560 (step 5) Minimum drop from the last long fill to add another buy. Must be less than both TP and SL.
SellDistanceMultiplier 10 215 (step 1) Multiplier applied to the long distance when spacing short entries.
CandleType 1-minute time-frame Candle series used for signal generation.

Implementation notes

  • BuyDistancePips must be strictly less than TakeProfitPips and StopLossPips; the strategy throws an exception at start-up otherwise, reproducing the MetaTrader validation.
  • Pip size is derived from the security's PriceStep. Adjust the parameters if the instrument uses a non-standard tick size.
  • All internal state is cleared in OnReseted, allowing the strategy to be restarted without residual grid data.
  • No colour customisation or manual indicator registration is used, matching the high-level API guidelines in this repository.
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;
	}
}