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 порт не может одновременно удерживать разнонаправленные позиции. Если сделки противоположных направлений приходят подряд, новая заявка сначала закроет текущую экспозицию, а затем откроет свежий слой.
Практические рекомендации
Тщательно подбирайте дистанции. Малые значения делают сетку плотнее и повышают нагрузку в волатильных режимах. Большие значения снижают частоту сделок, но увеличивают просадку на слой.
Осторожно выбирайте объём. Сеточные системы быстро наращивают позицию. Начинайте с консервативных величин и тестируйте стратегию в Designer или Backtester.
Добавьте внешние ограничения риска. В оригинальном советнике нет глобального стоп-лосса. Используйте портфельные защиты, чтобы ограничить хвостовые риски.
Следите за качеством исполнения. Алгоритм предполагает исполнение по лучшим ценам 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;
}
}
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.Indicators import AverageTrueRange, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class exp_amstell_strategy(Strategy):
"""
Grid-style strategy that scales into positions on ATR-based price movements.
Enters based on EMA direction, scales in on pullbacks, exits on ATR-based TP/SL.
"""
def __init__(self):
super(exp_amstell_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for grid distance", "Indicators")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA period for trend", "Indicators")
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(exp_amstell_strategy, self).OnReseted()
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
def OnStarted2(self, time):
super(exp_amstell_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
ema = ExponentialMovingAverage()
ema.Length = self._ema_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_val, ema_val):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr_val)
ema_val = float(ema_val)
if atr_val <= 0 or self._prev_ema == 0:
self._prev_ema = ema_val
return
close = float(candle.ClosePrice)
if self.Position > 0:
if close >= self._entry_price + atr_val * 1.5:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
elif close <= self._entry_price - atr_val * 3.0:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
elif self._grid_count < 3 and close <= self._entry_price - atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.BuyMarket()
elif self.Position < 0:
if close <= self._entry_price - atr_val * 1.5:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
elif close >= self._entry_price + atr_val * 3.0:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
elif self._grid_count < 3 and close >= self._entry_price + atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.SellMarket()
if self.Position == 0:
if close > ema_val and ema_val > self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.BuyMarket()
elif close < ema_val and ema_val < self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.SellMarket()
self._prev_ema = ema_val
def CreateClone(self):
return exp_amstell_strategy()