Ver en GitHub

Estrategia Amstell Gestor de Grid

Port de alto nivel del expert de MetaTrader "exp_Amstell-SL" que ejecuta un grid de promedios bidireccional. La estrategia realiza un seguimiento del precio de fill más reciente en cada lado y emite órdenes de mercado adicionales cuando el precio se aleja suficientemente, mientras liquida el lote abierto una vez que se alcanza una distancia fija de take-profit o stop-loss. La implementación usa las suscripciones de candles y los helpers de órdenes de alto nivel de StockSharp, por lo que se puede conectar a cualquier entorno que proporcione datos de candles para un único instrumento.

La lógica traducida está ligeramente adaptada para el modelo de portafolio neto de StockSharp: los grids largo y corto aún se gestionan por separado, pero no se mantienen al mismo tiempo. El grid largo está activo mientras la posición neta es no negativa, y el grid corto toma el control solo después de que toda la exposición larga ha sido aplanada.

Cómo funciona

Flujo de datos y ejecución

  • Se suscribe al CandleType configurado (predeterminado: candles de marco temporal de 1 minuto) y procesa solo candles finalizados.
  • Calcula offsets basados en pips desde el PriceStep del instrumento. Si el paso tiene 3 o 5 decimales, se multiplica por 10 para imitar el ajuste de pip de 3/5 dígitos de MetaTrader.
  • Todas las operaciones se colocan a través de helpers BuyMarket/SellMarket; no se usan órdenes pendientes.

Gestión del lado largo

  • Abre la primera posición larga (OrderVolume) tan pronto como no haya exposición larga existente y la estrategia no esté cerrando cortos.
  • Rastrea el precio de fill largo más reciente y el precio de entrada ponderado por volumen para el lote largo activo.
  • Coloca órdenes largas adicionales de tamaño OrderVolume siempre que el precio de cierre haya caído al menos BuyDistancePips (convertidos a unidades de precio) por debajo del último fill largo.

Gestión del lado corto

  • Una vez que el lote largo está completamente cerrado y la posición neta es no positiva, la estrategia permite entradas cortas.
  • Coloca la orden corta inicial cuando no hay exposición corta; shorts adicionales se abren después de que el precio sube BuyDistancePips * SellDistanceMultiplier por encima del fill corto anterior.
  • Mantiene el precio de fill corto más reciente y el precio de entrada ponderado por volumen para el lote corto activo.

Reglas de salida

  • Para cada dirección, calcula la ganancia no realizada relativa al fill promedio.
  • Cierra todo el lote largo con una venta a mercado cuando la ganancia alcanza TakeProfitPips pips o el drawdown alcanza StopLossPips pips.
  • Cierra todo el lote corto con una compra a mercado cuando la ganancia alcanza TakeProfitPips pips o el movimiento adverso alcanza StopLossPips pips.
  • Después de la liquidación, todos los precios y volúmenes en caché se restablecen para que un nuevo grid pueda comenzar en el próximo candle.

Diferencias con el expert MQL original

  • La versión StockSharp opera en cierres de candles en lugar de ticks individuales.
  • Los grids largo y corto se ejecutan secuencialmente en lugar de simultáneamente, coincidiendo con el modo de netting predeterminado de StockSharp.
  • Todas las distancias de protección se verifican contra el precio de entrada promedio en lugar de cada ticket individualmente, lo que refleja el comportamiento de posición neta agregada.

Parámetros

Parámetro Predeterminado Rango de optimización Descripción
OrderVolume 0.01 0.010.10 (paso 0.01) Cantidad enviada con cada orden de grid. Debe ser positiva.
TakeProfitPips 30 10150 (paso 10) Objetivo de ganancia para el lote activo expresado en pips.
StopLossPips 30 10150 (paso 10) Movimiento adverso máximo antes de abandonar el lote.
BuyDistancePips 10 560 (paso 5) Caída mínima desde el último fill largo para agregar otra compra. Debe ser menor que TP y SL.
SellDistanceMultiplier 10 215 (paso 1) Multiplicador aplicado a la distancia larga al espaciar entradas cortas.
CandleType Marco temporal de 1 minuto Serie de candles usada para generar señales.

Notas de implementación

  • BuyDistancePips debe ser estrictamente menor que TakeProfitPips y StopLossPips; la estrategia lanza una excepción al inicio en caso contrario, reproduciendo la validación de MetaTrader.
  • El tamaño de pip se deriva del PriceStep del instrumento. Ajuste los parámetros si el instrumento usa un tamaño de tick no estándar.
  • Todo el estado interno se limpia en OnReseted, permitiendo reiniciar la estrategia sin datos residuales del grid.
  • No se usa personalización de colores ni registro manual de indicadores, cumpliendo con las directrices de la API de alto nivel en este repositorio.
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;
	}
}