Ver no GitHub

Estratégia de escadas

A Estratégia de escadas reproduz o comportamento do especialista MetaTrader original. Ele começa colocando ordens de stop simétricas em torno do preço de venda atual e, em seguida, reconstrói continuamente a grade em torno do preenchimento mais recente. Os lucros são acumulados em etapas de preços (pips) sem ponderação por volume, exatamente como no script de origem. Quando uma meta de lucro é atingida, a estratégia liquida todas as posições por ordem de mercado, remove quaisquer stops pendentes e redefine a grade.

Lógica de negociação

  1. Quando nenhuma posição estiver aberta, coloque um stop de compra e um stop de venda a uma distância de ChannelSteps / 2 passos de preço acima e abaixo do preço de venda atual.
  2. Depois que a primeira ordem stop for preenchida, rearme a grade em torno do preço executado:
    • Se houver menos de duas ordens stop ativas, cancele as obsoletas.
    • Contanto que o preço de oferta atual permaneça dentro da metade da distância do canal da última entrada, coloque um novo stop de compra e um novo stop de venda a ChannelSteps de distância do preenchimento mais recente.
    • Quando AddLots estiver ativado, aumente o volume do pedido pendente no lote base após cada preenchimento.
  3. Mantenha duas listas contínuas com todas as entradas longas e curtas para reproduzir a cesta coberta usada pela versão MT4.
  4. Calcule o lucro não realizado da cesta em cada vela acabada usando a melhor oferta para posições compradas e a melhor oferta para posições vendidas. As distâncias são normalizadas pela etapa de preço do instrumento, refletindo o cálculo do ponto original.
  5. Acione uma liquidação total quando um dos limites for excedido:
    • ProfitSteps – lucro produzido apenas pelo símbolo atual.
    • CommonProfitSteps – lucro em toda a cesta.
  6. A liquidação envia ordens de mercado para fechar cada exposição longa e curta separadamente. As ordens stop pendentes são canceladas quando o cesto fica estável.

Nota: O especialista original anexou níveis de stop loss ao registrar ordens pendentes. StockSharp não suporta níveis de proteção por ordem através do API de alto nível, portanto a porta fecha negociações exclusivamente através da lógica baseada no lucro descrita acima.

Parâmetros

Parâmetro Descrição Padrão
ChannelSteps Distância (em passos de preço mínimo) entre as ordens stop simétricas. 1000
ProfitSteps Limite de lucro (em etapas) necessário para fechar a cesta local. 1500
CommonProfitSteps Limite de lucro global (em etapas) que força uma liquidação total. 1000
AddLots Quando ativado, aumenta o volume do próximo pedido pendente no lote base após cada preenchimento. true
BaseVolume Volume usado para o primeiro par de ordens stop. 0.1m
CandleType Prazo usado para assinaturas de velas e gerenciamento comercial. 1 minute

Notas de implementação

  • Usa o StockSharp API de alto nível com SubscribeCandles() e Bind() para processar apenas velas concluídas.
  • Rastreia entradas individuais dentro de OnOwnTradeReceived para que o cálculo do lucro possa imitar a lógica de hedge da versão MQL.
  • Os limites de lucro operam em distâncias puras de preços, sem multiplicar pelo volume executado, correspondendo à forma como o especialista MT4 somou os pips.
  • Todas as ordens de parada são criadas por meio de BuyStop e SellStop, enquanto as saídas são executadas com ordens de mercado para manter a lógica portátil entre provedores de dados.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stairs grid strategy: places trades at regular ATR-based intervals,
/// adding to position on trending moves, closing on profit target.
/// </summary>
public class StairsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _gridMultiplier;
	private readonly StrategyParam<int> _maxLayers;
	private readonly StrategyParam<decimal> _profitMultiplier;
	private readonly StrategyParam<int> _emaLength;

	private decimal _entryPrice;
	private decimal _lastGridPrice;
	private int _gridCount;
	private decimal _prevEma;

	public StairsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for grid step.", "Indicators");

		_gridMultiplier = Param(nameof(GridMultiplier), 1.5m)
			.SetDisplay("Grid Multiplier", "ATR multiplier for grid step.", "Grid");

		_maxLayers = Param(nameof(MaxLayers), 5)
			.SetDisplay("Max Layers", "Maximum grid layers.", "Grid");

		_profitMultiplier = Param(nameof(ProfitMultiplier), 2.0m)
			.SetDisplay("Profit Multiplier", "ATR multiplier for profit target.", "Grid");

		_emaLength = Param(nameof(EmaLength), 20)
			.SetDisplay("EMA Length", "EMA for trend direction.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public decimal GridMultiplier
	{
		get => _gridMultiplier.Value;
		set => _gridMultiplier.Value = value;
	}

	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	public decimal ProfitMultiplier
	{
		get => _profitMultiplier.Value;
		set => _profitMultiplier.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

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

		_entryPrice = 0;
		_lastGridPrice = 0;
		_gridCount = 0;
		_prevEma = 0;
	}

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

		var atr = new AverageTrueRange { Length = AtrLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal atrVal, decimal emaVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (atrVal <= 0 || _prevEma == 0)
		{
			_prevEma = emaVal;
			return;
		}

		var close = candle.ClosePrice;
		var gridStep = atrVal * GridMultiplier;
		var profitTarget = atrVal * ProfitMultiplier;

		// Check profit target
		if (Position > 0 && _entryPrice > 0)
		{
			if (close - _entryPrice >= profitTarget || close < emaVal)
			{
				SellMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (_entryPrice - close >= profitTarget || close > emaVal)
			{
				BuyMarket();
				_gridCount = 0;
				_entryPrice = 0;
				_lastGridPrice = 0;
			}
		}

		// Grid: add to winning direction
		if (Position > 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (close - _lastGridPrice >= gridStep)
			{
				BuyMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}
		else if (Position < 0 && _lastGridPrice > 0 && _gridCount < MaxLayers)
		{
			if (_lastGridPrice - close >= gridStep)
			{
				SellMarket();
				_lastGridPrice = close;
				_gridCount++;
			}
		}

		// Initial entry based on trend
		if (Position == 0)
		{
			var emaRising = emaVal > _prevEma;
			var emaFalling = emaVal < _prevEma;

			if (emaRising && close > emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (emaFalling && close < emaVal)
			{
				_entryPrice = close;
				_lastGridPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}