Стратегия Amstell Grid Manager
Высокоуровневый порт эксперта MetaTrader "exp_Amstell-SL", реализующий двухсторонний усредняющий сеточный алгоритм. Стратегия хранит последние цены исполнения по каждой стороне и докупает/допродаёт позицию при отклонении цены на заданное расстояние, закрывая весь блок ордеров после достижения фиксированного тейк-профита или стоп-лосса. Реализация использует подписку на свечи и вспомогательные методы высокоуровневого API StockSharp, поэтому её можно подключить к любому источнику свечных данных по одному инструменту.
Перенос адаптирован к неттинговой модели StockSharp: длинная и короткая сетки управляются раздельно, но не удерживаются одновременно. Пока совокупная позиция не отрицательная, активна длинная сетка; после полного закрытия лонга управление передаётся короткой стороне.
Как работает стратегия
Поток данных и исполнение
- Подписывается на свечи типа
CandleType (по умолчанию тайм-фрейм 1 минута) и обрабатывает только завершённые свечи.
- Пересчитывает точки (pips) из
PriceStep. Если шаг цены имеет 3 или 5 знаков после запятой, он дополнительно умножается на 10 — аналогично оригинальной проверке MetaTrader для инструментов с 3/5-значной точкой.
- Все сделки выполняются рыночными ордерами через
BuyMarket/SellMarket, отложенные заявки не используются.
Управление длинной сеткой
- Открывает первичный лонг объёмом
OrderVolume, когда нет существующего длинного экспозиционного блока и не выполняется принудительное закрытие шорта.
- Сохраняет последнюю цену покупки и средневзвешенную цену входа для текущего длинного блока.
- Добавляет очередной лонг тем же объёмом, если цена закрытия опустилась как минимум на
BuyDistancePips (в пересчёте на цену) ниже последнего исполнения.
Управление короткой сеткой
- Активируется только после полного закрытия длинного блока, когда чистая позиция не положительная.
- Открывает первый шорт, если нет активных коротких позиций; последующие шорты добавляются при росте цены на
BuyDistancePips * SellDistanceMultiplier выше последнего короткого исполнения.
- Аналогично хранит последнюю цену продажи и средневзвешенную цену входа короткого блока.
Правила закрытия
- Для каждого направления рассчитывает нереализованную прибыль относительно средневзвешенной цены входа.
- Закрывает весь длинный блок рыночной продажей при достижении
TakeProfitPips пунктов прибыли либо StopLossPips пунктов просадки.
- Закрывает весь короткий блок рыночной покупкой при тех же расстояниях по прибыли/убытку.
- После ликвидации обнуляет накопленные цены и объёмы, чтобы следующий блок стартовал «с чистого листа».
Отличия от оригинального эксперта MQL
- Логика основана на закрытиях свечей, а не на тиковых котировках.
- Лонг и шорт выполняются последовательно из-за неттинговой схемы StockSharp.
- Стопы и тейки проверяются относительно средневзвешенной цены блока, а не для каждой заявки отдельно.
Параметры
| Параметр |
Значение по умолчанию |
Диапазон оптимизации |
Описание |
OrderVolume |
0.01 |
0.01 – 0.10 (шаг 0.01) |
Объём каждой сеточной заявки. Должен быть положительным. |
TakeProfitPips |
30 |
10 – 150 (шаг 10) |
Цель по прибыли для текущего блока в пунктах. |
StopLossPips |
30 |
10 – 150 (шаг 10) |
Максимально допустимое противодвижение для блока. |
BuyDistancePips |
10 |
5 – 60 (шаг 5) |
Минимальное отклонение вниз перед добором лонга. Должно быть меньше тейка и стопа. |
SellDistanceMultiplier |
10 |
2 – 15 (шаг 1) |
Во сколько раз расстояние для шорта превышает длинное расстояние. |
CandleType |
тайм-фрейм 1 минута |
— |
Тип свечей, по которым выполняются расчёты. |
Особенности реализации
- При запуске проверяется условие
BuyDistancePips < TakeProfitPips и BuyDistancePips < StopLossPips; при нарушении генерируется исключение, как и в исходном советнике.
- Размер пункта вычисляется по
PriceStep; при нетипичном шаге цены стоит пересмотреть параметры.
- Метод
OnReseted полностью очищает внутренние состояния, чтобы повторный запуск начинался без остаточных данных.
- Код соответствует стилевым требованиям репозитория: используется только высокоуровневый API, без явной окраски графика и без ручной регистрации индикаторов.
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()