Auf GitHub ansehen

Omni Trend Strategie

Überblick

Die Omni Trend Strategie ist ein direkter Port des MetaTrader-Experten "Exp_Omni_Trend". Sie kombiniert einen gleitenden Durchschnitt mit einem ATR-basierten Kanal, um den dominanten Trend zu erkennen und zwischen Long- und Short-Exposition zu wechseln. Die StockSharp-Version behält das ursprüngliche Verhalten bei, einschließlich der Verzögerung zwischen Signalerkennung und Orderausführung sowie der Möglichkeit, individuelle Einstiegs- oder Ausstiegs-Beine zu deaktivieren.

Die Strategie abonniert die konfigurierte Kerzenserie und speist jeden abgeschlossenen Balken in die Omni-Trend-Logik ein. Der gleitende Durchschnitt dient als Schätzung der zentralen Tendenz, während ATR-Multiplikatoren Volatilitätshüllen aufbauen. Die Hüllen verhalten sich wie Trailing-Stops: Ein Preis, der über die vorherige Hüllengrenze hinaus schließt, dreht den Trend um, generiert ein neues Einstiegssignal in die neue Richtung und schließt sofort jede entgegengesetzte Exposition.

Wenn die optionalen Stop-Loss- und Take-Profit-Schwellenwerte aktiviert sind, wirken sie auf der Broker-Seite in Preisschritten und ergänzen die indikatorbasierten Ausstiege. Die Positionsgröße wird über die integrierte Volume-Eigenschaft der Strategie gesteuert (Standard 1).

Handelslogik

  1. Den gewählten gleitenden Durchschnitt (MaType, MaLength, AppliedPrice) auf dem Kerzenstrom berechnen.
  2. ATR (AtrLength) berechnen und zwei adaptive Bänder mit VolatilityFactor und MoneyRisk ableiten. Das obere Band schützt Short-Positionen, das untere Band schützt Long-Positionen.
  3. Wenn der Preis das Schutzband des vorherigen Balkens überschreitet, ändert sich der Trend:
    • Ein bullischer Ausbruch (HighPrice über dem vorherigen oberen Band) dreht den Trend auf "hoch", schließt jede Short-Position wenn erlaubt, und öffnet nach SignalBar abgeschlossenen Kerzen eine Long-Position.
    • Ein bärischer Ausbruch (LowPrice unter dem vorherigen unteren Band) dreht den Trend auf "runter", schließt jede Long-Position wenn erlaubt, und öffnet nach der konfigurierten Verzögerung eine Short-Position.
  4. Solange der Trend bullisch bleibt, fordert die Strategie weiterhin Short-Ausstiege; die symmetrische Regel gilt für einen bärischen Trend und Long-Ausstiege. Dies spiegelt das Verhalten des MetaTrader-Experten wider, wo das entgegengesetzte Band konstant flache Exposition gegen die vorherrschende Richtung erzwingt.
  5. Das optionale Risikomanagement überwacht jede abgeschlossene Kerze. Wenn der aktuelle Balken den Stop- oder Zielpreis (ausgedrückt in Preisschritten) erreicht, wird die Position sofort geschlossen und der gespeicherte Einstiegspreis wird zurückgesetzt.

Signale werden über eine FIFO-Warteschlange geplant. Wenn SignalBar null ist, werden sie beim Schluss derselben Kerze ausgeführt. Andernfalls werden sie auf der Eröffnung der Kerze ausgelöst, die die Verzögerung abschließt, was den "vorherigen Balken"-Ausführungsstil des Quellexperten repliziert.

Parameter

Name Beschreibung Standard
CandleType Kerzentyp (Zeitrahmen) für Berechnungen. 4-Stunden-Zeitrahmen
MaLength Periode des gleitenden Durchschnitts. 13
MaType Methode des gleitenden Durchschnitts: einfach, exponentiell, geglättet oder linear gewichtet. Exponentiell
AppliedPrice Preisfeld für den gleitenden Durchschnitt (Schluss, Eröffnung, Hoch, Tief, Median, Typisch, Gewichtet). Schluss
AtrLength ATR-Periode für den Volatilitätskanal. 11
VolatilityFactor Multiplikator für ATR beim Aufbau des Rohkanals. 1.3
MoneyRisk Versatzfaktor, der den Kanal vom gleitenden Durchschnitt wegschiebt, identisch mit dem MQL-Input. 0.15
SignalBar Anzahl abgeschlossener Kerzen vor der Signalausführung. 1
EnableBuyOpen Long-Positionen öffnen erlauben. true
EnableSellOpen Short-Positionen öffnen erlauben. true
EnableBuyClose Long-Positionen bei bärischem Trend schließen erlauben. true
EnableSellClose Short-Positionen bei bullischem Trend schließen erlauben. true
StopLossPoints Optionaler Schutz-Stop-Abstand in Preisschritten. 0 deaktiviert. 1000
TakeProfitPoints Optionaler Gewinnziel-Abstand in Preisschritten. 0 deaktiviert. 2000
Volume Strategie-Eigenschaft zur Steuerung der Handelsgröße. 1

Hinweise und Empfehlungen

  • Die StockSharp-Implementierung speist dieselben Indikatorwerte wie das Original ein und reproduziert seine Trendwechsel. Präzise Ausführungen hängen jedoch von der Datenquelle und der Ausführungslatenz ab.
  • Setzen Sie SignalBar = 1, um den Standardwert des Expertenberaters zu imitieren, bei dem Orders auf der Eröffnung der nächsten Kerze nach Verfügbarkeit eines Signals ausgeführt werden. Größere Werte verzögern die Ausführung weiter; 0 führt beim aktuellen Schluss aus.
  • Stop-Loss- und Take-Profit-Schwellenwerte werden in Punkten (Preisschritte) ausgedrückt. Stellen Sie sicher, dass das verbundene Wertpapier einen gültigen PriceStep bereitstellt.
  • Der integrierte Chart zeichnet die Kerzenserie, den gewählten gleitenden Durchschnitt und die eigenen Trades der Strategie für schnelle visuelle Validierung.
  • Deaktivieren Sie spezifische Einstiegs- oder Ausstiegs-Schalter, um die Strategie auf einseitigen Betrieb zu beschränken oder Ausstiege manuell zu handhaben.
  • Die Strategie erstellt keine Pending-Orders; sie gibt Market-Orders über BuyMarket und SellMarket aus, genau wie die direkte Order-Platzierung des Quellexperten.

Dateien

  • CS/OmniTrendStrategy.cs — C#-Implementierung der Strategie.
  • README.md, README_ru.md, README_zh.md — Dokumentation in Englisch, Russisch und Chinesisch.

Python-Unterstützung wurde auf Anfrage bewusst weggelassen.

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
	}
}