Ver no GitHub

Estratégia de Grade Amstell

A Estratégia de Grade Amstell é um port em C# do consultor especialista MetaTrader 5 exp_Amstell.mq5. Ela cria uma grade simétrica de compra/venda e aplica um take profit virtual a entradas individuais. A conversão segue as diretrizes da API de alto nível do StockSharp e substitui o processamento de ticks pelo processamento de candles enquanto mantém a ideia original intacta.

Como funciona

  1. Inicialização

    • A estratégia assina o tipo de candle configurado e inicia a proteção de posição.
    • Um tamanho de pip ajustado é calculado a partir do PriceStep do ativo e da precisão decimal. Símbolos de cinco dígitos e três dígitos recebem automaticamente um multiplicador de 10x, espelhando a implementação MT5.
  2. Primeira negociação

    • Quando os preços de compra e venda registrados estão vazios (lançamento inicial), uma ordem de compra a mercado é enviada imediatamente. Isso inicializa a grade exatamente como o consultor especialista original.
  3. Expansão da grade

    • Uma nova ordem de compra é emitida sempre que o preço de fechamento atual está pelo menos StepPips abaixo do último preço de compra registrado.
    • Uma nova ordem de venda é emitida sempre que o preço está pelo menos StepPips acima do último preço de venda registrado.
    • A estratégia rastreia internamente pilhas separadas de comprados e vendidos para que ordens alternadas possam coexistir mesmo em uma conta de netting. Ordens opostas primeiro reduzem a outra pilha antes de adicionar nova exposição, reproduzindo o comportamento de hedge da versão MT5.
  4. Take Profit virtual

    • Cada posição comprada aberta é monitorada de forma independente. Quando o preço avança TakeProfitPips, uma venda a mercado é enviada apenas pelo volume dessa posição.
    • Cada posição vendida aberta é tratada de forma similar na direção oposta. O take profit é "virtual" porque as posições são fechadas programaticamente sem usar ordens TP do lado do broker.
    • Após uma direção ter sido totalmente fechada enquanto o lado oposto ainda existe, o preço do último negócio correspondente é limpo para que a próxima ordem nessa direção possa disparar imediatamente, assim como no código original.
  5. Rastreamento de estado

    • O manipulador OnOwnTradeReceived reconstrói as pilhas de comprados/vendidos a partir de negociações executadas, permitindo que preenchimentos parciais e reversões sejam tratados com elegância.
    • Os últimos preços de compra/venda permanecem em cache quando ambos os lados estão zerados para que a grade aguarde o passo necessário antes de re-entrar após um reset completo.

Parâmetros

Parâmetro Padrão Descrição
Volume 0.1 Tamanho da ordem usado para cada ordem a mercado em ambas as direções.
TakeProfitPips 50 Distância em pips que deve ser ganha antes que uma posição individual seja fechada.
StepPips 15 Lacuna em pips entre ordens de grade consecutivas da mesma direção.
CandleType 1 Minute Fonte de dados de candles usada para aproximar a lógica baseada em ticks.

Todas as configurações baseadas em pips respeitam o passo de preço e a precisão do instrumento. Por exemplo, no EURUSD (5 dígitos) StepPips = 15 corresponde a 0,0015.

Notas práticas

  • A estratégia usa preços de fechamento de candles para emular as comparações em nível de tick encontradas no código MT5. Para operações de alta frequência, diminua o período.
  • Não existe stop-loss por padrão. Como em qualquer abordagem de grade, tendências descontroladas podem acumular grande exposição. Use volumes conservadores e considere supervisão baseada em sessões.
  • Como os take profits são tratados virtualmente, as negociações fechadas são imediatamente refletidas no PnL da estratégia sem colocar ordens TP visíveis no broker.
  • A implementação deixa os últimos preços em cache inalterados após ambos os lados serem zerados. Isso preserva o comportamento original onde a grade aguarda o deslocamento de preço antes de reiniciar.

Arquivos

  • CS/AmstellGridStrategy.cs – Implementação da estratégia StockSharp com extensos comentários em linha.
  • README.md, README_ru.md, README_zh.md – Documentação completa em inglês, russo e chinês.

Este port está pronto para personalização adicional (por exemplo, gerenciamento de dinheiro, limites de risco) diretamente dentro do ecossistema 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;
	}
}