Ver no GitHub

Estratégia Omni Tendência

Visão geral

A estratégia Omni Tendência é um port direto do especialista MetaTrader "Exp_Omni_Trend". Ela combina uma média móvel com um canal baseado em ATR para detectar a tendência dominante e alternar entre exposição comprada e vendida. A versão StockSharp mantém o comportamento original, incluindo o atraso entre detecção de sinal e execução de ordem, bem como a capacidade de desabilitar segmentos individuais de entrada ou saída.

A estratégia se inscreve na série de velas configurada e alimenta cada barra finalizada para a lógica Omni Tendência. A média móvel serve como estimativa da tendência central, enquanto os multiplicadores ATR constroem envelopes de volatilidade. Os envelopes se comportam como stops trailing: preço que fecha além do limite de envelope anterior inverte a tendência, gera um novo sinal de entrada na nova direção e fecha imediatamente qualquer exposição contrária.

Se os limites opcionais de stop-loss e take-profit estiverem habilitados, eles atuam no lado do broker em passos de preço, complementando as saídas baseadas em indicadores. O tamanho da posição é controlado através da propriedade integrada Volume da estratégia (padrão 1).

Lógica de Trading

  1. Calcular a média móvel escolhida (MaType, MaLength, AppliedPrice) no fluxo de velas.
  2. Calcular ATR (AtrLength) e derivar duas bandas adaptativas usando VolatilityFactor e MoneyRisk. A banda superior protege posições vendidas, a banda inferior protege posições compradas.
  3. Quando o preço excede a banda protetora da barra anterior, a tendência muda:
    • Um rompimento de alta (HighPrice acima da banda superior anterior) inverte a tendência para "acima", fecha qualquer posição vendida se permitido, e abre uma posição comprada após SignalBar velas completadas.
    • Um rompimento de baixa (LowPrice abaixo da banda inferior anterior) inverte a tendência para "abaixo", fecha qualquer posição comprada se permitido, e abre uma posição vendida após o atraso configurado.
  4. Enquanto a tendência permanece de alta, a estratégia continua a solicitar saídas vendidas; a regra simétrica se aplica para uma tendência de baixa e saídas compradas. Isso espelha o comportamento do especialista MetaTrader, onde a banda oposta força constantemente exposição plana contra a direção prevalecente.
  5. O gerenciamento de risco opcional monitora cada vela finalizada. Se a barra atual atingir o preço de stop ou alvo (expresso em passos de preço), a posição é fechada imediatamente, redefinindo o preço de entrada armazenado.

Os sinais são agendados através de uma fila FIFO. Quando SignalBar é zero, são executados no fechamento da mesma vela. Caso contrário, são acionados na abertura da vela que completa o atraso, o que replica o estilo de execução de "barra anterior" do especialista fonte.

Parâmetros

Nome Descrição Padrão
CandleType Tipo de vela (período) para cálculos. Período de 4 horas
MaLength Período da média móvel. 13
MaType Método da média móvel: simples, exponencial, suavizada ou ponderada linearmente. Exponencial
AppliedPrice Campo de preço para a média móvel (fechamento, abertura, máximo, mínimo, mediano, típico, ponderado). Fechamento
AtrLength Período ATR para o canal de volatilidade. 11
VolatilityFactor Multiplicador aplicado ao ATR ao construir o canal bruto. 1.3
MoneyRisk Fator de deslocamento que afasta o canal da média móvel, idêntico ao input MQL. 0.15
SignalBar Número de velas completadas a aguardar antes de agir em um sinal. 1
EnableBuyOpen Permitir abrir posições compradas. true
EnableSellOpen Permitir abrir posições vendidas. true
EnableBuyClose Permitir fechar posições compradas quando tendência de baixa é detectada. true
EnableSellClose Permitir fechar posições vendidas quando tendência de alta é detectada. true
StopLossPoints Distância de stop protetor opcional em passos de preço. 0 para desabilitar. 1000
TakeProfitPoints Distância do alvo de lucro opcional em passos de preço. 0 para desabilitar. 2000
Volume Propriedade da estratégia que controla o tamanho da operação. 1

Notas e Recomendações

  • A implementação StockSharp alimenta os mesmos valores de indicadores que o original e reproduz suas inversões de tendência. No entanto, execuções precisas dependem da fonte de dados e da latência de execução.
  • Defina SignalBar = 1 para imitar o padrão do consultor especialista, onde as ordens são executadas na abertura da próxima vela após um sinal estar disponível. Valores maiores atrasam ainda mais a execução; definir 0 executa no fechamento atual.
  • Os limites de stop-loss e take-profit são expressos em pontos (passos de preço). Certifique-se de que o ativo conectado expõe um PriceStep válido.
  • O gráfico integrado desenha a série de velas, a média móvel selecionada e as próprias operações da estratégia para validação visual rápida.
  • Desabilite alternâncias específicas de entrada ou saída para restringir a estratégia à operação unilateral ou para tratar saídas manualmente.
  • A estratégia não cria ordens pendentes; emite ordens de mercado usando BuyMarket e SellMarket exatamente como o placement de ordens direto do especialista fonte.

Arquivos

  • CS/OmniTrendStrategy.cs — Implementação em C# da estratégia.
  • README.md, README_ru.md, README_zh.md — Documentação em inglês, russo e chinês.

O suporte a Python foi intencionalmente omitido conforme solicitado.

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