Ver en GitHub

Estrategia de Cuadrícula Amstell

La Estrategia de Cuadrícula Amstell es un puerto en C# del asesor experto de MetaTrader 5 exp_Amstell.mq5. Crea una cuadrícula simétrica de compra/venta y aplica un take profit virtual a las entradas individuales. La conversión sigue las pautas de la API de alto nivel de StockSharp y reemplaza el manejo de ticks con procesamiento de velas mientras mantiene intacta la idea original.

Cómo funciona

  1. Inicialización

    • La estrategia se suscribe al tipo de vela configurado e inicia la protección de posición.
    • Se calcula un tamaño de pip ajustado a partir del PriceStep del instrumento y la precisión decimal. Los símbolos de cinco dígitos y tres dígitos reciben automáticamente un multiplicador de 10x, reflejando la implementación de MT5.
  2. Primera operación

    • Cuando los precios de compra y venta registrados están vacíos (lanzamiento inicial), se envía inmediatamente una orden de compra a mercado. Esto arranca la cuadrícula exactamente como el asesor experto original.
  3. Expansión de la cuadrícula

    • Se emite una nueva compra cada vez que el precio de cierre actual está al menos StepPips por debajo del último precio de compra registrado.
    • Se emite una nueva venta cada vez que el precio está al menos StepPips por encima del último precio de venta registrado.
    • La estrategia rastrea internamente pilas separadas de largos y cortos para que las órdenes alternadas puedan coexistir incluso en una cuenta de netting. Las órdenes opuestas primero reducen la otra pila antes de agregar nueva exposición, reproduciendo el comportamiento de cobertura de la versión MT5.
  4. Take Profit virtual

    • Cada largo abierto se monitorea de forma independiente. Cuando el precio avanza TakeProfitPips, se envía una venta a mercado solo por el volumen de esa posición.
    • Cada corto abierto se trata de manera similar en la dirección opuesta. El take profit es "virtual" porque las posiciones se cierran programáticamente sin usar órdenes TP del lado del broker.
    • Después de que una dirección se haya cerrado completamente mientras el lado opuesto aún existe, el precio del último trato correspondiente se limpia para que la siguiente orden en esa dirección pueda dispararse inmediatamente, igual que en el código original.
  5. Seguimiento del estado

    • El controlador OnOwnTradeReceived reconstruye las pilas de largos/cortos a partir de las operaciones ejecutadas, permitiendo manejar llenados parciales y reversiones con elegancia.
    • Los últimos precios de compra/venta permanecen en caché cuando ambos lados están planos para que la cuadrícula espere el paso requerido antes de volver a entrar después de un reinicio completo.

Parámetros

Parámetro Predeterminado Descripción
Volume 0.1 Tamaño de orden utilizado para cada orden de mercado en ambas direcciones.
TakeProfitPips 50 Distancia en pips que debe ganarse antes de que se cierre una posición individual.
StepPips 15 Brecha en pips entre órdenes de cuadrícula consecutivas de la misma dirección.
CandleType 1 Minute Fuente de datos de velas usada para aproximar la lógica basada en ticks.

Todas las configuraciones basadas en pips respetan el paso de precio y precisión del instrumento. Por ejemplo, en EURUSD (5 dígitos) StepPips = 15 corresponde a 0.0015.

Notas prácticas

  • La estrategia usa precios de cierre de velas para emular las comparaciones a nivel de tick que se encuentran en el código MT5. Para operaciones de alta frecuencia, reduzca el marco temporal.
  • No existe stop-loss por defecto. Como con cualquier enfoque de cuadrícula, las tendencias desbocadas pueden acumular gran exposición. Use volúmenes conservadores y considere la supervisión basada en sesiones.
  • Debido a que los take profits se manejan virtualmente, las operaciones cerradas se reflejan inmediatamente en el PnL de la estrategia sin colocar órdenes TP visibles en el broker.
  • La implementación deja los últimos precios en caché sin cambios después de que ambos lados se aplanen. Esto preserva el comportamiento original donde la cuadrícula espera el desplazamiento de precio antes de reiniciarse.

Archivos

  • CS/AmstellGridStrategy.cs – Implementación de la estrategia StockSharp con extensos comentarios en línea.
  • README.md, README_ru.md, README_zh.md – Documentación completa en inglés, ruso y chino.

Este puerto está listo para mayor personalización (por ejemplo, gestión de dinero, límites de riesgo) directamente dentro del ecosistema StockSharp.

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>
/// Grid strategy that alternates buy and sell entries with a virtual take profit.
/// </summary>
public class AmstellGridStrategy : Strategy
{
	private sealed class PositionEntry
	{
		public PositionEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; set; }

		public decimal Volume { get; set; }

		public bool IsClosing { get; set; }
	}

	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<PositionEntry> _longEntries = new();
	private readonly List<PositionEntry> _shortEntries = new();

	private decimal? _lastBuyPrice;
	private decimal? _lastSellPrice;
	private bool _hasInitialOrder;
	private decimal _pipSize;


	/// <summary>
	/// Virtual take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Distance between consecutive entries in pips.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Candle type used to generate trade decisions.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="AmstellGridStrategy"/> class.
	/// </summary>
	public AmstellGridStrategy()
	{

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Virtual take profit distance", "Risk")
			
			.SetOptimize(10, 150, 10);

		_stepPips = Param(nameof(StepPips), 15)
			.SetGreaterThanZero()
			.SetDisplay("Step (pips)", "Distance between grid entries", "Grid")
			
			.SetOptimize(5, 60, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for signal candles", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_longEntries.Clear();
		_shortEntries.Clear();
		_lastBuyPrice = null;
		_lastSellPrice = null;
		_hasInitialOrder = false;
		_pipSize = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only react to completed candles to emulate stable tick processing.
		if (candle.State != CandleStates.Finished)
			return;


		var price = candle.ClosePrice;
		var stepDistance = GetStepDistance();
		var takeProfitDistance = GetTakeProfitDistance();

		// Bootstrap the grid exactly like the MQL version.
		if (!_hasInitialOrder && _lastBuyPrice is null && _lastSellPrice is null)
		{
			BuyMarket(Volume);
			_hasInitialOrder = true;
			return;
		}

		// Check whether the grid should add a new long layer.
		if (CanOpenBuy(price, stepDistance))
		{
			BuyMarket(Volume);
			return;
		}

		// Mirror logic for the short side of the grid.
		if (CanOpenSell(price, stepDistance))
		{
			SellMarket(Volume);
			return;
		}

		// No new entries were placed, so check for virtual take-profit exits.
		if (TryClosePositions(price, takeProfitDistance))
			return;
	}

	private bool CanOpenBuy(decimal price, decimal stepDistance)
	{
		if (Volume <= 0)
			return false;

		return !_lastBuyPrice.HasValue || _lastBuyPrice.Value - price >= stepDistance;
	}

	private bool CanOpenSell(decimal price, decimal stepDistance)
	{
		if (Volume <= 0)
			return false;

		return !_lastSellPrice.HasValue || price - _lastSellPrice.Value >= stepDistance;
	}

	private bool TryClosePositions(decimal price, decimal takeProfitDistance)
	{
		if (takeProfitDistance <= 0)
			return false;

		// Evaluate longs first because the original EA does the same.
		foreach (var entry in _longEntries)
		{
			if (entry.IsClosing)
				continue;

			if (price - entry.Price >= takeProfitDistance)
			{
				// Prevent duplicate closing requests until the trade is processed.
				entry.IsClosing = true;
				SellMarket(entry.Volume);
				return true;
			}
		}

		// Short entries use the symmetrical distance check.
		foreach (var entry in _shortEntries)
		{
			if (entry.IsClosing)
				continue;

			if (entry.Price - price >= takeProfitDistance)
			{
				entry.IsClosing = true;
				BuyMarket(entry.Volume);
				return true;
			}
		}

		return false;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Order == null || trade.Order.Security != Security)
			return;

		var volume = trade.Trade.Volume;

		// Feed the executed trade into the synthetic short stack first.
		if (trade.Order.Side == Sides.Buy)
		{
			var remainder = ReduceEntries(_shortEntries, volume);

			if (remainder > 0)
			{
				// Remaining volume becomes a new long layer.
				_longEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
				_lastBuyPrice = trade.Trade.Price;
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			var remainder = ReduceEntries(_longEntries, volume);

			if (remainder > 0)
			{
				// Remaining volume becomes a new short layer.
				_shortEntries.Add(new PositionEntry(trade.Trade.Price, remainder));
				_lastSellPrice = trade.Trade.Price;
			}
		}

		// Recalculate helper state after rebuilding the stacks.
		UpdateLastPrices();
	}

	private decimal ReduceEntries(List<PositionEntry> entries, decimal volume)
	{
		var remaining = volume;

		// Consume volume using a FIFO approach just like MT5 positions.
		while (remaining > 0 && entries.Count > 0)
		{
			var entry = entries[0];
			var used = Math.Min(entry.Volume, remaining);
			entry.Volume -= used;
			remaining -= used;

			if (entry.Volume <= 0)
			{
				// Entry fully closed, remove it from the stack.
				entries.RemoveAt(0);
			}
			else
			{
				// Partial reduction keeps the entry alive; clear closing flag.
				entry.IsClosing = false;
			}
		}

		return remaining;
	}

	private void UpdateLastPrices()
	{
		// If only shorts remain, unlock the buy grid for immediate reuse.
		if (_longEntries.Count == 0 && _shortEntries.Count > 0)
		{
			_lastBuyPrice = null;
		}

		// If only longs remain, clear the last sell price to mimic MT5 logic.
		if (_shortEntries.Count == 0 && _longEntries.Count > 0)
		{
			_lastSellPrice = null;
		}

		// Any surviving entries should be marked as active again.
		for (var i = 0; i < _longEntries.Count; i++)
		{
			_longEntries[i].IsClosing = false;
		}

		for (var i = 0; i < _shortEntries.Count; i++)
		{
			_shortEntries[i].IsClosing = false;
		}
	}

	private decimal GetStepDistance()
	{
		var pip = _pipSize;
		if (pip <= 0)
		{
			// Fallback to the raw price step if the pip size has not been initialized yet.
			pip = Security?.PriceStep ?? 1m;
		}

		return StepPips * pip;
	}

	private decimal GetTakeProfitDistance()
	{
		var pip = _pipSize;
		if (pip <= 0)
		{
			// Same fallback logic as the step distance.
			pip = Security?.PriceStep ?? 1m;
		}

		return TakeProfitPips * pip;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0)
			step = 1m;

		return step;
	}
}