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.01 – 0.10 (step 0.01) |
Quantity submitted with every grid order. Must be positive. |
TakeProfitPips |
30 |
10 – 150 (step 10) |
Profit target for the active batch expressed in pips. |
StopLossPips |
30 |
10 – 150 (step 10) |
Maximum adverse move before abandoning the batch. |
BuyDistancePips |
10 |
5 – 60 (step 5) |
Minimum drop from the last long fill to add another buy. Must be less than both TP and SL. |
SellDistanceMultiplier |
10 |
2 – 15 (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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class amstell_grid_manager_strategy(Strategy):
"""Amstell averaging grid strategy with TP/SL and distance-based grid entries."""
def __init__(self):
super(amstell_grid_manager_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Quantity submitted with each grid order", "Trading")
self._take_profit_pips = self.Param("TakeProfitPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 30) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._buy_distance_pips = self.Param("BuyDistancePips", 10) \
.SetGreaterThanZero() \
.SetDisplay("Buy Distance (pips)", "Distance before adding another long", "Entries")
self._sell_distance_mult = self.Param("SellDistanceMultiplier", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Sell Distance Multiplier", "Multiplier applied to long distance for shorts", "Entries")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for processing", "General")
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def BuyDistancePips(self):
return self._buy_distance_pips.Value
@property
def SellDistanceMultiplier(self):
return self._sell_distance_mult.Value
@property
def CandleType(self):
return self._candle_type.Value
def _calc_pip_value(self):
sec = self.Security
if sec is None or sec.PriceStep is None or float(sec.PriceStep) <= 0:
return 1.0
step = float(sec.PriceStep)
decimals = sec.Decimals if sec.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def OnStarted2(self, time):
super(amstell_grid_manager_strategy, self).OnStarted2(time)
self.Volume = self.OrderVolume
self._pip_value = self._calc_pip_value()
self._tp_offset = self.TakeProfitPips * self._pip_value
self._sl_offset = self.StopLossPips * self._pip_value
self._buy_dist_offset = self.BuyDistancePips * self._pip_value
self._sell_dist_offset = self._buy_dist_offset * float(self.SellDistanceMultiplier)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
# Check long TP/SL
if not self._closing_long and self._long_volume > 0 and self._avg_long_price is not None:
profit = close - self._avg_long_price
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.SellMarket()
self._closing_long = True
return
# Check short TP/SL
if not self._closing_short and self._short_volume > 0 and self._avg_short_price is not None:
profit = self._avg_short_price - close
if profit >= self._tp_offset or -profit >= self._sl_offset:
self.BuyMarket()
self._closing_short = True
return
opened_long = False
# Grid long entries
if not self._closing_long and self.Position >= 0:
if self._long_volume <= 0:
self.BuyMarket()
self._record_buy(close)
opened_long = True
elif self._last_buy_price is not None and self._last_buy_price - close >= self._buy_dist_offset:
self.BuyMarket()
self._record_buy(close)
opened_long = True
if opened_long:
return
# Grid short entries
if not self._closing_short and self.Position <= 0:
if self._short_volume <= 0:
self.SellMarket()
self._record_sell(close)
elif self._last_sell_price is not None and close - self._last_sell_price >= self._sell_dist_offset:
self.SellMarket()
self._record_sell(close)
def _record_buy(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._long_volume + vol
total_cost = (self._avg_long_price if self._avg_long_price is not None else 0.0) * self._long_volume + price * vol
self._long_volume = new_vol
self._avg_long_price = total_cost / new_vol if new_vol > 0 else price
self._last_buy_price = price
self._closing_long = False
def _record_sell(self, price):
vol = float(self.Volume) if self.Volume > 0 else 1.0
new_vol = self._short_volume + vol
total_cost = (self._avg_short_price if self._avg_short_price is not None else 0.0) * self._short_volume + price * vol
self._short_volume = new_vol
self._avg_short_price = total_cost / new_vol if new_vol > 0 else price
self._last_sell_price = price
self._closing_short = False
def _check_position_sync(self):
if self.Position == 0:
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._closing_long = False
self._closing_short = False
def OnReseted(self):
super(amstell_grid_manager_strategy, self).OnReseted()
self._long_volume = 0.0
self._short_volume = 0.0
self._avg_long_price = None
self._avg_short_price = None
self._last_buy_price = None
self._last_sell_price = None
self._pip_value = 0.0
self._tp_offset = 0.0
self._sl_offset = 0.0
self._buy_dist_offset = 0.0
self._sell_dist_offset = 0.0
self._closing_long = False
self._closing_short = False
def CreateClone(self):
return amstell_grid_manager_strategy()