Ver en GitHub

Estrategia NUp1Down

Descripción general

La Estrategia NUp1Down es una conversión directa del experto de MetaTrader 5 "N bars up, then one bar down" (archivo NUp1Down.mq5). Escanea velas completadas entregadas por StockSharp y entra en una operación corta cuando aparece una vela bajista después de una secuencia configurable de velas alcistas que siguen haciendo cierres más altos. La estrategia está diseñada para traders discrecionales que quieren automatizar un patrón clásico de reversión de swing dentro de StockSharp Designer, Shell o Runner.

Lógica de trading

  1. Trabajar solo en velas terminadas proporcionadas por el parámetro CandleType.
  2. Mantener en memoria las últimas BarsCount + 1 velas. La vela más nueva debe cerrar por debajo de su apertura (vela de configuración bajista).
  3. Las BarsCount velas anteriores deben cerrar por encima de sus aperturas. Cada una de estas velas alcistas (excepto la más antigua) también debe cerrar por encima del cierre de la vela que vino justo antes, imponiendo un movimiento "escalera" al alza.
  4. Cuando el patrón se valida y no hay posición corta activa, la estrategia envía una orden de venta a mercado.
  5. El dimensionamiento de la posición usa el parámetro RiskPercent. El algoritmo estima cuántos contratos se pueden abrir para que el capital en riesgo (distancia al stop-loss convertida a valor monetario) no exceda el porcentaje elegido del portafolio. La propiedad base Volume sigue siendo el tamaño mínimo de lote y el modelo de riesgo solo puede aumentar el tamaño de la operación.

Gestión de posición

  • Al entrar se calculan un stop-loss protector y un nivel de take-profit desde el precio de entrada. Ambas distancias se expresan en pips y se traducen a precios usando el PriceStep del instrumento. Para símbolos con tres o cinco dígitos decimales, el tamaño del pip se ajusta automáticamente para coincidir con la definición de pip de MetaTrader.
  • Un stop trailing se recalcula en cada vela terminada. La distancia de trailing es igual a TrailingStopPips y el stop se desplaza solo si el precio se ha movido al menos TrailingStepPips a favor de la operación. La lógica de trailing emula el experto original: para operaciones cortas sigue el precio de demanda más bajo, mientras que las operaciones largas no son producidas por esta estrategia.
  • Las condiciones de salida se evalúan antes de buscar nuevas entradas en cada vela. La estrategia cierra la posición cuando se golpea el stop-loss o el take-profit, o cuando la lógica de trailing aprieta el stop por encima del precio de demanda actual.

Parámetros

Nombre Descripción
BarsCount Número de velas alcistas requeridas antes de la vela de configuración bajista (predeterminado: 3).
TakeProfitPips Distancia de take-profit en pips aplicada al precio de entrada (predeterminado: 50).
StopLossPips Distancia de stop-loss en pips aplicada al precio de entrada (predeterminado: 50).
TrailingStopPips Distancia entre el precio de mercado y el stop trailing (predeterminado: 10).
TrailingStepPips Movimiento favorable mínimo antes de que avance el stop trailing (predeterminado: 5).
RiskPercent Porcentaje del capital del portafolio a arriesgar en cada operación (predeterminado: 5).
CandleType Tipo de datos / marco temporal de velas usado para la detección del patrón (predeterminado: 1 hora).

Notas de uso

  • Configure la propiedad Volume al tamaño mínimo de orden permitido por su broker. El dimensionamiento basado en riesgo puede aumentar el tamaño de la operación pero nunca lo reduce por debajo de Volume.
  • La estrategia mantiene solo una posición corta agregada a la vez. Si existe una posición larga, se cerrará antes de abrir la corta.
  • El algoritmo trabaja con datos de velas. Los hits de stop-loss o take-profit intrabar se detectan usando el máximo/mínimo de la vela, por lo que el tiempo de ejecución real puede diferir de la ejecución a nivel de tick.
  • No se proporciona versión Python en esta versión. Solo está disponible la implementación C# dentro de API/2574/CS/NUp1DownStrategy.cs.
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;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that sells after a sequence of bullish candles followed by a bearish candle.
/// </summary>
public class NUp1DownStrategy : Strategy
{
	private readonly StrategyParam<int> _barsCount;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<(decimal Open, decimal Close)> _recentCandles = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _activeStopPrice;
	private decimal? _activeTakePrice;

	/// <summary>
	/// Number of consecutive bullish bars required before the bearish setup candle.
	/// </summary>
	public int BarsCount
	{
		get => _barsCount.Value;
		set => _barsCount.Value = value;
	}

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

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.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>
	/// Risk percentage used to size the position.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle type used for pattern detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="NUp1DownStrategy"/> class.
	/// </summary>
	public NUp1DownStrategy()
	{
		Volume = 1m;

		_barsCount = Param(nameof(BarsCount), 3)
			.SetGreaterThanZero()
			.SetDisplay("Bullish Bars", "Number of bullish bars before the down bar", "General")
			
			.SetOptimize(2, 6, 1);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
			
			.SetOptimize(5m, 30m, 5m);

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step (pips)", "Trailing step before adjusting stop", "Risk")
			
			.SetOptimize(1m, 20m, 1m);

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio risk percentage per trade", "Money Management")
			
			.SetOptimize(1m, 10m, 1m);

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

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

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

		_recentCandles.Clear();
		_pipSize = 0m;
		ResetPositionState();
	}

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

		_pipSize = CalculatePipSize();

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

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

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

		UpdateTrailingAndExits(candle);

		_recentCandles.Enqueue((candle.OpenPrice, candle.ClosePrice));
		while (_recentCandles.Count > BarsCount + 1)
			_recentCandles.Dequeue();

		if (_recentCandles.Count < BarsCount + 1)
			return;

		var candles = _recentCandles.ToArray();
		var last = candles[^1];

		if (last.Close >= last.Open)
			return;

		var isPattern = true;

		for (var i = 1; i <= BarsCount; i++)
		{
			var index = candles.Length - 1 - i;
			var bar = candles[index];

			if (bar.Close <= bar.Open)
			{
				isPattern = false;
				break;
			}

			if (i < BarsCount)
			{
				var prev = candles[index - 1];
				if (bar.Close <= prev.Close)
				{
					isPattern = false;
					break;
				}
			}
		}

		if (!isPattern)
			return;

		if (Position < 0)
			return;

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_activeStopPrice = _entryPrice + StopLossPips * _pipSize;
		_activeTakePrice = _entryPrice - TakeProfitPips * _pipSize;

		this.LogInfo($"Short entry after {BarsCount} bullish bars at {_entryPrice:0.#####}");
	}

	private void UpdateTrailingAndExits(ICandleMessage candle)
	{
		if (Position < 0)
		{
			var volumeToClose = Math.Abs(Position);
			if (volumeToClose <= 0m)
				return;

			if (_activeStopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				this.LogInfo($"Short exit by stop-loss at {stop:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeTakePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				this.LogInfo($"Short exit by take-profit at {take:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeStopPrice is decimal trailingStop)
			{
				var trailingDistance = TrailingStopPips * _pipSize;
				var trailingStep = TrailingStepPips * _pipSize;

				if (trailingDistance <= 0m)
					return;

				var currentAsk = candle.ClosePrice;
				var newStopCandidate = currentAsk + trailingDistance;

				if (newStopCandidate + trailingStep < trailingStop)
				{
					_activeStopPrice = newStopCandidate;
					this.LogInfo($"Short trailing stop moved to {_activeStopPrice:0.#####}");
				}
			}
		}
		else if (Position == 0)
		{
			ResetPositionState();
		}
	}

	private decimal CalculatePipSize()
	{
		if (Security?.PriceStep is decimal step && step > 0m)
		{
			var decimals = CountDecimalPlaces(step);
			return decimals is 3 or 5 ? step * 10m : step;
		}

		return 1m;
	}

	private static int CountDecimalPlaces(decimal value)
	{
		var text = value.ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.');
		return separatorIndex >= 0 ? text.Length - separatorIndex - 1 : 0;
	}

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

		if (Portfolio == null || stopDistance <= 0m)
			return baseVolume;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			return baseVolume;

		var capital = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (capital <= 0m)
			return baseVolume;

		var riskAmount = capital * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return baseVolume;

		var riskPerUnit = stopDistance;
		if (riskPerUnit <= 0m)
			return baseVolume;

		var volumeFromRisk = riskAmount / riskPerUnit;
		if (volumeFromRisk <= 0m)
			return baseVolume;

		return Math.Max(baseVolume, volumeFromRisk);
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_activeStopPrice = null;
		_activeTakePrice = null;
	}
}