Ver en GitHub

Estrategia NTK 07 de Operación en Rango

La Estrategia NTK 07 de Operación en Rango es un port del asesor experto de MetaTrader "NTK 07". El algoritmo mantiene órdenes stop simétricas alrededor del precio de mercado actual y gestiona posiciones abiertas con lógica de trailing y take-profit configurables. El objetivo es capturar rupturas que ocurren cerca de los bordes o el centro de un rango de precio a corto plazo respetando controles de riesgo estrictos.

Ideas centrales

  • Disparadores de entrada – Cuando la estrategia está plana evalúa un rango de lookback configurable. Si el precio está en los bordes del rango o cerca de su punto medio (dependiendo del modo de trade seleccionado) coloca órdenes buy stop y sell stop en un offset definido en pasos de precio.
  • Conciencia del rango – Los precios más altos y más bajos de las últimas N velas terminadas definen el rango de trading. Una longitud cero desactiva el filtro y permite colocar órdenes inmediatamente.
  • Riesgo adaptativo – Cada entrada usa el volumen base mientras un multiplicador de lotes opcional puede piramidizar órdenes stop adicionales después de que se abre una posición. Un límite de volumen a nivel de cartera bloquea nuevas órdenes cuando la exposición superaría el tope.
  • Gestión de salida – Tan pronto como se llena una posición, se cancela la orden stop opuesta. La estrategia luego registra órdenes de stop protector y take-profit opcionales usando los offsets configurados. El trailing puede seguir el máximo/mínimo de la vela anterior, una media móvil, o un buffer de distancia fija.
  • Filtro de sesión – El trading se permite solo entre las horas de inicio y fin seleccionadas y se desactiva automáticamente los fines de semana.

Parámetros

Parámetro Descripción
Entry Volume Tamaño base para cada orden de entrada.
Total Volume Limit Tamaño máximo de posición acumulada. Un valor de 0 deshabilita el tope.
Net Step Distancia en pasos de precio entre el mercado y las órdenes stop de entrada.
Stop Loss Offset inicial del stop-loss en pasos de precio relativo al precio de entrada.
Take Profit Distancia del take-profit en pasos de precio. Establecer en 0 para deshabilitar objetivos de ganancia.
Trailing Stop Distancia en pasos de precio usada para la lógica de trailing.
Lot Multiplier Multiplicador aplicado al piramidizar en una posición existente.
Trail High/Low Si está habilitado, los stops protectores siguen los extremos de la vela anterior.
Trail Moving Average Habilita el trailing usando un valor de media móvil. Solo un modo de trailing puede estar activo.
Trading Start/End Hour Ventana de tiempo de plataforma inclusiva para el trading.
Range Bars Número de velas completadas usadas para calcular el rango de trading. 0 deshabilita el filtro.
Trade Mode EdgesOfRange requiere que el precio toque los bordes del rango, CenterOfRange espera hasta que el precio esté cerca del punto medio del rango.
MA Period Longitud de la media móvil usada para el trailing.
Candle Type Agregación de velas usada para todos los cálculos.

Flujo de trabajo

  1. Suscripción de datos – La estrategia se suscribe a la serie de velas configurada y calcula la media móvil así como el precio más alto y más bajo sobre la longitud de rango elegida.
  2. Estado plano – Mientras no hay posición abierta, la estrategia evalúa la condición del rango. Si se satisface, coloca órdenes buy stop y sell stop pareadas en el offset especificado respetando el límite de volumen global.
  3. Manejo de posición – Cuando una entrada se llena, el stop opuesto se cancela. La estrategia coloca inmediatamente órdenes de stop-loss protector y take-profit opcional. La lógica de trailing luego actualiza el stop protector en cada nueva vela terminada.
  4. Piramidización – Si el multiplicador de lotes es mayor que 1, se coloca una orden stop adicional en la dirección de la posición actual mientras el límite de volumen total lo permita.
  5. Salida – Los stops o take-profits aplanan la posición y cancelan las órdenes protectoras restantes. El sistema luego vuelve a monitorear la próxima interacción del rango.

Notas

  • La estrategia funciona enteramente con pasos de precio, lo que la hace adecuada para instrumentos con diferentes tamaños de tick.
  • El trading se desactiva automáticamente los sábados y domingos para reflejar el comportamiento de la implementación MQL original.
  • Solo un modo de trailing puede habilitarse a la vez; habilitar ambos desencadenará un error de configuración al inicio.
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>
/// Port of the NTK 07 MetaTrader strategy that trades stop orders around a recent range.
/// </summary>
public class Ntk07RangeTraderStrategy : Strategy
{
	public enum TradeModeOptions
	{
		EdgesOfRange,
		CenterOfRange,
	}

	private readonly StrategyParam<decimal> _entryVolume;
	private readonly StrategyParam<decimal> _totalVolumeLimit;
	private readonly StrategyParam<decimal> _netStepPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<bool> _trailHighLow;
	private readonly StrategyParam<bool> _trailMa;
	private readonly StrategyParam<int> _tradingStartHour;
	private readonly StrategyParam<int> _tradingEndHour;
	private readonly StrategyParam<int> _rangeBars;
	private readonly StrategyParam<TradeModeOptions> _tradeMode;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _movingAverage;
	private Highest _rangeHighIndicator;
	private Lowest _rangeLowIndicator;

	private ICandleMessage _previousCandle;
	private decimal _priceStep;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private int _candlesSinceLastTrade;
	private const int CooldownCandles = 210;

	/// <summary>
	/// Base volume used for each entry order.
	/// </summary>
	public decimal EntryVolume
	{
		get => _entryVolume.Value;
		set => _entryVolume.Value = value;
	}

	/// <summary>
	/// Maximum total exposure allowed for the strategy. Set to zero for unlimited exposure.
	/// </summary>
	public decimal TotalVolumeLimit
	{
		get => _totalVolumeLimit.Value;
		set => _totalVolumeLimit.Value = value;
	}

	/// <summary>
	/// Distance of stop orders from the market in price steps.
	/// </summary>
	public decimal NetStepPoints
	{
		get => _netStepPoints.Value;
		set => _netStepPoints.Value = value;
	}

	/// <summary>
	/// Initial stop-loss distance in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

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

	/// <summary>
	/// Additional volume multiplier used when pyramiding into an existing position.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Enables trailing based on previous candle extremes.
	/// </summary>
	public bool UseTrailingAtHighLow
	{
		get => _trailHighLow.Value;
		set => _trailHighLow.Value = value;
	}

	/// <summary>
	/// Enables trailing based on a moving average.
	/// </summary>
	public bool UseTrailingMa
	{
		get => _trailMa.Value;
		set => _trailMa.Value = value;
	}

	/// <summary>
	/// Inclusive starting hour for trading (platform time).
	/// </summary>
	public int TradingStartHour
	{
		get => _tradingStartHour.Value;
		set => _tradingStartHour.Value = value;
	}

	/// <summary>
	/// Inclusive ending hour for trading (platform time).
	/// </summary>
	public int TradingEndHour
	{
		get => _tradingEndHour.Value;
		set => _tradingEndHour.Value = value;
	}

	/// <summary>
	/// Number of completed candles that define the reference range. Set to zero to disable range filtering.
	/// </summary>
	public int RangeBars
	{
		get => _rangeBars.Value;
		set => _rangeBars.Value = value;
	}

	/// <summary>
	/// Range interaction mode for entry logic.
	/// </summary>
	public TradeModeOptions TradeMode
	{
		get => _tradeMode.Value;
		set => _tradeMode.Value = value;
	}

	/// <summary>
	/// Length of the moving average used for trailing stops.
	/// </summary>
	public int MovingAveragePeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public Ntk07RangeTraderStrategy()
	{
		_entryVolume = Param(nameof(EntryVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Entry Volume", "Base volume for each entry order", "Risk");

		_totalVolumeLimit = Param(nameof(TotalVolumeLimit), 7m)
		.SetNotNegative()
		.SetDisplay("Total Volume Limit", "Maximum aggregated volume (0 disables the limit)", "Risk");

		_netStepPoints = Param(nameof(NetStepPoints), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Net Step", "Offset for stop entries measured in price steps", "Entries");

		_stopLossPoints = Param(nameof(StopLossPoints), 11m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Initial stop distance measured in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Take-profit distance measured in price steps", "Risk");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 8m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Stop", "Distance used for trailing calculations in price steps", "Risk");

		_lotMultiplier = Param(nameof(LotMultiplier), 1.7m)
		.SetGreaterThanZero()
		.SetDisplay("Lot Multiplier", "Volume multiplier when pyramiding", "Risk");

		_trailHighLow = Param(nameof(UseTrailingAtHighLow), true)
		.SetDisplay("Trail High/Low", "Use previous candle extremes for trailing", "Risk");

		_trailMa = Param(nameof(UseTrailingMa), false)
		.SetDisplay("Trail Moving Average", "Use moving average value for trailing", "Risk");

		_tradingStartHour = Param(nameof(TradingStartHour), 0)
		.SetDisplay("Trading Start Hour", "Trading window opening hour", "Sessions");

		_tradingEndHour = Param(nameof(TradingEndHour), 23)
		.SetDisplay("Trading End Hour", "Trading window closing hour", "Sessions");

		_rangeBars = Param(nameof(RangeBars), 0)
		.SetNotNegative()
		.SetDisplay("Range Bars", "Number of completed candles used for the range", "Entries");

		_tradeMode = Param(nameof(TradeMode), TradeModeOptions.EdgesOfRange)
		.SetDisplay("Trade Mode", "How price interacts with the range before placing orders", "Entries");

		_maPeriod = Param(nameof(MovingAveragePeriod), 100)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Moving average length for trailing", "Risk");

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

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

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

		_previousCandle = null;
		_movingAverage = null;
		_rangeHighIndicator = null;
		_rangeLowIndicator = null;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_priceStep = 0;
		_candlesSinceLastTrade = 0;
	}

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

		if (TradingStartHour < 0 || TradingStartHour > 23)
		throw new InvalidOperationException("TradingStartHour must be between 0 and 23.");

		if (TradingEndHour < 0 || TradingEndHour > 23)
		throw new InvalidOperationException("TradingEndHour must be between 0 and 23.");

		if (TradingStartHour >= TradingEndHour)
		throw new InvalidOperationException("TradingStartHour must be strictly less than TradingEndHour.");

		if (UseTrailingAtHighLow && UseTrailingMa)
		throw new InvalidOperationException("Only one trailing mode can be enabled at a time.");

		_priceStep = Security?.PriceStep ?? 1m;
		_movingAverage = new SimpleMovingAverage { Length = MovingAveragePeriod };

		var subscription = SubscribeCandles(CandleType);

		if (RangeBars > 0)
		{
			_rangeHighIndicator = new Highest { Length = Math.Max(2, RangeBars) };
			_rangeLowIndicator = new Lowest { Length = Math.Max(2, RangeBars) };

			subscription
			.Bind(_movingAverage, _rangeHighIndicator, _rangeLowIndicator, ProcessCandleWithRange)
			.Start();
		}
		else
		{
			subscription
			.Bind(_movingAverage, ProcessCandleWithoutRange)
			.Start();
		}

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

			if (UseTrailingMa)
			DrawIndicator(area, _movingAverage);

			DrawOwnTrades(area);
		}
	}

	private void ProcessCandleWithoutRange(ICandleMessage candle, decimal maValue)
	{
		ProcessCandleInternal(candle, maValue, null, null);
	}

	private void ProcessCandleWithRange(ICandleMessage candle, decimal maValue, decimal rangeHigh, decimal rangeLow)
	{
		var highValue = _rangeHighIndicator != null && _rangeHighIndicator.IsFormed ? rangeHigh : (decimal?)null;
		var lowValue = _rangeLowIndicator != null && _rangeLowIndicator.IsFormed ? rangeLow : (decimal?)null;

		ProcessCandleInternal(candle, maValue, highValue, lowValue);
	}

	private void ProcessCandleInternal(ICandleMessage candle, decimal maValue, decimal? rangeHigh, decimal? rangeLow)
	{
		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		var hour = candle.CloseTime.Hour;
		if (hour < TradingStartHour || hour > TradingEndHour)
		{
			_previousCandle = candle;
			return;
		}

		// Check SL/TP first.
		CheckProtection(candle);

		var netOffset = ToPrice(NetStepPoints);

		if (Position == 0 && netOffset > 0m)
		{
			_candlesSinceLastTrade++;

			if (_candlesSinceLastTrade > CooldownCandles)
			{
				// Flat - check if candle broke through entry levels.
				var allowEntries = true;

				if (rangeHigh.HasValue && rangeLow.HasValue && rangeHigh.Value > rangeLow.Value)
				{
					allowEntries = TradeMode switch
					{
						TradeModeOptions.EdgesOfRange => candle.ClosePrice >= rangeHigh.Value || candle.ClosePrice <= rangeLow.Value,
						TradeModeOptions.CenterOfRange => Math.Abs(candle.ClosePrice - ((rangeHigh.Value + rangeLow.Value) / 2m)) <= _priceStep,
						_ => true,
					};
				}

				if (allowEntries && _previousCandle != null)
				{
					var buyLevel = _previousCandle.ClosePrice + netOffset;
					var sellLevel = _previousCandle.ClosePrice - netOffset;

					if (candle.HighPrice >= buyLevel)
					{
						BuyMarket(EntryVolume);
						_entryPrice = candle.ClosePrice;
						_candlesSinceLastTrade = 0;
						SetProtectionLevels(true, candle, maValue);
					}
					else if (candle.LowPrice <= sellLevel)
					{
						SellMarket(EntryVolume);
						_entryPrice = candle.ClosePrice;
						_candlesSinceLastTrade = 0;
						SetProtectionLevels(false, candle, maValue);
					}
				}
			}
		}
		else if (Position > 0)
		{
			// Update trailing stop for longs.
			UpdateLongTrailing(candle, maValue);
		}
		else if (Position < 0)
		{
			// Update trailing stop for shorts.
			UpdateShortTrailing(candle, maValue);
		}

		_previousCandle = candle;
	}

	private void SetProtectionLevels(bool isLong, ICandleMessage candle, decimal maValue)
	{
		var stopLossOffset = ToPrice(StopLossPoints);
		var takeProfitOffset = ToPrice(TakeProfitPoints);

		if (isLong)
		{
			_stopPrice = stopLossOffset > 0m ? _entryPrice - stopLossOffset : null;
			_takePrice = takeProfitOffset > 0m ? _entryPrice + takeProfitOffset : null;
		}
		else
		{
			_stopPrice = stopLossOffset > 0m ? _entryPrice + stopLossOffset : null;
			_takePrice = takeProfitOffset > 0m ? _entryPrice - takeProfitOffset : null;
		}
	}

	private void CheckProtection(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetProtection();
				return;
			}
			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Position);
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}
			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
			}
		}
	}

	private void UpdateLongTrailing(ICandleMessage candle, decimal maValue)
	{
		var trailingOffset = ToPrice(TrailingStopPoints);
		decimal? newStop = _stopPrice;

		if (UseTrailingAtHighLow && _previousCandle != null)
		{
			var candidate = _previousCandle.LowPrice;
			if (candidate > 0m && (newStop == null || candidate > newStop.Value))
				newStop = candidate;
		}
		else if (UseTrailingMa && maValue > 0m)
		{
			if (newStop == null || maValue > newStop.Value)
				newStop = maValue;
		}
		else if (trailingOffset > 0m)
		{
			var candidate = candle.ClosePrice - trailingOffset;
			if (newStop == null || candidate > newStop.Value)
				newStop = candidate;
		}

		if (newStop.HasValue)
		{
			var maxStop = candle.ClosePrice - _priceStep;
			newStop = Math.Min(newStop.Value, maxStop);
			newStop = Math.Max(newStop.Value, 0m);
		}

		_stopPrice = newStop;
	}

	private void UpdateShortTrailing(ICandleMessage candle, decimal maValue)
	{
		var trailingOffset = ToPrice(TrailingStopPoints);
		decimal? newStop = _stopPrice;

		if (UseTrailingAtHighLow && _previousCandle != null)
		{
			var candidate = _previousCandle.HighPrice;
			if (candidate > 0m && (newStop == null || candidate < newStop.Value))
				newStop = candidate;
		}
		else if (UseTrailingMa && maValue > 0m)
		{
			if (newStop == null || maValue < newStop.Value)
				newStop = maValue;
		}
		else if (trailingOffset > 0m)
		{
			var candidate = candle.ClosePrice + trailingOffset;
			if (newStop == null || candidate < newStop.Value)
				newStop = candidate;
		}

		if (newStop.HasValue)
		{
			var minStop = candle.ClosePrice + _priceStep;
			newStop = Math.Max(newStop.Value, minStop);
		}

		_stopPrice = newStop;
	}

	private void ResetProtection()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_candlesSinceLastTrade = 0;
	}

	private decimal ToPrice(decimal points)
	{
		if (points <= 0m)
			return 0m;

		var step = _priceStep > 0m ? _priceStep : 1m;
		return points * step;
	}
}