Ver no GitHub

Estratégia Amstell Gerenciador de Grid

Port de alto nível do expert MetaTrader "exp_Amstell-SL" que executa um grid de médias bidirecional. A estratégia rastreia o preço de execução mais recente em cada lado e emite ordens de mercado adicionais quando o preço se afasta o suficiente, enquanto liquida o lote aberto assim que uma distância fixa de take-profit ou stop-loss é atingida. A implementação usa as assinaturas de candles e os helpers de ordens de alto nível do StockSharp, portanto pode ser conectada a qualquer ambiente que forneça dados de candles para um único instrumento.

A lógica traduzida está ligeiramente adaptada para o modelo de portfólio líquido do StockSharp: os grids comprado e vendido ainda são gerenciados separadamente, mas não são mantidos ao mesmo tempo. O grid comprado está ativo enquanto a posição líquida é não negativa, e o grid vendido assume o controle somente após toda a exposição comprada ter sido zerada.

Como funciona

Fluxo de dados e execução

  • Assina o CandleType configurado (padrão: candles de período de 1 minuto) e processa apenas candles finalizados.
  • Calcula offsets baseados em pips a partir do PriceStep do instrumento. Se o passo tem 3 ou 5 casas decimais, é multiplicado por 10 para imitar o ajuste de pip de 3/5 dígitos do MetaTrader.
  • Todas as operações são colocadas através dos helpers BuyMarket/SellMarket; nenhuma ordem pendente é usada.

Gestão do lado comprado

  • Abre a primeira posição comprada (OrderVolume) assim que não há exposição comprada existente e a estratégia não está no processo de fechar vendidos.
  • Rastreia o preço de execução comprado mais recente e o preço de entrada ponderado por volume para o lote comprado ativo.
  • Coloca ordens compradas adicionais de tamanho OrderVolume sempre que o preço de fechamento caiu pelo menos BuyDistancePips (convertidos em unidades de preço) abaixo da última execução comprada.

Gestão do lado vendido

  • Assim que o lote comprado está completamente fechado e a posição líquida é não positiva, a estratégia permite entradas vendidas.
  • Coloca a ordem vendida inicial quando não há exposição vendida; vendidos adicionais são abertos depois que o preço sobe BuyDistancePips * SellDistanceMultiplier acima da execução vendida anterior.
  • Mantém o preço de execução vendido mais recente e o preço de entrada ponderado por volume para o lote vendido ativo.

Regras de saída

  • Para cada direção, calcula o lucro não realizado relativo à execução média.
  • Fecha todo o lote comprado com uma venda a mercado quando o lucro atinge TakeProfitPips pips ou o drawdown atinge StopLossPips pips.
  • Fecha todo o lote vendido com uma compra a mercado quando o lucro atinge TakeProfitPips pips ou o movimento adverso atinge StopLossPips pips.
  • Após a liquidação, todos os preços e volumes em cache são redefinidos para que um novo grid possa começar no próximo candle.

Diferenças em relação ao expert MQL original

  • A versão StockSharp opera em fechamentos de candles em vez de ticks individuais.
  • Os grids comprado e vendido são executados sequencialmente em vez de simultaneamente, correspondendo ao modo de netting padrão do StockSharp.
  • Todas as distâncias de proteção são verificadas contra o preço de entrada médio em vez de cada ticket individualmente, o que reflete o comportamento da posição líquida agregada.

Parâmetros

Parâmetro Padrão Faixa de otimização Descrição
OrderVolume 0.01 0.010.10 (passo 0.01) Quantidade enviada com cada ordem de grid. Deve ser positiva.
TakeProfitPips 30 10150 (passo 10) Alvo de lucro para o lote ativo expresso em pips.
StopLossPips 30 10150 (passo 10) Movimento adverso máximo antes de abandonar o lote.
BuyDistancePips 10 560 (passo 5) Queda mínima da última execução comprada para adicionar outra compra. Deve ser menor que TP e SL.
SellDistanceMultiplier 10 215 (passo 1) Multiplicador aplicado à distância comprada ao espaçar entradas vendidas.
CandleType Período de 1 minuto Série de candles usada para geração de sinais.

Notas de implementação

  • BuyDistancePips deve ser estritamente menor que TakeProfitPips e StopLossPips; a estratégia lança uma exceção na inicialização caso contrário, reproduzindo a validação do MetaTrader.
  • O tamanho do pip é derivado do PriceStep do instrumento. Ajuste os parâmetros se o instrumento usar um tamanho de tick não padrão.
  • Todo o estado interno é limpo em OnReseted, permitindo reiniciar a estratégia sem dados residuais do grid.
  • Nenhuma personalização de cor ou registro manual de indicadores é usado, correspondendo às diretrizes de API de alto nível neste repositório.
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;
	}
}