Ver en GitHub

PosNegDiCrossoverEstrategia

Descripción general

PosNegDiCrossoverStrategy es un puerto StockSharp del MetaTrader experto _HPCS_PosNegDIsCrossOver_Mt4_EA_V01_WE. el El sistema original escucha los cruces entre las líneas +DI y -DI del índice direccional promedio (ADX) e inmediatamente abre una posición en la dirección del nuevo líder. Cada posición está protegida por stop-loss y take-profit simétricos. Los umbrales se miden en pips y las operaciones perdedoras desencadenan un ciclo de recuperación estilo martingala que vuelve a entrar con un volumen multiplicado. hasta que se alcanza un número fijo de intentos o se produce una salida rentable.

Lógica comercial

  1. Detección de señal: cuando la vela terminada entrega nuevos valores ADX, la estrategia compara el +DI y el -DI actuales lecturas con las anteriores. Aparece una señal alcista cuando +DI cruza por encima de -DI, mientras que se genera una señal bajista cuando +DI cruza por debajo de -DI. Sólo se permite una entrada inicial por barra para reflejar la protección MQL que evitó operaciones duplicadas en la misma vela.
  2. Filtro de tiempo: las entradas solo se permiten dentro de una ventana diaria definida por el usuario. Fuera de la ventana la estrategia sigue gestionando posiciones activas (stops virtuales y toma de ganancias) pero no abre nuevos ciclos ni continúa una secuencia de martingala.
  3. Colocación de órdenes: se envía una orden de mercado en la dirección detectada con el volumen base configurado. Después del llenado el La estrategia convierte TakeProfitPips y StopLossPips en precios absolutos usando el paso del instrumento (un multiplicador de 10x es aplicado para instrumentos cotizados con 3 o 5 decimales) y almacena esos niveles para controles de salida manuales.
  4. Manejo de protección – se inspecciona cada vela terminada: una posición larga se cierra si el mínimo perfora el stop o si el alto alcanza el objetivo; Las posiciones cortas utilizan las condiciones simétricas. Las salidas se ejecutan con órdenes de mercado por lo que el ciclo Puede evaluar el resultado antes de decidir el siguiente paso.
  5. Martingale bucle – después de una pérdida, la estrategia multiplica el volumen actual por MartingaleMultiplier, incrementa el ciclo contador e inmediatamente vuelve a entrar en la misma dirección (respetando la ventana de negociación). Cuando se produce una salida rentable o la El número de intentos llega a MartingaleCycleLimit, el ciclo se restablece al volumen base y espera el siguiente cruce ADX.

Parámetros

Nombre Predeterminado Descripción
CandleType plazo de 15 minutos Serie de velas utilizadas para cálculos ADX y monitoreo de parada/objetivo.
AdxPeriod 14 Longitud del indicador del índice direccional promedio.
UseTimeFilter true Habilita la ventana de negociación diaria.
StartTime 00:00 Inicio de la sesión de negociación (hora de cambio).
StopTime 23:59 Fin de la sesión de negociación (hora de cambio).
OrderVolume 0.1 Volumen inicial de órdenes de mercado para cada ciclo.
TakeProfitPips 10 Distancia al objetivo de ganancias en pips (convertida a precio mediante el paso del instrumento).
StopLossPips 10 Distancia al tope de protección en pips.
MartingaleMultiplier 2 Multiplicador de volumen aplicado después de cada operación perdedora durante el ciclo de martingala.
MartingaleCycleLimit 5 Número máximo de reingresos de martingala permitidos para la misma señal.

Notas

  • La estrategia verifica IsFormedAndOnlineAndAllowTrading() antes de enviar cualquier orden, asegurando una inicialización y riesgo adecuados. controles desde el marco.
  • El manejo virtual de stop-loss y take-profit imita el comportamiento MetaTrader donde las órdenes de protección se adjuntan directamente al posición. Se evalúan en velas terminadas para que sigan siendo compatibles con el StockSharp API de alto nivel.
  • Cuando la ventana de negociación está deshabilitada (ya sea por parámetro o estableciendo tiempos de inicio y finalización idénticos), la estrategia se comporta como un sistema 24 horas al día, 5 días a la semana, idéntico al experto original con is_start y is_stop cubriendo todo el día.
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 MetaTrader expert "_HPCS_PosNegDIsCrossOver_Mt4_EA_V01_WE".
/// Trades +DI/-DI crossovers of the ADX indicator and applies a martingale re-entry loop after losing trades.
/// </summary>
public class PosNegDiCrossoverStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<bool> _useTimeFilter;
	private readonly StrategyParam<TimeSpan> _startTime;
	private readonly StrategyParam<TimeSpan> _stopTime;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _martingaleMultiplier;
	private readonly StrategyParam<int> _martingaleCycleLimit;

	private decimal _previousPlusDi;
	private decimal _previousMinusDi;
	private bool _diInitialized;

	private bool _cycleActive;
	private Sides? _cycleSide;
	private decimal _currentVolume;
	private int _currentCycle;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	private bool _awaitingCycleResolution;
	private bool _lastExitWasLoss;

	private DateTimeOffset? _lastSignalTime;

	/// <summary>
	/// Initializes a new instance of the <see cref="PosNegDiCrossoverStrategy"/> class.
	/// </summary>
	public PosNegDiCrossoverStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for indicator calculations", "General");

		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Length of the Average Directional Index", "Indicators")
			
			.SetOptimize(7, 50, 1);

		_useTimeFilter = Param(nameof(UseTimeFilter), true)
			.SetDisplay("Use Time Filter", "Restrict entries to a daily time window", "Schedule");

		_startTime = Param(nameof(StartTime), new TimeSpan(0, 0, 0))
			.SetDisplay("Start Time", "Daily time when trading becomes available", "Schedule");

		_stopTime = Param(nameof(StopTime), new TimeSpan(23, 59, 0))
			.SetDisplay("Stop Time", "Daily time after which new entries are blocked", "Schedule");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Baseline market order volume", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
			.SetNotNegative()
			.SetDisplay("Take-Profit (pips)", "Distance to the profit target expressed in pips", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 10m)
			.SetNotNegative()
			.SetDisplay("Stop-Loss (pips)", "Distance to the protective stop expressed in pips", "Risk");

		_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Martingale Multiplier", "Volume multiplier applied after a loss", "Money Management");

		_martingaleCycleLimit = Param(nameof(MartingaleCycleLimit), 5)
			.SetGreaterThanZero()
			.SetDisplay("Martingale Cycle Limit", "Maximum number of martingale steps per signal", "Money Management");
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period of the Average Directional Index indicator.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Enable or disable the trading time window.
	/// </summary>
	public bool UseTimeFilter
	{
		get => _useTimeFilter.Value;
		set => _useTimeFilter.Value = value;
	}

	/// <summary>
	/// Daily start time of the trading window.
	/// </summary>
	public TimeSpan StartTime
	{
		get => _startTime.Value;
		set => _startTime.Value = value;
	}

	/// <summary>
	/// Daily end time of the trading window.
	/// </summary>
	public TimeSpan StopTime
	{
		get => _stopTime.Value;
		set => _stopTime.Value = value;
	}

	/// <summary>
	/// Base market order volume used to open a new cycle.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Volume multiplier applied after a losing trade.
	/// </summary>
	public decimal MartingaleMultiplier
	{
		get => _martingaleMultiplier.Value;
		set => _martingaleMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of martingale steps executed per signal.
	/// </summary>
	public int MartingaleCycleLimit
	{
		get => _martingaleCycleLimit.Value;
		set => _martingaleCycleLimit.Value = value;
	}

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

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

		_diInitialized = false;
		_previousPlusDi = 0m;
		_previousMinusDi = 0m;

		ResetCycle();
		_lastSignalTime = null;
	}

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

		ResetCycle();

		var adx = new AverageDirectionalIndex { Length = AdxPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(adx, ProcessCandle)
			.Start();

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

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

		HandleOpenPosition(candle);

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			return;
		}

		var value = (AverageDirectionalIndexValue)adxValue;
		if (value.Dx.Plus is not decimal plusDi || value.Dx.Minus is not decimal minusDi)
		{
			return;
		}

		if (!_diInitialized)
		{
			_previousPlusDi = plusDi;
			_previousMinusDi = minusDi;
			_diInitialized = true;
			return;
		}

		var bullishCross = plusDi > minusDi && _previousPlusDi <= _previousMinusDi;
		var bearishCross = plusDi < minusDi && _previousPlusDi >= _previousMinusDi;

		var time = candle.CloseTime;
		var withinWindow = !UseTimeFilter || IsWithinTradingWindow(time.TimeOfDay);

		if (withinWindow && !_cycleActive && !_awaitingCycleResolution)
		{
			if (bullishCross && Position <= 0m && !IsSameSignalBar(candle.OpenTime))
			{
				StartNewCycle(Sides.Buy);
				_lastSignalTime = candle.OpenTime;
			}
			else if (bearishCross && Position >= 0m && !IsSameSignalBar(candle.OpenTime))
			{
				StartNewCycle(Sides.Sell);
				_lastSignalTime = candle.OpenTime;
			}
		}

		_previousPlusDi = plusDi;
		_previousMinusDi = minusDi;
	}

	private void HandleOpenPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_awaitingCycleResolution)
			{
				return;
			}

			var exitVolume = Math.Abs(Position);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				// Long stop-loss reached inside the finished bar range.
				ExecuteExit(Sides.Sell, exitVolume, true);
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				// Long take-profit reached.
				ExecuteExit(Sides.Sell, exitVolume, false);
			}
		}
		else if (Position < 0m)
		{
			if (_awaitingCycleResolution)
			{
				return;
			}

			var exitVolume = Math.Abs(Position);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				// Short stop-loss reached inside the finished bar range.
				ExecuteExit(Sides.Buy, exitVolume, true);
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				// Short take-profit reached.
				ExecuteExit(Sides.Buy, exitVolume, false);
			}
		}
	}

	private void ExecuteExit(Sides exitSide, decimal volume, bool isLoss)
	{
		if (volume <= 0m)
		{
			return;
		}

		if (exitSide == Sides.Buy)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_stopPrice = null;
		_takePrice = null;
		_entryPrice = null;

		_awaitingCycleResolution = true;
		_lastExitWasLoss = isLoss;
	}

	private void StartNewCycle(Sides side)
	{
		var volume = OrderVolume;
		if (volume <= 0m)
		{
			return;
		}

		_cycleActive = true;
		_cycleSide = side;
		_currentCycle = 1;
		_currentVolume = volume;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_awaitingCycleResolution = false;
		_lastExitWasLoss = false;

		if (side == Sides.Buy)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}
	}

	private void ContinueMartingale()
	{
		if (_cycleSide is not Sides side)
		{
			ResetCycle();
			return;
		}

		var volume = _currentVolume;
		if (volume <= 0m)
		{
			ResetCycle();
			return;
		}

		if (UseTimeFilter && !IsWithinTradingWindow(CurrentTime.TimeOfDay))
		{
			ResetCycle();
			return;
		}

		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_awaitingCycleResolution = false;
		_lastExitWasLoss = false;

		if (side == Sides.Buy)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order.Security != Security)
		{
			return;
		}

		if (_cycleSide is not Sides side)
		{
			return;
		}

		var direction = trade.Order.Side;
		if ((side == Sides.Buy && direction != Sides.Buy) || (side == Sides.Sell && direction != Sides.Sell))
		{
			return;
		}

		// Store the most recent entry price to recalculate protective levels.
		_entryPrice = trade.Order.AveragePrice ?? trade.Trade.Price;
		UpdateProtectionLevels();
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position != 0m)
		{
			return;
		}

		if (_awaitingCycleResolution)
		{
			if (_lastExitWasLoss && _cycleActive && _currentCycle < MartingaleCycleLimit)
			{
				_currentCycle++;
				_currentVolume *= MartingaleMultiplier;
				ContinueMartingale();
			}
			else
			{
				ResetCycle();
			}

			_awaitingCycleResolution = false;
			_lastExitWasLoss = false;
		}
		else if (_cycleActive)
		{
			// Position was closed externally; stop the martingale loop.
			ResetCycle();
		}
	}

	private void UpdateProtectionLevels()
	{
		if (_entryPrice is not decimal entry || _cycleSide is not Sides side)
		{
			return;
		}

		var pip = GetPipSize();
		if (pip <= 0m)
		{
			return;
		}

		_stopPrice = StopLossPips > 0m
			? side == Sides.Buy ? entry - StopLossPips * pip : entry + StopLossPips * pip
			: null;

		_takePrice = TakeProfitPips > 0m
			? side == Sides.Buy ? entry + TakeProfitPips * pip : entry - TakeProfitPips * pip
			: null;
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
		{
			return 0.0001m;
		}

		var step = security.PriceStep ?? 0.0001m;
		if (step <= 0m)
		{
			step = 0.0001m;
		}

		var decimals = security.Decimals;
		return decimals is 3 or 5 ? step * 10m : step;
	}

	private bool IsWithinTradingWindow(TimeSpan timeOfDay)
	{
		var start = StartTime;
		var stop = StopTime;

		if (start == stop)
		{
			return true;
		}

		return start <= stop
			? timeOfDay >= start && timeOfDay <= stop
			: timeOfDay >= start || timeOfDay <= stop;
	}

	private bool IsSameSignalBar(DateTimeOffset candleOpenTime)
	{
		return _lastSignalTime != null && _lastSignalTime.Value == candleOpenTime;
	}

	private void ResetCycle()
	{
		_cycleActive = false;
		_cycleSide = null;
		_currentVolume = OrderVolume;
		_currentCycle = 0;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_awaitingCycleResolution = false;
		_lastExitWasLoss = false;
	}
}