Открыть на GitHub

Стратегия Vortex Indicator System

Стратегия переносит советник MetaTrader Vortex Indicator System.mq4 на высокоуровневый API StockSharp. Исходная идея была опу бликована в журнале Technical Analysis of Stocks & Commodities (январь 2010 года) и строится на пересечениях индикатора Vortex. После пересечения экстремум бара используется как точка пробоя: при достижении этого уровня следующая свеча запускает рыночную сделку. Перенос сохраняет тот же алгоритм – противоположная позиция закрывается при сигнале, уровень пробоя записывается, а след ующая свеча, пробивающая его, активирует заявку.

Как работает стратегия

  1. Открывается подписка на свечи типа CandleType. Поток данных привязывается к одному экземпляру VortexIndicator через метод Bind, поэтому стратегия синхронно получает значения VI+ и VI- для завершённых свечей.
  2. После формирования индикатора стратегия хранит значения VI предыдущей свечи, чтобы обнаруживать те же условия пересечения, чт о и в MQL-версии: VI+ пересекает VI- сверху или снизу между двумя последними закрытыми барами.
  3. Этап подготовки – при бычьем пересечении любая открытая короткая позиция закрывается, а максимум бара пересечения становит ся триггером для лонга. Медвежье пересечение закрывает лонг и сохраняет минимум бара как триггер для шорта.
  4. Этап активации – каждая следующая завершённая свеча проверяет, был ли достигнут уровень триггера (HighPrice ≥ уровень дл я лонга или LowPrice ≤ уровень для шорта). При выполнении условия отправляется рыночная заявка объёмом TradeVolume, увеличенн ым на остаточный противоположный объём (если предыдущее закрытие ещё не выполнено).
  5. После исполнения заявки соответствующий триггер очищается. Если пробой не происходит, подготовка остаётся активной до следующ его пересечения, которое перезапишет уровень.
  6. Выход из позиции осуществляется только по сигналу противоположного пересечения: позиция закрывается немедленно, записывается н овый уровень пробоя, полностью повторяя логику MetaTrader.

Сигналы

  • Бычья подготовка – когда на предыдущей свече VI+VI-, а на текущей закрытой свече VI+ > VI-. Триггером становится максимум этой свечи.
  • Бычья активация – первая свеча, чей максимум достигает уровня, отправляет рыночную покупку объёмом TradeVolume (плюс объё м, необходимый для закрытия оставшегося шорта).
  • Медвежья подготовка – когда ранее VI-VI+, а на текущей закрытой свече VI- > VI+. Триггером становится минимум эт ой свечи.
  • Медвежья активация – первая свеча, чей минимум касается уровня, отправляет рыночную продажу объёмом TradeVolume (плюс объё м, необходимый для закрытия лонга).

Параметры

Параметр Значение по умолчанию Описание
VortexLength 14 Период индикатора Vortex.
CandleType 1 час Таймфрейм свечей и обновления индикатора.
TradeVolume 1 Объём рыночной заявки при открытии позиции.

Особенности реализации

  • Стратегия реагирует только на закрытые свечи согласно требованиям конвертации. Внутридневные пробои фиксируются, когда све ча закрывается с максимумом/минимумом за пределами уровня.
  • В методе OnStopped очищаются все ожидающие триггеры, чтобы при повторном запуске не оставалось старого состояния.
  • При исполнении пробойной сделки объём увеличивается на величину текущей противоположной позиции, что соответствует поведению M etaTrader, где активная позиция закрывалась перед открытием новой.
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Vortex Indicator Breakout: Dual EMA crossover breakout with ATR stops.
/// </summary>
public class VortexIndicatorBreakoutStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;

	public VortexIndicatorBreakoutStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_fastEmaLength = Param(nameof(FastEmaLength), 14)
			.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");

		_slowEmaLength = Param(nameof(SlowEmaLength), 28)
			.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;

		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastEma, slowEma, atr, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0)
		{
			_prevFast = fastVal;
			_prevSlow = slowVal;
			return;
		}

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (fastVal < slowVal && _prevFast >= _prevSlow)
			{
				SellMarket();
				_entryPrice = 0;
			}
			else if (close <= _entryPrice - atrVal * 2m)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow)
			{
				BuyMarket();
				_entryPrice = 0;
			}
			else if (close >= _entryPrice + atrVal * 2m)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (fastVal < slowVal && _prevFast >= _prevSlow)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevFast = fastVal;
		_prevSlow = slowVal;
	}
}