Конвертация советника MetaTrader exp_Amstell-SL. Стратегия сразу открывает длинную и короткую позиции, добавляет новые ордера при движении цены против последнего входа на заданное количество пунктов и закрывает каждую сделку по виртуальным (программным) уровням тейк-профита и стоп-лосса.
Логика стратегии
Стартовые входы: при запуске, если открытых сделок нет, отправляются рыночные ордера Buy (по Ask) и Sell (по Bid).
Наращивание позиции при просадке:
Для покупок: когда текущий Ask ниже цены последнего лонга на ReentryPoints пунктов (по умолчанию 10), отправляется новый Buy тем же объёмом.
Для продаж: когда текущий Bid выше цены последнего шорта на ReentryPoints пунктов, открывается дополнительный Sell тем же объёмом.
Правила выхода (виртуальное управление):
Для каждой сделки Buy контролируются лучшие Bid и Ask. Если Bid вырос на TakeProfitPoints пунктов от цены входа или Ask упал на StopLossPoints пунктов, лонг закрывается рыночной продажей.
Для каждой сделки Sell проверяется, ушёл ли Ask ниже на TakeProfitPoints пунктов или вырос ли Bid на StopLossPoints. В этих случаях шорт закрывается рыночной покупкой.
Последовательность действий: сначала оцениваются условия выхода, и только затем рассматриваются новые входы, что повторяет логику оригинального эксперта, завершающего обработку тика сразу после закрытия сделки.
Параметры
TakeProfitPoints – расстояние в пунктах для фиксации прибыли. Значение по умолчанию: 30.
StopLossPoints – расстояние в пунктах для защиты от убытков. Значение по умолчанию: 30.
Volume – объём каждого нового ордера. Значение по умолчанию: 0.01.
ReentryPoints – величина неблагоприятного движения (в пунктах), после которой добавляется очередной ордер. Значение по умолчанию: 10.
Дополнительные замечания
Размер пункта вычисляется по Security.PriceStep; если биржа его не предоставляет, используется значение 1.
Стратегия может одновременно удерживать лонги и шорты, так как независимо ведёт учёт покупок и продаж, что соответствует хеджирующему стилю оригинального советника.
Уровни тейк-профита и стоп-лосса исполняются виртуально: на биржевой стакан они не выставляются, закрытие происходит через рыночные заявки.
Риск быстро возрастает при сильном трендовом движении, поскольку новые ордера добавляются без сокращения предыдущей позиции.
Наиболее уместно применять стратегию к инструментам, где «пункт» соответствует минимальному шагу цены (например, основные валютные пары на MetaTrader).
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Amstell SL: Grid averaging strategy with ATR-based take profit and stop loss.
/// Adds positions on adverse moves and exits on profit/stop targets.
/// </summary>
public class AmstellSlStrategy : 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;
private int _cooldown;
public AmstellSlStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
_emaLength = Param(nameof(EmaLength), 50)
.SetDisplay("EMA Length", "EMA trend filter.", "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;
_cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_prevEma = 0;
_gridCount = 0;
_cooldown = 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;
}
if (_cooldown > 0)
{
_cooldown--;
_prevEma = emaVal;
if (Position == 0) return;
}
var close = candle.ClosePrice;
// Position management with grid and stops
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2.5m)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
_cooldown = 10;
}
else if (close <= _entryPrice - atrVal * 4m)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
_cooldown = 10;
}
else if (_gridCount < 1 && close <= _entryPrice - atrVal * 2m)
{
_entryPrice = (_entryPrice + close) / 2m;
_gridCount++;
BuyMarket();
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2.5m)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
_cooldown = 10;
}
else if (close >= _entryPrice + atrVal * 4m)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
_cooldown = 10;
}
else if (_gridCount < 1 && close >= _entryPrice + atrVal * 2m)
{
_entryPrice = (_entryPrice + close) / 2m;
_gridCount++;
SellMarket();
}
}
// Entry on EMA trend
if (Position == 0 && _cooldown == 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.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange, ExponentialMovingAverage
class amstell_sl_strategy(Strategy):
def __init__(self):
super(amstell_sl_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._ema_length = self.Param("EmaLength", 50) \
.SetDisplay("EMA Length", "EMA trend filter.", "Indicators")
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
self._cooldown = 0
@property
def CandleType(self):
return self._candle_type.Value
@property
def AtrLength(self):
return self._atr_length.Value
@property
def EmaLength(self):
return self._ema_length.Value
def OnStarted2(self, time):
super(amstell_sl_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
self._cooldown = 0
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self._ema, self.ProcessCandle).Start()
def ProcessCandle(self, candle, atr_val, ema_val):
if candle.State != CandleStates.Finished:
return
av = float(atr_val)
ev = float(ema_val)
if av <= 0 or self._prev_ema == 0:
self._prev_ema = ev
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_ema = ev
if self.Position == 0:
return
close = float(candle.ClosePrice)
# Position management with grid and stops
if self.Position > 0:
if close >= self._entry_price + av * 2.5:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
self._cooldown = 10
elif close <= self._entry_price - av * 4.0:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
self._cooldown = 10
elif self._grid_count < 1 and close <= self._entry_price - av * 2.0:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.BuyMarket()
elif self.Position < 0:
if close <= self._entry_price - av * 2.5:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
self._cooldown = 10
elif close >= self._entry_price + av * 4.0:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
self._cooldown = 10
elif self._grid_count < 1 and close >= self._entry_price + av * 2.0:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.SellMarket()
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_ema = ev
return
# Entry on EMA trend
if self.Position == 0 and self._cooldown == 0:
if close > ev and ev > self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.BuyMarket()
elif close < ev and ev < self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.SellMarket()
self._prev_ema = ev
def OnReseted(self):
super(amstell_sl_strategy, self).OnReseted()
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
self._cooldown = 0
def CreateClone(self):
return amstell_sl_strategy()