Ver en GitHub

BrakeoutTraderV1

BrakeoutTraderV1 es un sistema de rompimiento simple construido alrededor de un nivel de precio estático. La estrategia observa los precios de cierre de velas completadas y entra cuando el mercado cierra a través del nivel de rompimiento elegido. Cuando el cierre cruza por encima del nivel, se abre una posición larga (sujeto a filtros de dirección); cuando cruza por debajo, se toma una posición corta. El tamaño de la posición se calcula a partir del porcentaje de riesgo configurado y la distancia al stop-loss, habilitando escalado automático con la equidad de la cuenta.

Lógica de trading

  • Procesar solo velas finalizadas del CandleType seleccionado. Las velas incompletas se ignoran.
  • Mantener el último precio de cierre para detectar rompimientos del BreakoutLevel especificado por el usuario.
  • Entrada larga: la última vela cierra por encima de BreakoutLevel mientras que el cierre anterior estaba en o por debajo del nivel, y EnableLong es verdadero. Cualquier posición corta abierta se aplana antes de enviar la nueva orden.
  • Entrada corta: la última vela cierra por debajo de BreakoutLevel mientras que el cierre anterior estaba en o por encima del nivel, y EnableShort es verdadero. Primero se cierra cualquier posición larga.
  • Las órdenes se envían a mercado. La cantidad se calcula para que la pérdida entre el precio de entrada y la distancia al stop-loss corresponda al RiskPercent de la equidad de la cuenta actual. Si el tamaño basado en riesgo no puede determinarse, la estrategia recurre al valor base Volume.
  • Después de cada entrada, la estrategia almacena niveles estáticos de toma de ganancias y stop-loss expresados en pips (StopLossPoints y TakeProfitPoints). Cuando el precio alcanza cualquiera de los niveles, la posición abierta se cierra a mercado y los niveles en caché se limpian.
  • Nunca hay múltiples operaciones abiertas en la misma dirección simultáneamente porque la posición neta se gestiona explícitamente.

Gestión de posición

  • Un stop protector se establece por debajo de la entrada para operaciones largas y por encima de la entrada para cortas. La distancia es StopLossPoints * pip, donde pip se deriva de Security.PriceStep y su precisión (3 o 5 decimales implican un ajuste de diez veces, como en la implementación MQL original).
  • Un objetivo de ganancia se establece simétricamente usando TakeProfitPoints.
  • Si el stop y el objetivo se activarían durante la misma vela, se evalúa primero el stop, reflejando la ejecución conservadora del servidor.
  • Las señales opuestas siempre cierran cualquier posición activa antes de establecer la nueva, evitando exposición hedgeada.
  • El helper reinicia automáticamente los niveles en caché cuando la posición vuelve a cero.

Parámetros

  • BreakoutLevel – Nivel de precio estático monitoreado para rompimientos.
  • EnableLong / EnableShort – Filtros de dirección que permiten abrir posiciones largas o cortas.
  • StopLossPoints – Distancia del stop-loss en pips (múltiplos del tamaño de pip derivado).
  • TakeProfitPoints – Distancia del take-profit en pips.
  • RiskPercent – Porcentaje de la equidad de la cuenta a arriesgar por operación. Usado para determinar el volumen de la orden desde la distancia del stop-loss.
  • CandleType – Serie de datos de vela usada para generación de señales (por defecto velas de 15 minutos).
  • Volume – Tamaño base de la orden usado cuando el cálculo basado en riesgo no está disponible.

Detalles

  • Criterios de entrada: El cierre cruza por encima/debajo de BreakoutLevel en la última vela completada.
  • Largo/Corto: Opera ambas direcciones, controlado por los indicadores EnableLong y EnableShort.
  • Criterios de salida: Niveles estáticos de stop-loss y take-profit, más aplanamiento en señales de rompimiento opuestas.
  • Stops: Stop-loss de distancia fija medida en pips.
  • Valores predeterminados: BreakoutLevel = 0, StopLossPoints = 140, TakeProfitPoints = 180, RiskPercent = 10, CandleType = 15 minutos, EnableLong = EnableShort = true.
  • Filtros: Ninguno más allá de los selectores de dirección; no se aplican filtros de tendencia o volatilidad.

Notas de uso

  • El instrumento debe soportar el cálculo de pip utilizado por el EA original. Para símbolos con 3 o 5 decimales, el pip se escala automáticamente por diez.
  • Asegurarse de que el portafolio conectado proporcione CurrentValue para que el dimensionamiento basado en riesgo funcione correctamente. Si la equidad no está disponible, las operaciones se ejecutarán con el Volume configurado.
  • Dado que las órdenes se ejecutan a mercado, los llenados reales pueden diferir del cierre de la vela. Ajustar las distancias de stop y take para tener en cuenta el deslizamiento si es necesario.
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>
/// Breakout strategy that trades when the closing price crosses a predefined level.
/// The strategy sizes positions based on the selected risk percentage and applies static stop-loss and take-profit levels.
/// </summary>
public class BrakeoutTraderV1Strategy : Strategy
{
	private readonly StrategyParam<decimal> _breakoutLevel;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousClose;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _pipSize;

	/// <summary>
	/// Price level that must be broken to generate signals.
	/// </summary>
	public decimal BreakoutLevel
	{
		get => _breakoutLevel.Value;
		set => _breakoutLevel.Value = value;
	}

	/// <summary>
	/// Enables or disables long breakout trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enables or disables short breakout trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in points relative to the pip size.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in points relative to the pip size.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

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

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

	/// <summary>
	/// Initializes a new instance of <see cref="BrakeoutTraderV1Strategy"/>.
	/// </summary>
	public BrakeoutTraderV1Strategy()
	{
		_breakoutLevel = Param(nameof(BreakoutLevel), 65000m)
			.SetDisplay("Breakout Level", "Static price level monitored for breakouts", "Signal");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow long breakout positions", "Signal");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow short breakout positions", "Signal");

		_stopLossPoints = Param(nameof(StopLossPoints), 140m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Stop-loss distance expressed in pip points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 180m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit Points", "Take-profit distance expressed in pip points", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Percentage of equity risked per trade", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for breakout detection", "General");
	}

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

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

		_previousClose = null;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_pipSize = 0m;
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals;
		_pipSize = priceStep;

		if (decimals is 3 or 5)
			_pipSize *= 10m;

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

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

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

		// removed StartProtection(null, null)
	}

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

		if (ManageOpenPosition(candle))
		{
			UpdatePreviousClose(candle.ClosePrice);
			return;
		}

		var prevClose = _previousClose;
		if (prevClose is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var currentClose = candle.ClosePrice;
		var prevValue = prevClose.Value;

		var breakoutUp = currentClose > BreakoutLevel && prevValue <= BreakoutLevel;
		var breakoutDown = currentClose < BreakoutLevel && prevValue >= BreakoutLevel;

		if (breakoutUp && EnableLong)
		{
			EnterLong(currentClose);
		}
		else if (breakoutDown && EnableShort)
		{
			EnterShort(currentClose);
		}

		_previousClose = currentClose;
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Exit long if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}

			// Exit long if take-profit is reached.
			if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0)
		{
			// Exit short if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}

			// Exit short if take-profit is reached.
			if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (_entryPrice.HasValue)
		{
			// Reset cached levels after position is fully closed.
			ResetPositionState();
		}

		return false;
	}

	private void EnterLong(decimal price)
	{
		if (Position > 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position < 0 ? Math.Abs(Position) : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		BuyMarket();
		SetPositionTargets(price, true, volume > 0m);
	}

	private void EnterShort(decimal price)
	{
		if (Position < 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position > 0 ? Position : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		SellMarket();
		SetPositionTargets(price, false, volume > 0m);
	}

	private void SetPositionTargets(decimal entryPrice, bool isLong, bool hasNewPosition)
	{
		if (!hasNewPosition)
		{
			return;
		}

		_entryPrice = entryPrice;

		if (StopLossPoints > 0m && _pipSize > 0m)
			_stopPrice = isLong
				? entryPrice - StopLossPoints * _pipSize
				: entryPrice + StopLossPoints * _pipSize;
		else
			_stopPrice = null;

		if (TakeProfitPoints > 0m && _pipSize > 0m)
			_takePrice = isLong
				? entryPrice + TakeProfitPoints * _pipSize
				: entryPrice - TakeProfitPoints * _pipSize;
		else
			_takePrice = null;
	}

	private decimal CalculateOrderVolume()
	{
		var baseVolume = Volume;
		var stopDistance = StopLossPoints * _pipSize;

		if (stopDistance <= 0m || RiskPercent <= 0m)
			return AdjustVolume(baseVolume);

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return AdjustVolume(baseVolume);

		var riskValue = equity * RiskPercent / 100m;
		if (riskValue <= 0m)
			return AdjustVolume(baseVolume);

		var qty = riskValue / stopDistance;
		var adjusted = AdjustVolume(qty);

		return adjusted > 0m ? adjusted : AdjustVolume(baseVolume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep;
			if (step is decimal s && s > 0m)
			{
				volume = Math.Floor(volume / s) * s;
			}

			var min = security.MinVolume;
			if (min is decimal minVol && volume < minVol)
				volume = minVol;

			var max = security.MaxVolume;
			if (max is decimal maxVol && maxVol > 0m && volume > maxVol)
				volume = maxVol;

			if (volume <= 0m)
				volume = step is decimal stepVal && stepVal > 0m ? stepVal : 0m;
		}

		if (volume <= 0m)
			volume = volume == 0m ? 1m : Math.Abs(volume);

		return volume;
	}

	private void UpdatePreviousClose(decimal close)
	{
		_previousClose = close;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}
}