Ver en GitHub

Estrategia de escaleras

La Estrategia de escaleras reproduce el comportamiento del experto MetaTrader original. Comienza colocando órdenes stop simétricas alrededor del precio de venta actual y luego reconstruye continuamente la cuadrícula alrededor del llenado más reciente. Las ganancias se acumulan en incrementos de precio (pips) sin ponderación por volumen, exactamente como en el script original. Cuando se alcanza un objetivo de ganancias, la estrategia liquida todas las posiciones por orden de mercado, elimina las paradas pendientes y restablece la red.

Lógica comercial

  1. Cuando no haya posiciones abiertas, coloque un stop de compra y un stop de venta a una distancia de ChannelSteps / 2 pasos de precio por encima y por debajo del precio de venta actual.
  2. Después de que se complete la primera orden de parada, vuelva a armar la cuadrícula alrededor del precio ejecutado:
    • Si hay menos de dos órdenes stop activas, cancele las obsoletas.
    • Siempre que el precio de oferta actual se mantenga dentro de la mitad de la distancia del canal desde la última entrada, coloque un nuevo stop de compra y de venta a ChannelSteps del llenado más reciente.
    • Cuando AddLots esté habilitado, aumente el volumen de pedidos pendientes en el lote base después de cada ejecución.
  3. Mantenga dos listas actualizadas con todas las entradas largas y cortas para reproducir la cesta cubierta utilizada por la versión MT4.
  4. Calcule el beneficio no realizado de la cesta en cada vela terminada utilizando la mejor oferta para posiciones largas y la mejor demanda para posiciones cortas. Las distancias se normalizan según el paso del precio del instrumento, reflejando el cálculo de puntos original.
  5. Activar una liquidación total cuando se supere cualquiera de los umbrales:
    • ProfitSteps – beneficio producido únicamente por el símbolo actual.
    • CommonProfitSteps – beneficio en toda la cesta.
  6. La liquidación envía órdenes de mercado para cerrar cada exposición larga y corta por separado. Las órdenes stop pendientes se cancelan una vez que la cesta está plana.

Nota: El experto original adjuntó niveles de stop-loss al registrar órdenes pendientes. StockSharp no admite niveles de protección por orden a través del nivel alto API, por lo tanto, el puerto cierra operaciones exclusivamente a través de la lógica basada en ganancias descrita anteriormente.

Parámetros

Parámetro Descripción Predeterminado
ChannelSteps Distancia (en pasos de precio mínimo) entre las órdenes stop simétricas. 1000
ProfitSteps Umbral de ganancia (en pasos) requerido para cerrar la canasta local. 1500
CommonProfitSteps Umbral de beneficio global (en pasos) que obliga a una liquidación total. 1000
AddLots Cuando esté habilitado, aumente el siguiente volumen de pedido pendiente en el lote base después de cada ejecución. true
BaseVolume Volumen utilizado para el primer par de órdenes stop. 0.1m
CandleType Plazo utilizado para las suscripciones de velas y la gestión comercial. 1 minute

Notas de implementación

  • Utiliza la API de alto nivel de StockSharp con SubscribeCandles() y Bind() para procesar velas terminadas únicamente.
  • Realiza un seguimiento de las entradas individuales dentro de OnOwnTradeReceived para que el cálculo de ganancias pueda imitar la lógica de cobertura de la versión MQL.
  • Los umbrales de ganancias operan en distancias puras de precio-escalón, sin multiplicarse por el volumen ejecutado, coincidiendo con la forma en que el experto de MT4 sumó los pips.
  • Todas las órdenes stop se crean a través de BuyStop y SellStop, mientras que las salidas se ejecutan con órdenes de mercado para mantener la lógica portátil entre los proveedores de datos.
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;
	}
}