View on GitHub

Vortex Oscillator System Strategy

Overview

The Vortex Oscillator System is a direct port of the MetaTrader 5 expert advisor that relies on the Vortex Oscillator to capture sharp shifts between positive and negative directional movement. The oscillator is constructed as the spread between the Vortex positive line (VI+) and the Vortex negative line (VI-) calculated on the selected candle series. Deep negative readings indicate that VI- dominates VI+, while strong positive values show VI+ leadership. The strategy interprets those extremes as potential inflection zones and reacts with mean-reversion style entries backed by oscillator-driven exits.

How the Strategy Works

  1. Candles are built using the configured timeframe and fed to the built-in VortexIndicator.
  2. Once the indicator becomes formed, the oscillator value is derived as VI+ - VI- on every finished candle.
  3. The oscillator is compared against user-defined thresholds:
    • When it falls below the buy threshold, a long setup is detected.
    • When it rises above the sell threshold, a short setup is detected.
  4. Optional filters can restrict long signals to the zone between the buy threshold and a dedicated stop-loss level (and vice versa for short signals).
  5. Whenever a new setup appears, the strategy closes any opposite position and opens a trade in the signal direction with the configured volume.
  6. Open positions are continuously monitored. If the oscillator hits the configured stop-loss or take-profit boundaries, the position is closed immediately.

This sequence reproduces the original MetaTrader logic: trades are evaluated only on completed bars, both directions are mutually exclusive, and oscillator-based protective rules govern exits.

Entry Rules

  • Long entry
    • Triggered when the oscillator is less than or equal to the buy threshold.
    • If the long stop-loss option is enabled, the oscillator must also remain above the long stop-loss level.
    • Any active short position is closed before opening the long trade.
  • Short entry
    • Triggered when the oscillator is greater than or equal to the sell threshold.
    • If the short stop-loss option is enabled, the oscillator must also remain below the short stop-loss level.
    • Any active long position is closed before opening the short trade.
  • If the oscillator value is between the buy and sell thresholds, all setups are cancelled and no position change occurs.

Exit Rules

  • Long positions
    • Close immediately when the oscillator crosses below or equals the long stop-loss level (if enabled).
    • Close immediately when the oscillator rises to or above the long take-profit level (if enabled).
  • Short positions
    • Close immediately when the oscillator crosses above or equals the short stop-loss level (if enabled).
    • Close immediately when the oscillator falls to or below the short take-profit level (if enabled).

The exit checks are performed after every candle close, guaranteeing a faithful recreation of the MT5 monitoring loop.

Parameters

  • Vortex Length – lookback period for the Vortex indicator (default 14).
  • Candle Type – timeframe used for building candles supplied to the indicator.
  • Use Buy Stop Loss – enables the oscillator-based stop-loss filter and exit for long trades.
  • Use Buy Take Profit – enables the oscillator-based take-profit exit for long trades.
  • Use Sell Stop Loss – enables the oscillator-based stop-loss filter and exit for short trades.
  • Use Sell Take Profit – enables the oscillator-based take-profit exit for short trades.
  • Buy Threshold – oscillator value that qualifies a long entry (default -0.75).
  • Buy Stop Loss Level – oscillator value that closes long positions when the long stop-loss option is active (default -1.00).
  • Buy Take Profit Level – oscillator value that closes long positions when the long take-profit option is active (default 0.00).
  • Sell Threshold – oscillator value that qualifies a short entry (default 0.75).
  • Sell Stop Loss Level – oscillator value that closes short positions when the short stop-loss option is active (default 1.00).
  • Sell Take Profit Level – oscillator value that closes short positions when the short take-profit option is active (default 0.00).
  • Volume – trade size used for new positions (default 0.1, matching the original expert advisor).

Implementation Notes

  • Processing occurs strictly on completed candles to avoid duplicating signals within the same bar.
  • Oscillator thresholds can be optimized thanks to the provided ranges in the parameter metadata.
  • The strategy automatically flips positions by sending a market order large enough to close the opposing side and establish the new exposure.
  • Stop-loss and take-profit features work independently; enabling one does not require the other.
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>
/// Vortex oscillator trading system ported from the MetaTrader implementation.
/// Opens long positions when the oscillator drops below a configured level and
/// shorts when the oscillator rises above the upper threshold.
/// Optional stop-loss and take-profit rules monitor the oscillator value to exit positions.
/// </summary>
public class VortexOscillatorSystemStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useBuyStopLoss;
	private readonly StrategyParam<bool> _useBuyTakeProfit;
	private readonly StrategyParam<bool> _useSellStopLoss;
	private readonly StrategyParam<bool> _useSellTakeProfit;
	private readonly StrategyParam<decimal> _buyThreshold;
	private readonly StrategyParam<decimal> _buyStopLossLevel;
	private readonly StrategyParam<decimal> _buyTakeProfitLevel;
	private readonly StrategyParam<decimal> _sellThreshold;
	private readonly StrategyParam<decimal> _sellStopLossLevel;
	private readonly StrategyParam<decimal> _sellTakeProfitLevel;

	private VortexIndicator _vortexIndicator = null!;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public VortexOscillatorSystemStrategy()
	{
		_length = Param(nameof(Length), 14)
			.SetGreaterThanZero()
			.SetDisplay("Vortex Length", "Period used for the Vortex indicator.", "General")
			
			.SetOptimize(7, 28, 7);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build candles for calculations.", "General");

		_useBuyStopLoss = Param(nameof(UseBuyStopLoss), false)
			.SetDisplay("Use Buy Stop Loss", "Enable oscillator-based stop loss for long positions.", "Risk Management");

		_useBuyTakeProfit = Param(nameof(UseBuyTakeProfit), false)
			.SetDisplay("Use Buy Take Profit", "Enable oscillator-based take profit for long positions.", "Risk Management");

		_useSellStopLoss = Param(nameof(UseSellStopLoss), false)
			.SetDisplay("Use Sell Stop Loss", "Enable oscillator-based stop loss for short positions.", "Risk Management");

		_useSellTakeProfit = Param(nameof(UseSellTakeProfit), false)
			.SetDisplay("Use Sell Take Profit", "Enable oscillator-based take profit for short positions.", "Risk Management");

		_buyThreshold = Param(nameof(BuyThreshold), -0.1m)
			.SetDisplay("Buy Threshold", "Oscillator value that triggers a long setup.", "Signals")
			
			.SetOptimize(-1.5m, -0.25m, 0.25m);

		_buyStopLossLevel = Param(nameof(BuyStopLossLevel), -1m)
			.SetDisplay("Buy Stop Loss Level", "Oscillator value that closes long trades when stop loss is enabled.", "Signals");

		_buyTakeProfitLevel = Param(nameof(BuyTakeProfitLevel), 0m)
			.SetDisplay("Buy Take Profit Level", "Oscillator value that closes long trades when take profit is enabled.", "Signals");

		_sellThreshold = Param(nameof(SellThreshold), 0.1m)
			.SetDisplay("Sell Threshold", "Oscillator value that triggers a short setup.", "Signals")
			
			.SetOptimize(0.25m, 1.5m, 0.25m);

		_sellStopLossLevel = Param(nameof(SellStopLossLevel), 1m)
			.SetDisplay("Sell Stop Loss Level", "Oscillator value that closes short trades when stop loss is enabled.", "Signals");

		_sellTakeProfitLevel = Param(nameof(SellTakeProfitLevel), 0m)
			.SetDisplay("Sell Take Profit Level", "Oscillator value that closes short trades when take profit is enabled.", "Signals");

		Volume = 0.1m;
	}

	/// <summary>
	/// Vortex indicator lookback length.
	/// </summary>
	public int Length
	{
		get => _length.Value;
		set => _length.Value = value;
	}

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

	/// <summary>
	/// Enable oscillator-based stop loss for long positions.
	/// </summary>
	public bool UseBuyStopLoss
	{
		get => _useBuyStopLoss.Value;
		set => _useBuyStopLoss.Value = value;
	}

	/// <summary>
	/// Enable oscillator-based take profit for long positions.
	/// </summary>
	public bool UseBuyTakeProfit
	{
		get => _useBuyTakeProfit.Value;
		set => _useBuyTakeProfit.Value = value;
	}

	/// <summary>
	/// Enable oscillator-based stop loss for short positions.
	/// </summary>
	public bool UseSellStopLoss
	{
		get => _useSellStopLoss.Value;
		set => _useSellStopLoss.Value = value;
	}

	/// <summary>
	/// Enable oscillator-based take profit for short positions.
	/// </summary>
	public bool UseSellTakeProfit
	{
		get => _useSellTakeProfit.Value;
		set => _useSellTakeProfit.Value = value;
	}

	/// <summary>
	/// Oscillator level used to open long trades.
	/// </summary>
	public decimal BuyThreshold
	{
		get => _buyThreshold.Value;
		set => _buyThreshold.Value = value;
	}

	/// <summary>
	/// Oscillator level that closes long trades when stop loss is enabled.
	/// </summary>
	public decimal BuyStopLossLevel
	{
		get => _buyStopLossLevel.Value;
		set => _buyStopLossLevel.Value = value;
	}

	/// <summary>
	/// Oscillator level that closes long trades when take profit is enabled.
	/// </summary>
	public decimal BuyTakeProfitLevel
	{
		get => _buyTakeProfitLevel.Value;
		set => _buyTakeProfitLevel.Value = value;
	}

	/// <summary>
	/// Oscillator level used to open short trades.
	/// </summary>
	public decimal SellThreshold
	{
		get => _sellThreshold.Value;
		set => _sellThreshold.Value = value;
	}

	/// <summary>
	/// Oscillator level that closes short trades when stop loss is enabled.
	/// </summary>
	public decimal SellStopLossLevel
	{
		get => _sellStopLossLevel.Value;
		set => _sellStopLossLevel.Value = value;
	}

	/// <summary>
	/// Oscillator level that closes short trades when take profit is enabled.
	/// </summary>
	public decimal SellTakeProfitLevel
	{
		get => _sellTakeProfitLevel.Value;
		set => _sellTakeProfitLevel.Value = value;
	}

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

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

		_vortexIndicator = new VortexIndicator
		{
			Length = Length,
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(_vortexIndicator, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue vortexValue)
	{
		// Process only finished candles to mirror bar-close logic from the original script.
		if (candle.State != CandleStates.Finished)
			return;

		if (!_vortexIndicator.IsFormed)
			return;

		var vortexTyped = (VortexIndicatorValue)vortexValue;
		var viPlus = vortexTyped.PlusVi ?? 0m;
		var viMinus = vortexTyped.MinusVi ?? 0m;

		// Vortex oscillator equals the difference between VI+ and VI- lines.
		var oscillator = viPlus - viMinus;

		var longSetupExists = false;
		var shortSetupExists = false;

		// Long setups are considered when the oscillator falls below the buy threshold.
		if (UseBuyStopLoss)
		{
			if (oscillator <= BuyThreshold && oscillator > BuyStopLossLevel)
			{
				longSetupExists = true;
				shortSetupExists = false;
			}
		}
		else if (oscillator <= BuyThreshold)
		{
			longSetupExists = true;
			shortSetupExists = false;
		}

		// Short setups require the oscillator to rise above the sell threshold.
		if (UseSellStopLoss)
		{
			if (oscillator >= SellThreshold && oscillator < SellStopLossLevel)
			{
				shortSetupExists = true;
				longSetupExists = false;
			}
		}
		else if (oscillator >= SellThreshold)
		{
			shortSetupExists = true;
			longSetupExists = false;
		}

		// Neutral zone cancels both long and short intentions.
		if (oscillator >= BuyThreshold && oscillator <= SellThreshold)
		{
			longSetupExists = false;
			shortSetupExists = false;
		}

		var currentPosition = Position;

		if (longSetupExists && currentPosition <= 0)
		{
			// Close existing shorts and open a long position when a valid long setup appears.
			BuyMarket();
		}
		else if (shortSetupExists && currentPosition >= 0)
		{
			// Close existing longs and open a short position when a valid short setup appears.
			SellMarket();
		}

		currentPosition = Position;

		if (currentPosition > 0)
		{
			// Manage long positions with oscillator-based stops and targets.
			if (UseBuyStopLoss && oscillator <= BuyStopLossLevel)
			{
				SellMarket();
				return;
			}

			if (UseBuyTakeProfit && oscillator >= BuyTakeProfitLevel)
			{
				SellMarket();
				return;
			}
		}
		else if (currentPosition < 0)
		{
			// Manage short positions with oscillator-based stops and targets.
			if (UseSellStopLoss && oscillator >= SellStopLossLevel)
			{
				BuyMarket();
				return;
			}

			if (UseSellTakeProfit && oscillator <= SellTakeProfitLevel)
			{
				BuyMarket();
			}
		}
	}
}