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.01 – 0.10 (passo 0.01) Quantidade enviada com cada ordem de grid. Deve ser positiva.
TakeProfitPips 30 10 – 150 (passo 10) Alvo de lucro para o lote ativo expresso em pips.
StopLossPips 30 10 – 150 (passo 10) Movimento adverso máximo antes de abandonar o lote.
BuyDistancePips 10 5 – 60 (passo 5) Queda mínima da última execução comprada para adicionar outra compra. Deve ser menor que TP e SL.
SellDistanceMultiplier 10 2 – 15 (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;
	}
}