Ver en GitHub

Estrategia de Punto de Ruptura Diario

Descripción general

La Estrategia de Punto de Ruptura Diario es un puerto de StockSharp del asesor experto MetaTrader 5 «Daily BreakPoint» (compilación 19498). El algoritmo monitorea la distancia entre el precio actual y la apertura diaria. Cuando el movimiento desde la apertura diaria supera un umbral configurable y la vela anterior cumple estrictos requisitos de tamaño de cuerpo, la estrategia entra en la dirección del rompimiento o revierte la exposición existente dependiendo del indicador CloseBySignal.

La estrategia trabaja con dos flujos de datos al mismo tiempo:

  1. Velas intradía definidas por el parámetro CandleType para la generación de señales.
  2. Velas diarias usadas para rastrear el precio de apertura de la sesión más reciente.

Lógica de trading

  1. Cuando termina una nueva vela intradía, la estrategia lee el último precio de apertura diaria y calcula los niveles de rompimiento usando BreakPointPips (convertido en precios absolutos a través del tamaño del tick del instrumento).
  2. El tamaño del cuerpo de la vela recientemente cerrada debe estar dentro del rango [LastBarSizeMinPips, LastBarSizeMaxPips].
  3. Configuración alcista
    • La vela debe cerrar por encima de su apertura (Close > Open).
    • El cierre debe ser al menos BreakPointPips por encima de la apertura diaria.
    • El precio de rompimiento (apertura diaria + punto de ruptura) debe estar dentro del cuerpo de la vela.
    • Si CloseBySignal = false, la estrategia abre una posición larga. De lo contrario, cierra cualquier exposición larga abierta y establece una posición corta.
  4. Configuración bajista refleja el caso alcista: una vela bajista cuyo cierre está al menos BreakPointPips por debajo de la apertura diaria y cuyo cuerpo contiene el nivel de rompimiento activa una entrada corta (CloseBySignal = false) o una reversión a una posición larga (CloseBySignal = true).
  5. Las órdenes se envían como órdenes de mercado usando el OrderVolume configurado. El tamaño de la posición es acumulativo, por lo que múltiples señales pueden escalar la posición en cualquier dirección.

Gestión de riesgos

  • Stop Loss / Take Profit: Objetivos fijos opcionales definidos en pips (StopLossPips, TakeProfitPips). Cuando se establece en cero, el nivel correspondiente está deshabilitado. La estrategia evalúa los máximos y mínimos de las velas para detectar hits.
  • Stop Trailing: Habilitado cuando TrailingStopPips > 0. Una vez que la ganancia abierta supera TrailingStopPips + TrailingStepPips, el stop se arrastra detrás del precio por TrailingStopPips. El parámetro de paso previene ajustes frecuentes de stop en mercados planos.
  • Todas las distancias de precio se convierten de pips usando el PriceStep del instrumento. Para cotizaciones de 3 o 5 decimales, el pip equivale a diez pasos de precio, replicando el comportamiento del asesor experto original.

Parámetros

Nombre Descripción
OrderVolume Volumen base usado para cada orden de mercado.
CloseBySignal Si es true, la estrategia cierra posiciones existentes y abre la dirección opuesta cuando aparece una señal de rompimiento.
BreakPointPips Distancia desde la apertura diaria requerida para confirmar un rompimiento.
LastBarSizeMinPips / LastBarSizeMaxPips Tamaño mínimo y máximo del cuerpo de la vela disparadora.
TrailingStopPips Distancia del stop trailing. Establecer en 0 para deshabilitar el trailing.
TrailingStepPips Movimiento adicional requerido antes de cada ajuste de trailing.
StopLossPips Stop loss fijo opcional. 0 lo deshabilita.
TakeProfitPips Take profit fijo opcional. 0 lo deshabilita.
CandleType Serie de velas intradía usada para la generación de señales.

Notas de uso

  • La estrategia se suscribe automáticamente a las velas intradía y diarias. Asegúrese de que el proveedor de datos admita los marcos temporales solicitados.
  • Dado que la lógica evalúa velas terminadas, las órdenes se envían al precio de cierre de la barra de señal.
  • La conversión de pip asume precios estilo Forex. Revise los valores predeterminados cuando aplique la estrategia a instrumentos con tamaños de tick no convencionales.
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>
/// Daily breakout strategy that reacts to the distance from the daily open.
/// Converted from the MetaTrader Daily BreakPoint expert advisor.
/// </summary>
public class DailyBreakPointStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _breakPointPips;
	private readonly StrategyParam<decimal> _lastBarSizeMinPips;
	private readonly StrategyParam<decimal> _lastBarSizeMaxPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _currentDayOpen;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal _pipSize;

	/// <summary>
	/// Order volume.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Reverse the position when the opposite signal appears.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Break distance from the daily open expressed in pips.
	/// </summary>
	public decimal BreakPointPips
	{
		get => _breakPointPips.Value;
		set => _breakPointPips.Value = value;
	}

	/// <summary>
	/// Minimum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMinPips
	{
		get => _lastBarSizeMinPips.Value;
		set => _lastBarSizeMinPips.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMaxPips
	{
		get => _lastBarSizeMaxPips.Value;
		set => _lastBarSizeMaxPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Fixed stop loss in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="DailyBreakPointStrategy"/> class.
	/// </summary>
	public DailyBreakPointStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Default order volume", "General");

		_closeBySignal = Param(nameof(CloseBySignal), true)
		.SetDisplay("Close By Signal", "Reverse existing position on opposite signal", "General");

		_breakPointPips = Param(nameof(BreakPointPips), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Break Point (pips)", "Distance from the daily open", "Signals");

		_lastBarSizeMinPips = Param(nameof(LastBarSizeMinPips), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Min (pips)", "Minimum body size of the previous bar", "Signals");

		_lastBarSizeMaxPips = Param(nameof(LastBarSizeMaxPips), 5000m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Max (pips)", "Maximum body size of the previous bar", "Signals");

		_trailingStopPips = Param(nameof(TrailingStopPips), 2m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 2m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetDisplay("Stop Loss (pips)", "Fixed stop loss distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 30m)
		.SetDisplay("Take Profit (pips)", "Fixed take profit distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Intraday candle series", "Data");
	}

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

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

		_currentDayOpen = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_pipSize = 0m;
	}

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

		Volume = OrderVolume;
		_pipSize = CalculatePipSize();

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

		var intradaySubscription = SubscribeCandles(CandleType);
		intradaySubscription.Bind(ProcessCandle).Start();

		var dailySubscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, intradaySubscription);
			DrawOwnTrades(area);
		}
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.0001m;
		if (step <= 0m)
		step = 0.0001m;

		var decimals = Security?.Decimals;
		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (step is null || step.Value <= 0m)
		return price;

		var value = price / step.Value;
		var rounded = Math.Round(value, 0, MidpointRounding.AwayFromZero);
		return rounded * step.Value;
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State == CandleStates.Finished || candle.State == CandleStates.Active)
		_currentDayOpen = candle.OpenPrice;
	}

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

		Volume = OrderVolume;


		if (_pipSize <= 0m)
		_pipSize = CalculatePipSize();

		var dayOpen = _currentDayOpen;
		if (dayOpen is null)
		return;

		var breakOffset = BreakPointPips * _pipSize;
		var minBody = LastBarSizeMinPips * _pipSize;
		var maxBody = LastBarSizeMaxPips * _pipSize;
		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var stopLossOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		UpdateTrailing(candle, trailingStop, trailingStep);
		HandleRiskExits(candle);

		var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var minPrice = Math.Min(candle.OpenPrice, candle.ClosePrice);
		var maxPrice = Math.Max(candle.OpenPrice, candle.ClosePrice);

		var breakBuy = dayOpen.Value + breakOffset;
		var breakSell = dayOpen.Value - breakOffset;

		var bullishBody = candle.ClosePrice > candle.OpenPrice;
		var bearishBody = candle.ClosePrice < candle.OpenPrice;

		var bullishSignal = bullishBody && breakOffset > 0m &&
		candle.ClosePrice - dayOpen.Value >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		var bearishSignal = bearishBody && breakOffset > 0m &&
		dayOpen.Value - candle.ClosePrice >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		if (bullishSignal)
		{
			ExecuteBullishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
		else if (bearishSignal)
		{
			ExecuteBearishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
	}

	private void UpdateTrailing(ICandleMessage candle, decimal trailingStop, decimal trailingStep)
	{
		if (trailingStop <= 0m)
		return;

		if (Position > 0 && _longEntryPrice.HasValue)
		{
			var profit = candle.ClosePrice - _longEntryPrice.Value;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice - (trailingStop + trailingStep);
				if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
				_longStopPrice = NormalizePrice(candle.ClosePrice - trailingStop);
			}
		}
		else if (Position < 0 && _shortEntryPrice.HasValue)
		{
			var profit = _shortEntryPrice.Value - candle.ClosePrice;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice + (trailingStop + trailingStep);
				if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold || _shortStopPrice.Value == 0m)
				_shortStopPrice = NormalizePrice(candle.ClosePrice + trailingStop);
			}
		}
	}

	private void HandleRiskExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (volume > 0m && _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (volume > 0m && _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	private void ExecuteBullishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
			}

			ResetLongState();

			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
		}
		else
		{
			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
			ResetShortState();
		}
	}

	private void ExecuteBearishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
			}

			ResetShortState();

			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
		}
		else
		{
			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
			ResetLongState();
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}
}