Ver en GitHub

Estrategia de Precio Extremo

Descripción general

La Estrategia de Precio Extremo replica el asesor experto de MetaTrader Price_Extreme_Strategy mediante la API de alto nivel de StockSharp. El sistema monitorea un canal deslizante derivado del máximo más alto y el mínimo más bajo durante un número configurable de velas completadas. Se generan señales de ruptura cuando la vela de referencia seleccionada cierra por encima del límite superior o por debajo del límite inferior. La lógica puede invertirse opcionalmente para transformar las condiciones de ruptura en entradas de contra-tendencia.

Esta conversión mantiene el flujo de trabajo de trading orientado a eventos. Las órdenes se envían inmediatamente después del cierre de cada vela finalizada, replicando el comportamiento del algoritmo MQL original que reaccionaba en el tick de apertura de la siguiente barra.

Lógica del indicador

El canal de Precio Extremo se reconstruye en cada vela finalizada usando los indicadores Highest y Lowest de StockSharp:

  • Highest rastrea el máximo de los altos durante las últimas N velas.
  • Lowest rastrea el mínimo de los bajos durante las últimas N velas.

Estos búferes emulan el estudio personalizado Price_Extreme_Indicator incluido con el asesor experto original. La longitud del indicador se expone a través del parámetro Level Length.

Un parámetro separado Signal Shift define qué vela cerrada se usa para evaluar la condición de ruptura. Un shift de 1 significa "usar la vela que acaba de cerrar" (por defecto). Valores mayores permiten esperar confirmación adicional haciendo referencia a barras más antiguas.

Reglas de trading

  1. Recalcular los valores del canal superior e inferior para cada vela finalizada.
  2. Recuperar la vela especificada por Signal Shift del búfer de historial interno.
  3. Generar intenciones direccionales:
    • Ruptura alcista: el cierre de la vela está por encima del valor del canal superior.
    • Ruptura bajista: el cierre de la vela está por debajo del valor del canal inferior.
  4. Aplicar inversión opcional con Reverse Signals:
    • Si está desactivado, operar en la dirección de la ruptura (comprar en ruptura alcista, vender en ruptura bajista).
    • Si está activado, intercambiar las reacciones (vender en ruptura alcista, comprar en ruptura bajista).
  5. Respetar los permisos Enable Long y Enable Short antes de enviar órdenes.
  6. Cerrar automáticamente cualquier posición opuesta antes de abrir una nueva operación para que solo exista una posición neta en todo momento.

Gestión de riesgos

La estrategia proporciona manejo de stop-loss y take-profit que replica los controles basados en puntos de la versión MQL:

  • Stop Loss y Take Profit se expresan en pasos de precio (Security.PriceStep).
  • Los precios objetivo se recalculan cuando cambia el tamaño de la posición neta.
  • Si una vela finalizada supera los niveles de protección (mínimo por debajo del stop para largos, máximo por encima del stop para cortos, etc.), la posición se cierra mediante orden de mercado y los objetivos de protección se borran.
  • StartProtection() se activa durante OnStarted para aprovechar las salvaguardas integradas de StockSharp.

Parámetros

Parámetro Descripción Predeterminado Grupo
LevelLength Número de velas completadas consideradas al calcular el canal extremo. 5 Indicator
SignalShift Índice de la vela cerrada usada para la validación de ruptura (1 = última vela cerrada). 1 Indicator
EnableLong Permite comprar cuando es true. true Trading
EnableShort Permite vender cuando es true. true Trading
ReverseSignals Invierte las reacciones de ruptura (comprar en bajada, vender en subida). false Trading
OrderVolume Volumen enviado con cada orden de mercado. Debe ser mayor que cero. 1 Trading
StopLossPoints Distancia del stop-loss medida en pasos de precio. Un valor de 0 desactiva el stop. 0 Risk
TakeProfitPoints Distancia del take-profit medida en pasos de precio. Un valor de 0 desactiva el objetivo. 0 Risk
CandleType Marco temporal principal para la suscripción de datos. Velas de 5 minutos Data

Todos los parámetros usan StrategyParam<T> con metadatos de UI para que puedan optimizarse o modificarse desde el Designer.

Guía de uso

  1. Adjuntar la estrategia a un instrumento y establecer el Candle Type para que coincida con el marco temporal usado en la configuración original de MetaTrader.
  2. Ajustar Level Length si se desea un canal de Precio Extremo más amplio o más estrecho.
  3. Configurar Signal Shift para controlar cuántas velas cerradas esperar antes de evaluar la ruptura.
  4. Seleccionar las direcciones de operación deseadas mediante Enable Long, Enable Short y Reverse Signals.
  5. Definir Order Volume, Stop Loss y Take Profit según las preferencias de riesgo. Recuerde que ambos valores de protección operan en pasos de precio.
  6. Iniciar la estrategia. Las velas, las bandas del indicador y las operaciones ejecutadas se grafican automáticamente cuando hay un área de gráfico disponible.

Notas adicionales

  • La estrategia opera intencionalmente sobre una sola posición neta, replicando la lógica de cobertura del experto MQL al aplanar el lado opuesto antes de entrar en una nueva operación.
  • Los stops y objetivos de protección se evalúan en velas completadas. En trading en vivo, esto aproxima las órdenes de protección del lado del servidor usadas por el script original.
  • No se incluye versión en Python, según lo solicitado.
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 based on the Price Extreme indicator.
/// </summary>
public class PriceExtremeStrategy : Strategy
{
	private readonly StrategyParam<int> _levelLength;
	private readonly StrategyParam<int> _signalShift;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<ICandleMessage> _history = new();
	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _prevPosition;
	private decimal _entryPrice;
	private decimal _prevUpper;
	private decimal _prevLower;

	/// <summary>
	/// Number of candles used to build extreme levels.
	/// </summary>
	public int LevelLength
	{
		get => _levelLength.Value;
		set => _levelLength.Value = value;
	}

	/// <summary>
	/// Shift in candles used for the breakout signal.
	/// </summary>
	public int SignalShift
	{
		get => _signalShift.Value;
		set => _signalShift.Value = value;
	}

	/// <summary>
	/// Enable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Reverse long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

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

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	/// <summary>
	/// Type of candles used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="PriceExtremeStrategy"/>.
	/// </summary>
	public PriceExtremeStrategy()
	{
		_levelLength = Param(nameof(LevelLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Level Length", "Number of candles for price extremes", "Indicator")
			
			.SetOptimize(3, 30, 1);

		_signalShift = Param(nameof(SignalShift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Shift", "Closed candles used for breakout", "Indicator");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow buying trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow selling trades", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert breakout direction", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume sent with market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit", "Profit target in price steps", "Risk");

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

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

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

		_history.Clear();
		_highs.Clear();
		_lows.Clear();
		_prevUpper = 0m;
		_prevLower = 0m;
		_entryPrice = 0m;
		_prevPosition = 0m;
		ResetTargets();
	}

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

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

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

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

	private bool CanOpenLong => EnableLong && OrderVolume > 0m;
	private bool CanOpenShort => EnableShort && OrderVolume > 0m;

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

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);
		_history.Add(candle);

		var maxHistory = Math.Max(LevelLength + SignalShift + 2, 10);
		if (_history.Count > maxHistory)
		{
			var removeCount = _history.Count - maxHistory;
			_history.RemoveRange(0, removeCount);
			_highs.RemoveRange(0, removeCount);
			_lows.RemoveRange(0, removeCount);
		}

		if (_highs.Count < LevelLength)
			return;

		var upper = decimal.MinValue;
		var lower = decimal.MaxValue;
		for (var i = _highs.Count - LevelLength; i < _highs.Count; i++)
		{
			if (_highs[i] > upper) upper = _highs[i];
			if (_lows[i] < lower) lower = _lows[i];
		}

		if (_history.Count < SignalShift)
			return;

		var signalCandle = _history[_history.Count - SignalShift];

		var breakoutUp = candle.ClosePrice > _prevUpper && _prevUpper > 0;
		var breakoutDown = candle.ClosePrice < _prevLower && _prevLower > 0;

		_prevUpper = upper;
		_prevLower = lower;

		var wantLong = ReverseSignals ? breakoutDown : breakoutUp;
		var wantShort = ReverseSignals ? breakoutUp : breakoutDown;

		if (wantLong && CanOpenLong && Position == 0)
		{
			BuyMarket();
		}
		else if (wantShort && CanOpenShort && Position == 0)
		{
			SellMarket();
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0)
			_entryPrice = 0m;
	}

	private void UpdateTargets()
	{
		_stopPrice = null;
		_takePrice = null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m || Position == 0m)
			return;

		if (Position > 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice - StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice + TakeProfitPoints * step;
		}
		else if (Position < 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice + StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice - TakeProfitPoints * step;
		}
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
	}

	private void ResetTargets()
	{
		_stopPrice = null;
		_takePrice = null;
		_prevPosition = Position;
	}
}