Открыть на GitHub

4153 Система осциллятора Vortex

Обзор

Стратегия повторяет логику советника MetaTrader 4 «Vortex Oscillator System», но реализована на высокоуровневом API StockSharp. Внутри рассчитывается нормализованный осциллятор Vortex на основе стандартных компонентов VI+ и VI-, и далее производится торговля при выходе импульса за нейтральный диапазон. Алгоритм работает с одним инструментом и всегда полностью переворачивает позицию при смене направления.

Правила торговли

  • Поток свечей типа CandleType передаётся в индикатор Vortex с периодом VortexLength. Осциллятор вычисляется по формуле (VI+ - VI-) / (VI+ + VI-), поэтому его значения лежат в диапазоне [-1, 1].
  • Сигнал на покупку активируется, когда осциллятор опускается ниже BuyThreshold и, при включённом флаге UseBuyStopLoss, остаётся выше порога BuyStopLossLevel. Сигнал на продажу активируется, когда осциллятор поднимается выше SellThreshold и, если включён UseSellStopLoss, держится ниже SellStopLossLevel.
  • Когда осциллятор возвращается внутрь нейтральной зоны между BuyThreshold и SellThreshold, оба сигнала сбрасываются — следующий пробой должен начаться из нейтрального состояния.
  • При активном длинном сигнале и нулевой либо короткой позиции регистрируется рыночная покупка объёмом Volume плюс необходимая величина для закрытия текущего шорта. Короткие сигналы работают зеркально: отправляется рыночная продажа объёмом Volume плюс открытый лонг.
  • Позиция может быть закрыта без противоположного сигнала. Если включён UseBuyStopLoss и осциллятор достигает BuyStopLossLevel, длинная позиция закрывается. Параметр UseBuyTakeProfit фиксирует прибыль по лонгу при достижении уровня BuyTakeProfitLevel. Аналогичные проверки для SellStopLossLevel и SellTakeProfitLevel управляют закрытием коротких позиций при активных флагах UseSellStopLoss и UseSellTakeProfit.

Параметры

  • VortexLength – период расчёта индикатора Vortex.
  • CandleType – тип или таймфрейм свечей, используемый для анализа.
  • Volume – базовый объём заявок; при перевороте автоматически добавляется объём для закрытия текущей позиции.
  • BuyThreshold – уровень осциллятора, активирующий длинный сигнал.
  • UseBuyStopLoss – требует, чтобы осциллятор оставался выше BuyStopLossLevel перед формированием длинного сигнала.
  • BuyStopLossLevel – уровень осциллятора, при котором длинная позиция немедленно закрывается при активном стопе.
  • UseBuyTakeProfit – включает фиксацию прибыли по лонгу по уровню осциллятора.
  • BuyTakeProfitLevel – целевой уровень осциллятора для выхода из длинной позиции при включённом тейк-профите.
  • SellThreshold – уровень осциллятора, активирующий короткий сигнал.
  • UseSellStopLoss – требует, чтобы осциллятор оставался ниже SellStopLossLevel перед формированием короткого сигнала.
  • SellStopLossLevel – уровень осциллятора, при котором короткая позиция немедленно закрывается при активном стопе.
  • UseSellTakeProfit – включает фиксацию прибыли по короткой позиции по уровню осциллятора.
  • SellTakeProfitLevel – целевой уровень осциллятора для выхода из шорта при включённом тейк-профите.

Дополнительные заметки

  • На графике автоматически отображаются свечи и сделки стратегии; отдельное окно для осциллятора не создаётся.
  • Благодаря нормализации осциллятора пороговые значения -0.75, 0.75, -1.00 и 1.00 полностью соответствуют оригинальному советнику и могут оптимизироваться средствами StockSharp.
  • Стратегия не допускает одновременных лонгов и шортов: каждый переворот сначала закрывает текущую позицию и затем открывает новую сторону одной рыночной заявкой.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Vortex Oscillator System: RSI momentum breakout with EMA filter and ATR stops.
/// </summary>
public class VortexOscillatorSystemMomentumStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevRsi;
	private decimal _entryPrice;

	public VortexOscillatorSystemMomentumStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "RSI period.", "Indicators");

		_emaLength = Param(nameof(EmaLength), 50)
			.SetDisplay("EMA Length", "Trend filter.", "Indicators");

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

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

	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

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

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

		_prevRsi = 0;
		_entryPrice = 0;
	}

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

		_prevRsi = 0;
		_entryPrice = 0;

		var rsi = new RelativeStrengthIndex { Length = RsiLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, ema, atr, ProcessCandle)
			.Start();

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

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

		if (_prevRsi == 0 || atrVal <= 0)
		{
			_prevRsi = rsiVal;
			return;
		}

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 2.5m || close <= _entryPrice - atrVal * 1.5m || rsiVal < 40)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 2.5m || close >= _entryPrice + atrVal * 1.5m || rsiVal > 60)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (rsiVal > 55 && _prevRsi <= 55 && close > emaVal)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (rsiVal < 45 && _prevRsi >= 45 && close < emaVal)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevRsi = rsiVal;
	}
}