Открыть на GitHub

Стратегия Omni Trend

Описание

Omni Trend — это перенос советника MetaTrader "Exp_Omni_Trend" на платформу StockSharp. Алгоритм сочетает скользящую среднюю и волатильностный канал на основе ATR. Как только цена пробивает границу предыдущего канала, стратегия меняет направление торговли, закрывает противоположные позиции и открывает сделку по новому тренду. Реализация сохраняет все ключевые настройки оригинала, включая задержку исполнения (SignalBar) и возможность отключать отдельные входы или выходы.

Стратегия подписывается на выбранные свечи и обрабатывает только завершённые бары. Скользящая средняя (MaType, MaLength, AppliedPrice) определяет базовый уровень, а ATR (AtrLength) задаёт ширину адаптивных полос через множители VolatilityFactor и MoneyRisk. Полосы выступают в роли трейлинг-стопов: пересечение верхней полосы переводит систему в бычий режим, нижней — в медвежий. В новом режиме немедленно закрываются противоположные позиции (если разрешено) и, после выдержки SignalBar, открывается сделка по направлению тренда.

Дополнительные уровни StopLossPoints и TakeProfitPoints измеряются в шагах цены инструмента и действуют как жёсткие ограничения убытков/прибыли. Размер позиции задаётся свойством Volume (по умолчанию 1).

Логика торговли

  1. Рассчитать выбранную скользящую среднюю от указанного ценового поля.
  2. Рассчитать ATR и построить верхнюю/нижнюю защитные полосы с использованием VolatilityFactor и MoneyRisk.
  3. Если HighPrice пробивает предыдущую верхнюю полосу:
    • направление меняется на «вверх»;
    • при активном флаге EnableSellClose закрываются все короткие позиции;
    • при активном EnableBuyOpen выставляется отложенный сигнал на покупку через SignalBar баров.
  4. Если LowPrice пробивает предыдущую нижнюю полосу:
    • направление меняется на «вниз»;
    • при активном EnableBuyClose закрываются длинные позиции;
    • при активном EnableSellOpen формируется сигнал на продажу через заданную задержку.
  5. Пока тренд сохраняется, противоположные выходы продолжают генерироваться, что обеспечивает постоянное сопровождение позиции, как и в оригинальном советнике.
  6. Проверка риск-менеджмента выполняется на каждом закрытом баре: если цена достигает стопа или цели (в шагах цены), позиция закрывается немедленно и сбрасывается цена входа.

Сигналы попадают в очередь FIFO. При SignalBar = 0 они исполняются на закрытии текущего бара. При значениях больше нуля исполнение переносится на открытие бара, который завершает задержку, имитируя работу MQL5-версии.

Параметры

Параметр Описание Значение по умолчанию
CandleType Тип/таймфрейм свечей. 4 часа
MaLength Период скользящей средней. 13
MaType Метод расчёта скользящей (Simple, Exponential, Smoothed, LinearWeighted). Exponential
AppliedPrice Используемая цена: Close, Open, High, Low, Median, Typical, Weighted. Close
AtrLength Период ATR. 11
VolatilityFactor Множитель ATR при построении канала. 1.3
MoneyRisk Коэффициент смещения канала от скользящей средней. 0.15
SignalBar Количество закрытых баров до исполнения сигнала. 1
EnableBuyOpen Разрешить открытие длинных позиций. true
EnableSellOpen Разрешить открытие коротких позиций. true
EnableBuyClose Разрешить закрытие длинных позиций при медвежьем тренде. true
EnableSellClose Разрешить закрытие коротких позиций при бычьем тренде. true
StopLossPoints Стоп-лосс в шагах цены (0 — отключён). 1000
TakeProfitPoints Тейк-профит в шагах цены (0 — отключён). 2000
Volume Размер позиции (свойство стратегии). 1

Рекомендации

  • Значение SignalBar = 1 повторяет поведение советника: вход выполняется на открытии следующей свечи после появления сигнала. Ноль означает исполнение на закрытии текущего бара.
  • Убедитесь, что инструмент имеет корректный PriceStep, если используете уровни стопа/цели.
  • Встроенный график отображает свечи, выбранную скользящую среднюю и сделки стратегии для визуальной проверки.
  • Отключайте отдельные флаги Enable*, чтобы ограничить работу только покупками или только продажами либо оставлять закрытие позиций вручную.
  • Стратегия отправляет рыночные заявки (BuyMarket, SellMarket), аналогично тому, как советник выполняет немедленные ордера.

Состав

  • CS/OmniTrendStrategy.cs — реализация стратегии.
  • README.md, README_ru.md, README_zh.md — документация на трёх языках.

Python-версия сознательно не создавалась согласно заданию.

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