View on GitHub

Omni Trend Strategy

Overview

The Omni Trend strategy is a direct port of the MetaTrader expert "Exp_Omni_Trend". It combines a moving average with an ATR-based channel to detect the dominant trend and to flip between long and short exposure. The StockSharp version keeps the original behaviour, including the delay between signal detection and order execution as well as the ability to disable individual entry or exit legs.

The strategy subscribes to the configured candle series and feeds each finished bar to the Omni Trend logic. The moving average serves as the central tendency estimate, while ATR multiplies build volatility envelopes. The envelopes behave like trailing stops: price closing beyond the previous envelope boundary flips the trend, generates a fresh entry signal in the new direction, and immediately closes any opposing exposure.

If the optional stop-loss and take-profit thresholds are enabled, they act on the broker side in price steps, complementing the indicator-based exits. Position size is controlled through the built-in Volume property of the strategy (default 1).

Trading Logic

  1. Compute the chosen moving average (MaType, MaLength, AppliedPrice) on the candle stream.
  2. Compute ATR (AtrLength) and derive two adaptive bands using VolatilityFactor and MoneyRisk. The upper band protects short positions, the lower band protects long positions.
  3. When price exceeds the previous bar's protective band the trend changes:
    • A bullish breakout (HighPrice above the previous upper band) turns the trend to "up", closes any short position if allowed, and opens a long position after SignalBar completed candles.
    • A bearish breakout (LowPrice below the previous lower band) turns the trend to "down", closes any long position if allowed, and opens a short position after the configured delay.
  4. While the trend stays bullish the strategy continues to request short exits; the symmetric rule applies for a bearish trend and long exits. This mirrors the behaviour of the MetaTrader expert, where the opposite band constantly forces flat exposure against the prevailing direction.
  5. Optional risk management monitors each finished candle. If the current bar reaches the stop or target price (expressed in price steps) the position is closed immediately, resetting the stored entry price.

Signals are scheduled through a FIFO queue. When SignalBar is zero they are executed at the close of the same candle. Otherwise, they are triggered on the open of the candle that completes the delay, which replicates the "previous bar" execution style of the source expert.

Parameters

Name Description Default
CandleType Candle type (timeframe) used for calculations. 4-hour time frame
MaLength Period of the moving average. 13
MaType Moving average method: simple, exponential, smoothed, or linear weighted. Exponential
AppliedPrice Price field passed to the moving average (close, open, high, low, median, typical, weighted). Close
AtrLength ATR period used by the volatility channel. 11
VolatilityFactor Multiplier applied to ATR when building the raw channel. 1.3
MoneyRisk Offset factor that shifts the channel away from the moving average, identical to the MQL input. 0.15
SignalBar Number of completed candles to wait before acting on a signal. 1
EnableBuyOpen Allow opening long positions. true
EnableSellOpen Allow opening short positions. true
EnableBuyClose Allow closing long positions when a bearish trend is detected. true
EnableSellClose Allow closing short positions when a bullish trend is detected. true
StopLossPoints Optional protective stop distance in price steps. Set to 0 to disable. 1000
TakeProfitPoints Optional profit target distance in price steps. Set to 0 to disable. 2000
Volume Strategy property that controls trade size. 1

Notes and Recommendations

  • The StockSharp implementation feeds the same indicator values as the original and reproduces its trend flips. Nevertheless, precise fills depend on the data source and execution latency.
  • Set SignalBar = 1 to mimic the expert adviser default, where orders are executed at the open of the next candle after a signal becomes available. Larger values further delay execution; setting 0 executes on the current close.
  • Stop-loss and take-profit thresholds are expressed in points (price steps). Ensure the connected security exposes a valid PriceStep.
  • The built-in chart draws the candle series, the selected moving average, and the strategy's own trades for quick visual validation.
  • Disable specific entry or exit toggles to restrict the strategy to one-sided operation or to handle exits manually.
  • The strategy does not create pending orders; it issues market orders using BuyMarket and SellMarket just like the source expert's direct order placement.

Files

  • CS/OmniTrendStrategy.cs — C# implementation of the strategy.
  • README.md, README_ru.md, README_zh.md — documentation in English, Russian, and Chinese.

Python support is intentionally omitted as requested.

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>
/// Trend-following strategy that replicates the Omni Trend MetaTrader expert.
/// </summary>
public class OmniTrendStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<MovingAverageMethods> _maType;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _volatilityFactor;
	private readonly StrategyParam<decimal> _moneyRisk;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _enableBuyOpen;
	private readonly StrategyParam<bool> _enableSellOpen;
	private readonly StrategyParam<bool> _enableBuyClose;
	private readonly StrategyParam<bool> _enableSellClose;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private readonly List<SignalInfo> _pendingSignals = new();

	private IIndicator _ma;
	private AverageTrueRange _atr;
	private decimal _previousSmin;
	private decimal _previousSmax;
	private decimal _previousTrendUp;
	private decimal _previousTrendDown;
	private int _previousTrend;
	private bool _isInitialized;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	public OmniTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to build Omni Trend signals", "General")
			;

		_maLength = Param(nameof(MaLength), 13)
			.SetDisplay("MA Length", "Moving average period", "Indicators")
			.SetGreaterThanZero()
			;

		_maType = Param(nameof(MaType), MovingAverageMethods.Exponential)
			.SetDisplay("MA Type", "Moving average calculation method", "Indicators")
			;

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Price field used by the moving average", "Indicators")
			;

		_atrLength = Param(nameof(AtrLength), 11)
			.SetDisplay("ATR Length", "ATR period for volatility bands", "Indicators")
			.SetGreaterThanZero()
			;

		_volatilityFactor = Param(nameof(VolatilityFactor), 1.3m)
			.SetDisplay("Volatility Factor", "Multiplier applied to ATR", "Indicators")
			.SetGreaterThanZero()
			;

		_moneyRisk = Param(nameof(MoneyRisk), 0.15m)
			.SetDisplay("Money Risk", "Offset factor used to position trend bands", "Indicators")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 0)
			.SetDisplay("Signal Bar", "Delay in bars before acting on a signal", "Trading")
			;

		_enableBuyOpen = Param(nameof(EnableBuyOpen), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions", "Trading");

		_enableSellOpen = Param(nameof(EnableSellOpen), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions", "Trading");

		_enableBuyClose = Param(nameof(EnableBuyClose), true)
			.SetDisplay("Enable Long Exits", "Allow closing long positions", "Trading");

		_enableSellClose = Param(nameof(EnableSellClose), true)
			.SetDisplay("Enable Short Exits", "Allow closing short positions", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price steps", "Risk");

		Volume = 1m;
	}

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

	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = Math.Max(1, value);
	}

	public MovingAverageMethods MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = Math.Max(1, value);
	}

	public decimal VolatilityFactor
	{
		get => _volatilityFactor.Value;
		set => _volatilityFactor.Value = value;
	}

	public decimal MoneyRisk
	{
		get => _moneyRisk.Value;
		set => _moneyRisk.Value = value;
	}

	public int SignalBar
	{
		get => Math.Max(0, _signalBar.Value);
		set => _signalBar.Value = Math.Max(0, value);
	}

	public bool EnableBuyOpen
	{
		get => _enableBuyOpen.Value;
		set => _enableBuyOpen.Value = value;
	}

	public bool EnableSellOpen
	{
		get => _enableSellOpen.Value;
		set => _enableSellOpen.Value = value;
	}

	public bool EnableBuyClose
	{
		get => _enableBuyClose.Value;
		set => _enableBuyClose.Value = value;
	}

	public bool EnableSellClose
	{
		get => _enableSellClose.Value;
		set => _enableSellClose.Value = value;
	}

	public int StopLossPoints
	{
		get => Math.Max(0, _stopLossPoints.Value);
		set => _stopLossPoints.Value = Math.Max(0, value);
	}

	public int TakeProfitPoints
	{
		get => Math.Max(0, _takeProfitPoints.Value);
		set => _takeProfitPoints.Value = Math.Max(0, value);
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pendingSignals.Clear();
		_ma = null;
		_atr = null;
		_previousSmin = 0m;
		_previousSmax = 0m;
		_previousTrendUp = 0m;
		_previousTrendDown = 0m;
		_previousTrend = 0;
		_isInitialized = false;
		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

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

		_ma = CreateMovingAverage(MaType, MaLength);
		_atr = new AverageTrueRange
		{
			Length = AtrLength,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

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

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

		if (_ma is null || _atr is null)
			return;

		var atrValue = _atr.Process(new CandleIndicatorValue(_atr, candle));
		var appliedPrice = GetAppliedPrice(candle, AppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, appliedPrice, candle.OpenTime) { IsFinal = true });

		if (!atrValue.IsFinal || !maValue.IsFinal)
			return;

		CheckRiskManagement(candle);

		var atr = atrValue.GetValue<decimal>();
		var ma = maValue.GetValue<decimal>();
		var signal = CalculateSignal(candle, ma, atr);

		_pendingSignals.Add(signal);
		while (_pendingSignals.Count > SignalBar)
		{
			var pending = _pendingSignals[0];
			try { _pendingSignals.RemoveAt(0); } catch { break; }
			ExecuteSignal(candle, pending);
		}
	}

	private SignalInfo CalculateSignal(ICandleMessage candle, decimal ma, decimal atr)
	{
		var smax = ma + VolatilityFactor * atr;
		var smin = ma - VolatilityFactor * atr;

		if (!_isInitialized)
		{
			_previousSmax = smax;
			_previousSmin = smin;
			_previousTrendUp = 0m;
			_previousTrendDown = 0m;
			_previousTrend = 0;
			_isInitialized = true;
			return SignalInfo.Empty;
		}

		var trend = _previousTrend;
		if (candle.HighPrice > _previousSmax)
			trend = 1;
		else if (candle.LowPrice < _previousSmin)
			trend = -1;

		decimal? trendUp = null;
		decimal? trendDown = null;

		if (trend > 0)
		{
			if (smin < _previousSmin)
				smin = _previousSmin;

			var candidate = smin - (MoneyRisk - 1m) * atr;
			if (_previousTrend > 0 && _previousTrendUp > 0m && candidate < _previousTrendUp)
				candidate = _previousTrendUp;

			trendUp = candidate;
		}
		else if (trend < 0)
		{
			if (smax > _previousSmax)
				smax = _previousSmax;

			var candidate = smax + (MoneyRisk - 1m) * atr;
			if (_previousTrend < 0 && _previousTrendDown > 0m && candidate > _previousTrendDown)
				candidate = _previousTrendDown;

			trendDown = candidate;
		}

		var signal = SignalInfo.Empty;

		if (trend > 0)
		{
			if (_previousTrend <= 0 && trendUp.HasValue && EnableBuyOpen)
				signal.BuyOpen = true;

			if (trendUp.HasValue && EnableSellClose)
				signal.SellClose = true;
		}
		else if (trend < 0)
		{
			if (_previousTrend >= 0 && trendDown.HasValue && EnableSellOpen)
				signal.SellOpen = true;

			if (trendDown.HasValue && EnableBuyClose)
				signal.BuyClose = true;
		}

		_previousTrend = trend;
		_previousSmax = smax;
		_previousSmin = smin;
		_previousTrendUp = trendUp ?? 0m;
		_previousTrendDown = trendDown ?? 0m;

		return signal;
	}

	private void ExecuteSignal(ICandleMessage candle, SignalInfo signal)
	{
		if (signal.BuyClose && Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0)
				SellMarket();
			_longEntryPrice = null;
		}

		if (signal.SellClose && Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0)
				BuyMarket();
			_shortEntryPrice = null;
		}

		var executionPrice = SignalBar == 0 ? candle.ClosePrice : candle.OpenPrice;

		if (signal.BuyOpen && Position <= 0)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
				_shortEntryPrice = null;
			}

			BuyMarket();
			_longEntryPrice = executionPrice;
		}

		if (signal.SellOpen && Position >= 0)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
				_longEntryPrice = null;
			}

			SellMarket();
			_shortEntryPrice = executionPrice;
		}
	}

	private void CheckRiskManagement(ICandleMessage candle)
	{
		if (Security is null)
			return;

		var step = Security?.PriceStep ?? 0.01m;
		if (step <= 0m)
			return;

		if (Position > 0)
		{
			if (StopLossPoints > 0 && _longEntryPrice.HasValue)
			{
				var stopPrice = _longEntryPrice.Value - StopLossPoints * step;
				if (candle.LowPrice <= stopPrice || candle.ClosePrice <= stopPrice)
				{
					SellMarket();
					_longEntryPrice = null;
					return;
				}
			}

			if (TakeProfitPoints > 0 && _longEntryPrice.HasValue)
			{
				var targetPrice = _longEntryPrice.Value + TakeProfitPoints * step;
				if (candle.HighPrice >= targetPrice || candle.ClosePrice >= targetPrice)
				{
					SellMarket();
					_longEntryPrice = null;
					return;
				}
			}
		}
		else if (Position < 0)
		{
			if (StopLossPoints > 0 && _shortEntryPrice.HasValue)
			{
				var stopPrice = _shortEntryPrice.Value + StopLossPoints * step;
				if (candle.HighPrice >= stopPrice || candle.ClosePrice >= stopPrice)
				{
					BuyMarket();
					_shortEntryPrice = null;
					return;
				}
			}

			if (TakeProfitPoints > 0 && _shortEntryPrice.HasValue)
			{
				var targetPrice = _shortEntryPrice.Value - TakeProfitPoints * step;
				if (candle.LowPrice <= targetPrice || candle.ClosePrice <= targetPrice)
				{
					BuyMarket();
					_shortEntryPrice = null;
					return;
				}
			}
		}
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPriceTypes type)
	{
		return type switch
		{
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods type, int length)
	{
		return type switch
		{
			MovingAverageMethods.Simple => new SMA { Length = length },
			MovingAverageMethods.Exponential => new EMA { Length = length },
			MovingAverageMethods.Smoothed => new EMA { Length = length },
			MovingAverageMethods.LinearWeighted => new SMA { Length = length },
			_ => new EMA { Length = length }
		};
	}

	private struct SignalInfo
	{
		public static readonly SignalInfo Empty = new();
		public bool BuyOpen;
		public bool BuyClose;
		public bool SellOpen;
		public bool SellClose;
	}

	public enum MovingAverageMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	public enum AppliedPriceTypes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
}