Ver en GitHub

Estrategia de Rompimiento 20PRExp-3

La estrategia 20PRExp-3 es un sistema de rompimiento que compara la sesión de trading actual con los extremos de precio del día anterior. Reconstruye el canal diario en cada vela de cinco minutos completada, confirma el momentum con una expansión del volumen de ticks de 30 minutos, y entra solo cuando el precio rompe más allá del máximo o mínimo de sesión actualizado. Una vez en el mercado, imita al experto original de MetaTrader 5 usando salidas de Parabolic SAR, stops trailing dinámicos y dimensionamiento fijo de riesgo basado en la distancia al stop de protección.

Concepto

  • Canal diario: Rastrear el máximo en ejecución, mínimo y punto medio del día de trading actual.
  • Confirmación de rompimiento: Requerir que el precio cierre más allá del límite del canal con un filtro de rango diario mínimo (GapPoints).
  • Expansión de volumen: Comparar las dos últimas velas de 30 minutos completadas y exigir al menos un aumento 1.5× en el volumen de ticks para evitar rompimientos finos.
  • Filtro de tiempo: Permitir nuevas posiciones solo después de la hora de inicio de sesión configurada (SessionStartHour) para evitar el rango nocturno de baja liquidez.
  • Simetría de riesgo: Las operaciones largas usan el mínimo diario como stop loss, las operaciones cortas usan el máximo diario. Los desplazamientos de take profit y trailing se miden en puntos de precio.

Datos de mercado

  • Velas de cinco minutos para la señal primaria y el cálculo del Parabolic SAR.
  • Velas de treinta minutos para el filtro de ratio de volumen de ticks.
  • Las estadísticas de máximo/mínimo diario se derivan al vuelo de los datos de cinco minutos, por lo que no se requiere suscripción diaria separada.

Lógica de entrada

  1. Esperar una vela de cinco minutos terminada después de la hora de inicio configurada.
  2. Calcular el máximo/mínimo/punto medio del día actual y el ancho del canal.
  3. Verificar que el ancho del canal exceda GapPoints * PriceStep.
  4. Calcular el ratio de volumen de ticks = (último volumen de 30 minutos completado) / (volumen de 30 minutos anterior) y asegurar que sea mayor que 1.5.
  5. Configuración larga: la vela cierra en o por encima del máximo diario actual → comprar.
  6. Configuración corta: la vela cierra en o por debajo del mínimo diario actual → vender.
  7. Omitir nuevas entradas mientras haya una posición activa (máximo un trade abierto).

Gestión de salida

  • Stop inicial: las operaciones largas usan el mínimo diario, las operaciones cortas usan el máximo diario capturado en la entrada.
  • Take profit: opcional; colocado a TakeProfitPoints * PriceStep de la entrada en ambos lados del mercado.
  • Reversión Parabolic SAR: cierra la posición si el valor de SAR cruza el cierre de la vela anterior (comportamiento del EA original).
  • Stop trailing: se activa una vez que la ganancia excede TrailingStopPoints * PriceStep y se mueve al menos TrailingStepPoints * PriceStep.
  • Take trailing espejo: siempre que se actualice el stop trailing, el nivel de take-profit se reposiciona simétricamente alrededor del cierre actual.

Dimensionamiento de posición

  • El volumen de posición se deriva de RiskPercent: la estrategia arriesga un porcentaje del valor actual del portafolio basado en la distancia entre entrada y stop.
  • Si la valoración del portafolio no está disponible, el algoritmo recurre a Volume + |Position| y, como último recurso, opera un solo contrato.

Parámetros

Parámetro Predeterminado Descripción
CandleType Velas de 5 minutos Marco temporal primario para señales y Parabolic SAR.
VolumeCandleType Velas de 30 minutos Marco temporal usado para evaluar la expansión del volumen de ticks.
TakeProfitPoints 20 Distancia al objetivo de ganancia expresada en puntos de precio. Establecer en 0 para deshabilitar.
TrailingStopPoints 10 Distancia en puntos para la activación del stop trailing.
TrailingStepPoints 10 Progreso adicional mínimo (en puntos) antes de que el stop trailing se mueva de nuevo.
RiskPercent 5 Porcentaje del capital del portafolio arriesgado por operación.
GapPoints 50 Ancho mínimo del canal diario en puntos requerido para habilitar un rompimiento.
SessionStartHour 7 Hora (0–23) después de la cual la estrategia puede abrir nuevas posiciones.

Notas

  • Los parámetros de Parabolic SAR (paso 0.005, máx 0.01) coinciden con la estrategia MQL original.
  • Los valores del punto medio diario se calculan para completitud y pueden graficarse como referencia visual si se desea.
  • Dado que la expansión de volumen se evalúa en velas de 30 minutos completadas, la confirmación del rompimiento usa la última información cierre-a-cierre disponible, lo que es robusto tanto para pruebas históricas como para trading en vivo.
  • Todos los comentarios en código están en inglés para alinearse con las directrices del repositorio.
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>
/// 20PRExp-3 breakout strategy ported from MetaTrader 5.
/// Tracks the current day's range, waits for volume expansion, and trades breakouts beyond the high or low.
/// </summary>
public class TwentyPrExpThreeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _gapPoints;
	private readonly StrategyParam<int> _sessionStartHour;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _volumeCandleType;

	// Daily levels that are recalculated every trading day.
	private decimal _dailyHigh;
	private decimal _dailyLow;
	private decimal _dailyMid;
	private decimal _dailyRange;
	private DateTime _currentDay;

	// Previous candle close needed for Parabolic SAR exit condition.
	private decimal _previousClose;
	private bool _hasPreviousClose;

	// Last two 30-minute volumes for expansion filter.
	private decimal _currentVolumeBar;
	private decimal _previousVolumeBar;

	// Position management state.
	private decimal _longEntryPrice;
	private decimal _longStop;
	private decimal _longTake;
	private decimal _shortEntryPrice;
	private decimal _shortStop;
	private decimal _shortTake;

	/// <summary>
	/// Take profit distance in price points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimum progress in points before the trailing stop is moved again.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Percentage of portfolio equity to risk per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum daily channel width in points before breakouts are allowed.
	/// </summary>
	public decimal GapPoints
	{
		get => _gapPoints.Value;
		set => _gapPoints.Value = value;
	}

	/// <summary>
	/// Hour (0-23) after which new positions are allowed.
	/// </summary>
	public int SessionStartHour
	{
		get => _sessionStartHour.Value;
		set => _sessionStartHour.Value = value;
	}

	/// <summary>
	/// Primary candle type used for signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type used for the volume filter.
	/// </summary>
	public DataType VolumeCandleType
	{
		get => _volumeCandleType.Value;
		set => _volumeCandleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TwentyPrExpThreeStrategy"/>.
	/// </summary>
	public TwentyPrExpThreeStrategy()
	{
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
			.SetDisplay("Take Profit (pts)", "Target distance in points", "Risk Management")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 10m)
			.SetDisplay("Trailing Stop (pts)", "Trailing stop distance", "Risk Management")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 10m)
			.SetDisplay("Trailing Step (pts)", "Minimum advance before moving trailing stop", "Risk Management")
			;

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetDisplay("Risk %", "Portfolio percentage to risk per trade", "Position Sizing")
			;

		_gapPoints = Param(nameof(GapPoints), 100m)
			.SetDisplay("Range Filter (pts)", "Minimum daily range in points", "Filters")
			;

		_sessionStartHour = Param(nameof(SessionStartHour), 12)
			.SetDisplay("Session Start Hour", "Hour after which breakout trades are enabled", "Filters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");

		_volumeCandleType = Param(nameof(VolumeCandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Volume Candle Type", "Higher timeframe for tick volume filter", "General");
	}

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

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

		_dailyHigh = 0m;
		_dailyLow = 0m;
		_dailyMid = 0m;
		_dailyRange = 0m;
		_currentDay = default;
		_previousClose = 0m;
		_hasPreviousClose = false;
		_currentVolumeBar = 0m;
		_previousVolumeBar = 0m;

		ResetLongState();
		ResetShortState();
	}

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

		// Parabolic SAR parameters mirror the original expert advisor values.
		var parabolicSar = new ParabolicSar
		{
			Acceleration = 0.005m,
			AccelerationMax = 0.01m
		};

		var mainSubscription = SubscribeCandles(CandleType);
		mainSubscription
			.Bind(parabolicSar, ProcessMainCandle)
			.Start();

		var volumeSubscription = SubscribeCandles(VolumeCandleType);
		volumeSubscription
			.Bind(ProcessVolumeCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));

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

	private void ProcessVolumeCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Shift the last two finished 30-minute volumes to approximate tick volume expansion.
		_previousVolumeBar = _currentVolumeBar;
		_currentVolumeBar = candle.TotalVolume;
	}

	private void ProcessMainCandle(ICandleMessage candle, decimal sarValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateDailyLevels(candle);

		if (Position != 0)
		{
			UpdatePreviousClose(candle);
			return;
		}

		var signal = GetTradeSignal(candle);

		if (signal > 0)
			BuyMarket();
		else if (signal < 0)
			SellMarket();

		UpdatePreviousClose(candle);
	}

	private void UpdateDailyLevels(ICandleMessage candle)
	{
		var candleDay = candle.OpenTime.Date;

		if (_currentDay != candleDay)
		{
			_currentDay = candleDay;
			_dailyHigh = candle.HighPrice;
			_dailyLow = candle.LowPrice;
		}
		else
		{
			if (candle.HighPrice > _dailyHigh)
				_dailyHigh = candle.HighPrice;

			if (_dailyLow == 0m || candle.LowPrice < _dailyLow)
				_dailyLow = candle.LowPrice;
		}

		_dailyMid = (_dailyHigh + _dailyLow) / 2m;
		_dailyRange = _dailyHigh - _dailyLow;
	}

	private void ManageOpenPosition(ICandleMessage candle, decimal sarValue)
	{
		if (Position > 0)
		{
			// Close longs when Parabolic SAR crosses above the previous close.
			if (_hasPreviousClose && sarValue > _previousClose)
			{
				if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
				ResetLongState();
				ResetShortState();
				return;
			}

			UpdateLongTrailing(candle);
			CheckLongTargets(candle);
		}
		else if (Position < 0)
		{
			// Close shorts when Parabolic SAR crosses below the previous close.
			if (_hasPreviousClose && sarValue < _previousClose)
			{
				if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
				ResetLongState();
				ResetShortState();
				return;
			}

			UpdateShortTrailing(candle);
			CheckShortTargets(candle);
		}
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _longEntryPrice <= 0m)
			return;

		var pointValue = GetPointValue();
		var trailingDistance = TrailingStopPoints * pointValue;

		if (trailingDistance <= 0m)
			return;

		var profit = candle.ClosePrice - _longEntryPrice;

		if (profit <= trailingDistance)
			return;

		var newStop = candle.ClosePrice - trailingDistance;
		var minStep = TrailingStepPoints > 0m ? TrailingStepPoints * pointValue : 0m;

		if (_longStop > 0m && minStep > 0m && newStop - _longStop < minStep)
			return;

		_longStop = newStop;
		_longTake = TrailingStopPoints > 0m ? candle.ClosePrice + trailingDistance : _longTake;
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _shortEntryPrice <= 0m)
			return;

		var pointValue = GetPointValue();
		var trailingDistance = TrailingStopPoints * pointValue;

		if (trailingDistance <= 0m)
			return;

		var profit = _shortEntryPrice - candle.ClosePrice;

		if (profit <= trailingDistance)
			return;

		var newStop = candle.ClosePrice + trailingDistance;
		var minStep = TrailingStepPoints > 0m ? TrailingStepPoints * pointValue : 0m;

		if (_shortStop > 0m && minStep > 0m && _shortStop - newStop < minStep)
			return;

		_shortStop = newStop;
		_shortTake = TrailingStopPoints > 0m ? candle.ClosePrice - trailingDistance : _shortTake;
	}

	private void CheckLongTargets(ICandleMessage candle)
	{
		var position = Position;

		if (position <= 0m)
			return;

		if (_longStop > 0m && candle.LowPrice <= _longStop)
		{
			SellMarket();
			ResetLongState();
			return;
		}

		if (_longTake > 0m && candle.HighPrice >= _longTake)
		{
			SellMarket();
			ResetLongState();
		}
	}

	private void CheckShortTargets(ICandleMessage candle)
	{
		var position = Position;

		if (position >= 0m)
			return;

		var volume = Math.Abs(position);

		if (_shortStop > 0m && candle.HighPrice >= _shortStop)
		{
			BuyMarket();
			ResetShortState();
			return;
		}

		if (_shortTake > 0m && candle.LowPrice <= _shortTake)
		{
			BuyMarket();
			ResetShortState();
		}
	}

	private int GetTradeSignal(ICandleMessage candle)
	{
		var pointValue = GetPointValue();
		var rangeThreshold = GapPoints * pointValue;
		var hasRange = _dailyRange > 0m && _dailyRange > rangeThreshold;

		var hasVolumeHistory = _previousVolumeBar > 0m && _currentVolumeBar > 0m;
		var volumeRatio = hasVolumeHistory ? _currentVolumeBar / _previousVolumeBar : 0m;

		if (!hasRange)
			return 0;

		if (candle.ClosePrice >= _dailyHigh && _dailyHigh > 0m)
			return 1;

		if (candle.ClosePrice <= _dailyLow && _dailyLow > 0m)
			return -1;

		return 0;
	}

	private void TryEnterLong(decimal entryPrice)
	{
		if (_dailyLow <= 0m)
			return;

		var stopPrice = _dailyLow;
		var stopDistance = entryPrice - stopPrice;

		if (stopDistance <= 0m)
			return;

		var volume = CalculatePositionSize(stopDistance);

		if (volume <= 0m)
			return;

		BuyMarket();

		_longEntryPrice = entryPrice;
		_longStop = stopPrice;
		_longTake = TakeProfitPoints > 0m ? entryPrice + TakeProfitPoints * GetPointValue() : 0m;

		ResetShortState();
	}

	private void TryEnterShort(decimal entryPrice)
	{
		if (_dailyHigh <= 0m)
			return;

		var stopPrice = _dailyHigh;
		var stopDistance = stopPrice - entryPrice;

		if (stopDistance <= 0m)
			return;

		var volume = CalculatePositionSize(stopDistance);

		if (volume <= 0m)
			return;

		SellMarket();

		_shortEntryPrice = entryPrice;
		_shortStop = stopPrice;
		_shortTake = TakeProfitPoints > 0m ? entryPrice - TakeProfitPoints * GetPointValue() : 0m;

		ResetLongState();
	}

	private decimal CalculatePositionSize(decimal stopDistance)
	{
		if (stopDistance <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		var riskFraction = RiskPercent / 100m;

		if (riskFraction > 0m && portfolioValue > 0m)
		{
			var riskAmount = portfolioValue * riskFraction;
			var sized = riskAmount / stopDistance;

			if (sized > 0m)
				return sized;
		}

		var fallback = Volume + Math.Abs(Position);
		return fallback > 0m ? fallback : 1m;
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep;
		return step.HasValue && step.Value > 0m ? step.Value : 1m;
	}

	private void UpdatePreviousClose(ICandleMessage candle)
	{
		_previousClose = candle.ClosePrice;
		_hasPreviousClose = true;
	}

	private void ResetLongState()
	{
		_longEntryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
	}
}