Ver en GitHub

Estrategia Renko Level EA

Descripción general

  • Convertida del asesor experto de MetaTrader Renko Level EA.mq5.
  • Emula el indicador original manteniendo un nivel Renko superior e inferior derivado del parámetro BrickSize.
  • Evalúa velas finalizadas proporcionadas por CandleType (predeterminado: marco temporal de 1 minuto) y reacciona cuando la cuadrícula Renko se desplaza.
  • No usa stops ni objetivos fijos; cada salida ocurre a través de una señal opuesta.

Lógica de trading

  1. En la primera vela finalizada el precio de cierre se redondea a la cuadrícula Renko para inicializar los niveles superior e inferior.
  2. Para cada vela subsiguiente:
    • Si el cierre permanece entre los límites actuales, la cuadrícula permanece sin cambios.
    • Un cierre por encima del nivel superior eleva el bloque Renko hacia arriba al siguiente valor de cuadrícula.
    • Un cierre por debajo del nivel inferior empuja el bloque hacia abajo.
  3. Un cambio en el nivel Renko superior se interpreta como un rompimiento direccional.
    • Nivel superior creciente → señal alcista (a menos que ReverseSignals esté habilitado).
    • Nivel superior decreciente → señal bajista.
  4. Las señales pueden opcionalmente invertirse (ReverseSignals) o piramidarse (AllowIncrease) para coincidir con el comportamiento del EA original.

Gestión de órdenes

  • Antes de entrar largo, cualquier posición corta se cierra; lo opuesto ocurre antes de entrar corto.
  • Cuando AllowIncrease = false, la estrategia abre un nuevo trade solo si no existe ninguna posición en esa dirección.
  • Cuando AllowIncrease = true, se permiten órdenes adicionales de tamaño OrderVolume incluso si una posición ya está abierta.
  • No hay stop-loss ni take-profit dedicados; los reversales de posición sirven como mecanismo de salida.
  • StartProtection() se invoca una vez para mantener las salvaguardas de riesgo alineadas con el framework base.

Parámetros

Nombre Descripción Predeterminado Optimizable
BrickSize Tamaño del bloque Renko medido como múltiplos de Security.PriceStep. Define cuánto debe moverse el precio para desplazar la cuadrícula. 30 Sí (10 → 100 paso 10)
OrderVolume Volumen enviado con cada orden de mercado. 1 No
ReverseSignals Invierte las acciones alcistas y bajistas. Refleja la entrada Reverse del EA. false No
AllowIncrease Permite añadir a una posición existente en lugar de esperar una posición plana. Refleja el indicador Increase del EA. false No
CandleType Fuente de velas usada para los cálculos. Predeterminado a velas de marco temporal de 1 minuto, pero se puede suministrar cualquier serie soportada. TimeFrameCandleMessage(1m) No

Notas prácticas

  • BrickSize se adapta automáticamente al instrumento negociado porque multiplica el PriceStep definido por la bolsa.
  • La decisión se basa puramente en precios de cierre; los movimientos intrabarra importan solo cuando forman el cierre final.
  • Combinar ReverseSignals y AllowIncrease permite probar variantes contratendencia y de piramidación del EA.
  • Funciona en cualquier mercado donde la lógica de rompimiento estilo Renko es relevante, incluyendo forex, futuros e instrumentos cripto.

Clasificación

  • Régimen: Seguimiento de tendencia (rompimiento Renko).
  • Dirección: Largo/Corto.
  • Complejidad: Moderado (seguimiento de niveles personalizado, ajuste mínimo).
  • Stops: Ninguno; salidas en señales inversas.
  • Marco temporal: Configurable mediante CandleType.
  • Indicadores: Proyección de nivel Renko personalizada.
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>
/// Strategy that mirrors the Renko Level Expert Advisor from MetaTrader.
/// Tracks level changes generated by a Renko style grid and flips positions accordingly.
/// </summary>
public class RenkoLevelEaStrategy : Strategy
{
	private readonly StrategyParam<int> _brickSize;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<bool> _allowIncrease;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _upperLevel;
	private decimal _lowerLevel;
	private decimal? _previousUpperLevel;
	private bool _levelsInitialized;

	/// <summary>
	/// Renko brick size expressed in price steps.
	/// </summary>
	public int BrickSize
	{
		get => _brickSize.Value;
		set => _brickSize.Value = value;
	}

	/// <summary>
	/// Volume for each executed market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// When enabled, long and short signals are swapped.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Allows adding to an existing position instead of waiting for a flat position.
	/// </summary>
	public bool AllowIncrease
	{
		get => _allowIncrease.Value;
		set => _allowIncrease.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate price movement.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="RenkoLevelEaStrategy"/>.
	/// </summary>
	public RenkoLevelEaStrategy()
	{
		_brickSize = Param(nameof(BrickSize), 3000)
			.SetGreaterThanZero()
			.SetDisplay("Brick Size", "Renko block size in price steps", "Renko Levels")
			
			.SetOptimize(10, 100, 10);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume for market orders", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short actions", "Trading");

		_allowIncrease = Param(nameof(AllowIncrease), false)
			.SetDisplay("Allow Increase", "Allow adding to existing positions", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for calculations", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		// Reset previously calculated Renko levels.
		_upperLevel = 0m;
		_lowerLevel = 0m;
		_previousUpperLevel = null;
		_levelsInitialized = false;
	}

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

		// Subscribe to candle data that feeds the Renko level logic.
		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(ProcessCandle)
			.Start();

		// Draw prices and trades if a chart is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

		// Enable built-in protection features.
		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Trade only on completed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure the strategy is ready to place trades.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		if (priceStep <= 0)
			priceStep = 1m;

		// Update Renko bounds with the latest closing price.
		if (!UpdateLevels(candle.ClosePrice, priceStep))
			return;

		// Skip the very first signal to mirror indicator warm-up.
		if (_previousUpperLevel == null)
		{
			_previousUpperLevel = _upperLevel;
			return;
		}

		// Proceed only if the Renko level actually changed.
		if (AreEqual(_previousUpperLevel.Value, _upperLevel, priceStep))
			return;

		var isUpMove = _upperLevel > _previousUpperLevel.Value;

		if (ReverseSignals)
			isUpMove = !isUpMove;

		if (isUpMove)
			HandleLongSignal();
		else
			HandleShortSignal();

		_previousUpperLevel = _upperLevel;
	}

	private bool UpdateLevels(decimal closePrice, decimal priceStep)
	{
		var stepCount = BrickSize;
		if (stepCount <= 0)
			return false;

		if (!_levelsInitialized)
		{
			CalculateBounds(closePrice, priceStep, stepCount, out var ceil, out var round, out var floor);
			_upperLevel = round;
			_lowerLevel = floor;
			_levelsInitialized = true;
			return true;
		}

		if (closePrice >= _lowerLevel && closePrice <= _upperLevel)
			return false;

		CalculateBounds(closePrice, priceStep, stepCount, out var newCeil, out var newRound, out var newFloor);

		if (closePrice < _lowerLevel)
		{
			if (AreEqual(newRound, _lowerLevel, priceStep))
				return false;

			_upperLevel = newCeil;
			_lowerLevel = newRound;
			return true;
		}

		if (closePrice > _upperLevel)
		{
			if (AreEqual(newRound, _upperLevel, priceStep))
				return false;

			_lowerLevel = newFloor;
			_upperLevel = newRound;
			return true;
		}

		return false;
	}

	private void CalculateBounds(decimal price, decimal priceStep, int stepCount, out decimal priceCeil, out decimal priceRound, out decimal priceFloor)
	{
		var normalizedStep = (decimal)stepCount;

		var ratio = price / priceStep / normalizedStep;
		var rounded = Math.Round(ratio, MidpointRounding.AwayFromZero);

		priceRound = (decimal)rounded * normalizedStep * priceStep;

		var ceilRatio = (priceRound + normalizedStep / 2m * priceStep) / priceStep / normalizedStep;
		var ceilCount = Math.Ceiling((double)ceilRatio);
		priceCeil = (decimal)ceilCount * normalizedStep * priceStep;

		var floorRatio = (priceRound - normalizedStep / 2m * priceStep) / priceStep / normalizedStep;
		var floorCount = Math.Floor((double)floorRatio);
		priceFloor = (decimal)floorCount * normalizedStep * priceStep;
	}

	private bool AreEqual(decimal left, decimal right, decimal priceStep)
	{
		var tolerance = priceStep / 2m;
		return Math.Abs(left - right) <= tolerance;
	}

	private void HandleLongSignal()
	{
		// Close the short side before flipping to long.
		if (Position < 0)
			ClosePosition();

		// Respect the increase toggle to avoid stacking positions unintentionally.
		if (!AllowIncrease && Position > 0)
			return;

		BuyMarket(OrderVolume);
	}

	private void HandleShortSignal()
	{
		// Close the long side before flipping to short.
		if (Position > 0)
			ClosePosition();

		// Respect the increase toggle for short accumulation.
		if (!AllowIncrease && Position < 0)
			return;

		SellMarket(OrderVolume);
	}
}