Открыть на GitHub

PosNegDiCrossoverStrategy

Обзор

PosNegDiCrossoverStrategy — это перенос MetaTrader-советника _HPCS_PosNegDIsCrossOver_Mt4_EA_V01_WE на платформу StockSharp. Алгоритм анализирует пересечения линий +DI и -DI индикатора ADX, открывая сделки в сторону нового лидера. Каждая позиция страхуется симметричными стоп-лоссом и тейк-профитом (в пунктах), а серия убыточных сделок сопровождается мартингейл-петлей: объём умножается на заданный коэффициент до достижения прибыли или лимита попыток.

Логика работы

  1. Определение сигнала. После закрытия свечи стратегия получает свежие значения ADX и сравнивает текущие +DI/-DI с предыдущими. Пересечение +DI вверх даёт сигнал на покупку, пересечение вниз — на продажу. Как и в оригинальном коде, на одну свечу приходится только один стартовый вход.
  2. Временной фильтр. Новые сделки разрешены только внутри окна StartTimeStopTime. Вне интервала стратегия продолжает сопровождать открытые позиции (виртуальные стопы и цели), но не запускает новые циклы и не продолжает мартингейл.
  3. Выставление заявок. При появлении сигнала отправляется рыночная заявка объёмом OrderVolume. После исполнения значения TakeProfitPips и StopLossPips переводятся в абсолютные цены с учётом шага цены инструмента (для трёх- и пятизнаковых котировок используется множитель 10) и сохраняются для дальнейшей проверки.
  4. Сопровождение позиции. По завершении каждой свечи проверяется диапазон: для лонга пробой минимума ниже стопа или достижение максимума выше цели приводит к закрытию рыночной заявкой; для шорта условия зеркальные. Такой подход позволяет мгновенно классифицировать исход сделки.
  5. Мартингейл. Если выход был убыточным, объём умножается на MartingaleMultiplier, счётчик цикла увеличивается и выполняется повторный вход в ту же сторону (при условии, что время всё ещё подходит). После прибыльного выхода или при достижении MartingaleCycleLimit цикл сбрасывается и стратегия ждёт следующего пересечения ADX.

Параметры

Имя Значение по умолчанию Описание
CandleType таймфрейм 15 минут Свечи, по которым рассчитывается ADX и проверяются стоп/тейк.
AdxPeriod 14 Длина индикатора ADX.
UseTimeFilter true Включает фильтр торгового окна.
StartTime 00:00 Начало торгового окна (биржевое время).
StopTime 23:59 Конец торгового окна (биржевое время).
OrderVolume 0,1 Базовый объём рыночных заявок.
TakeProfitPips 10 Дистанция до цели в пунктах (переводится в цену).
StopLossPips 10 Дистанция до стоп-лосса в пунктах.
MartingaleMultiplier 2 Множитель объёма после убыточной сделки.
MartingaleCycleLimit 5 Максимальное число мартингейл-попыток для одного сигнала.

Дополнительные сведения

  • Перед выставлением заявок вызывается IsFormedAndOnlineAndAllowTrading(), чтобы убедиться в готовности подключений и разрешении торговли.
  • Виртуальные защитные уровни повторяют механику MetaTrader, где стоп и тейк задаются для позиции напрямую; обработка выполняется на закрытии свечей и не требует низкоуровневых API.
  • Если StartTime и StopTime совпадают или UseTimeFilter отключён, стратегия работает круглосуточно, как и исходный эксперт с параметрами is_start/is_stop, охватывающими весь день.
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;
	}
}