Ver no GitHub

Estratégia OpenTiks

Visão geral

A Estratégia OpenTiks transporta o clássico MetaTrader consultor especialista OpenTiks.mq4 para o ecossistema StockSharp. O robô original procurei uma escada de velas com máximos estritamente monótonos e aberturas para detectar rompimentos precoces. Uma vez que um sinal surgiu, ele abriu uma ordem de mercado, opcionalmente anexou um stop de proteção e, em seguida, seguiu a posição enquanto realizava lucros progressivamente reduzindo repetidamente a exposição pela metade. A versão StockSharp reflete essas ideias usando chamadas API de alto nível, assinaturas de velas, e os auxiliares de ordem integrados para que a lógica seja executada dentro do Designer, do Runner ou de qualquer aplicativo S# personalizado.

Detecção de padrões

Uma negociação pode ser lançada quando quatro velas consecutivas satisfazem um dos seguintes padrões:

  • Rompimento de alta – para a vela atual e as três barras anteriores: cada High é estritamente superior à anterior High, e cada Open é estritamente superior ao Open anterior.
  • Rompimento de baixa – para a mesma janela de quatro barras: cada High é estritamente inferior ao High anterior, e cada Open é estritamente inferior ao Open anterior.

Os sinais são avaliados em velas concluídas entregues pelo CandleType configurado. Quando a condição de rompimento for atendida, o estratégia envia uma ordem de mercado com o volume configurado (normalizado para o título VolumeStep e limitado por MinVolume e MaxVolume). O parâmetro MaxOrders limita quantas entradas simultâneas podem existir; um valor zero desativa a verificação, enquanto qualquer número positivo bloqueia novas negociações quando a posição líquida absoluta dividida pelo volume de pedidos normalizado atinge esse valor limite.

Gestão de riscos e saídas

  • Stop loss – se StopLossPoints for maior que zero, a estratégia monitora a última vela em busca de reversões de preços. Longo as posições são liquidadas quando a mínima da vela penetra entryPrice - StopLossPoints × PriceStep. As posições curtas saem quando os toques altos entryPrice + StopLossPoints × PriceStep.
  • Trailing stop – quando o preço avança pelo menos TrailingStopPoints × PriceStep além da entrada, um trailing stop é armado na mesma distância atrás (para posições compradas) ou à frente (para posições vendidas) do fechamento. Cada vez que o nível final melhora, o a posição restante é opcionalmente reduzida.
  • Realização progressiva de lucros – quando UsePartialClose está ativado, a estratégia sempre fecha metade da exposição atual o trailing stop avança. Os volumes são arredondados para o VolumeStep do instrumento. Se o tamanho reduzido pela metade ficar abaixo MinVolume, toda a posição é fechada, correspondendo ao comportamento do especialista MetaTrader.

Todos os cálculos de stop e trailing são realizados em velas finalizadas, então as saídas ocorrem no próximo fechamento da barra, em vez de em cada carrapato de entrada. Isso mantém a implementação consistente com o API de alto nível de API, mantendo-se próximo do original ideia de reagir a novas barras.

Parâmetros

Nome Tipo Padrão Descrição
OrderVolume decimal 0.1 Tamanho base do lote para cada entrada no mercado. A estratégia normaliza de acordo com a etapa e os limites do volume do título.
StopLossPoints decimal 0 Distância de parada protetora expressa em faixas de preço (etapas de preço). Um valor zero desativa a parada.
TrailingStopPoints decimal 30 Distância mantida pelo trailing stop quando a posição passa para o lucro, também em faixas de preço.
MaxOrders int 1 Número máximo de entradas abertas simultaneamente. Zero remove a restrição.
UsePartialClose bool true Ativa a lógica de redução pela metade que bloqueia os ganhos sempre que o trailing stop avança.
CandleType DataType 1 minute período de tempo Assinatura de vela primária usada para avaliação de sinal e verificações de rastreamento.

Notas de implementação

  • StockSharp trabalha com posições líquidas, então todas as ordens para o título configurado acumulam em uma única compra ou venda exposição. O parâmetro MaxOrders, portanto, atua na posição agregada e não em tickets MetaTrader individuais.
  • O rastreamento baseado em velas significa que as verificações de parada acontecem uma vez por barra concluída. Os comerciantes que precisam de proteção no nível do tick podem reduzir o tamanho da vela ou estender a lógica para assinar negociações.
  • Os fechamentos parciais respeitam os metadados do instrumento (VolumeStep, MinVolume, MaxVolume) para evitar pedidos rejeitados.
  • Os comentários em inglês embutidos destacam os principais pontos de decisão para que o arquivo funcione como material educacional ao adaptar a ideia a outras experiências inovadoras ou de gestão de dinheiro.

Dicas de uso

  1. Selecione um tipo de vela que corresponda ao período de tempo usado na configuração original do MetaTrader (por exemplo, M1 ou M5).
  2. Verifique as configurações de etapa e lote do instrumento; o padrão OrderVolume de 0.1 é adequado para contratos estilo Forex, mas pode ser ajustado para futuros, ações ou símbolos criptográficos.
  3. Experimente TrailingStopPoints e UsePartialClose para encontrar um equilíbrio entre bloqueio agressivo de lucro e permissão os vencedores correm.
  4. Combine a estratégia com gráficos StockSharp para confirmar visualmente o padrão de escada e observar as saídas parciais em tempo real tempo.
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Reimplementation of the MetaTrader expert advisor "OpenTiks" for StockSharp.
/// Detects four consecutive candles with strictly monotonic opens and highs to trigger entries,
/// then manages the position with optional stop-loss, trailing stop and progressive partial exits.
/// </summary>
public class OpenTiksStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<bool> _usePartialClose;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _priceStep;
	private decimal _volumeStep;
	private decimal _minVolumeLimit;
	private decimal _maxVolumeLimit;

	private decimal? _high1;
	private decimal? _high2;
	private decimal? _high3;

	private decimal? _open1;
	private decimal? _open2;
	private decimal? _open3;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	private SimpleMovingAverage _dummySma;
	private decimal _previousPosition;
	private decimal? _lastTradePrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="OpenTiksStrategy"/> class.
	/// </summary>
	public OpenTiksStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume of each market entry in lots.", "Trading");

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

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Trailing distance expressed in price points.", "Risk");

		_maxOrders = Param(nameof(MaxOrders), 1)
			.SetNotNegative()
			.SetDisplay("Max Orders", "Maximum number of simultaneously open entries. Zero disables the limit.", "Trading");

		_usePartialClose = Param(nameof(UsePartialClose), true)
			.SetDisplay("Use Partial Close", "Close half of the position whenever the trailing stop advances.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for pattern detection.", "General");
	}

	/// <summary>
	/// Order volume used for every market entry.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in price points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open entries.
	/// </summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

	/// <summary>
	/// Enables progressive partial exits when true.
	/// </summary>
	public bool UsePartialClose
	{
		get => _usePartialClose.Value;
		set => _usePartialClose.Value = value;
	}

	/// <summary>
	/// Candle type requested from the market data feed.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_priceStep = 0;
		_volumeStep = 0;
		_minVolumeLimit = 0;
		_maxVolumeLimit = 0;
		_high1 = null;
		_high2 = null;
		_high3 = null;
		_open1 = null;
		_open2 = null;
		_open3 = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_dummySma = null;
		_previousPosition = 0m;
		_lastTradePrice = null;
	}

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

		var security = Security;
		_priceStep = security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_volumeStep = security?.VolumeStep ?? 0m;
		_minVolumeLimit = security?.MinVolume ?? 0m;
		_maxVolumeLimit = security?.MaxVolume ?? 0m;

		Volume = NormalizeEntryVolume(OrderVolume);

		_dummySma = new SimpleMovingAverage { Length = 2 };

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

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		_lastTradePrice = trade.Trade?.Price ?? trade.Order.Price;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		var delta = Position - _previousPosition;

		if (Position > 0m)
		{
			if (_previousPosition <= 0m)
			{
				_longEntryPrice = _lastTradePrice;
				_longTrailingStop = null;
				_shortEntryPrice = null;
				_shortTrailingStop = null;
			}
			else if (delta > 0m && _lastTradePrice is decimal priceLong)
			{
				var previousVolume = Math.Max(0m, _previousPosition);
				var currentVolume = Math.Max(0m, Position);
				if (currentVolume > 0m)
				{
					var currentEntry = _longEntryPrice ?? priceLong;
					_longEntryPrice = (currentEntry * previousVolume + priceLong * delta) / currentVolume;
				}
			}
		}
		else if (Position < 0m)
		{
			if (_previousPosition >= 0m)
			{
				_shortEntryPrice = _lastTradePrice;
				_shortTrailingStop = null;
				_longEntryPrice = null;
				_longTrailingStop = null;
			}
			else if (delta < 0m && _lastTradePrice is decimal priceShort)
			{
				var previousVolume = Math.Max(0m, Math.Abs(_previousPosition));
				var currentVolume = Math.Max(0m, Math.Abs(Position));
				if (currentVolume > 0m)
				{
					var currentEntry = _shortEntryPrice ?? priceShort;
					_shortEntryPrice = (currentEntry * previousVolume + priceShort * Math.Abs(delta)) / currentVolume;
				}
			}
		}
		else
		{
			_longEntryPrice = null;
			_shortEntryPrice = null;
			_longTrailingStop = null;
			_shortTrailingStop = null;
		}

		_previousPosition = Position;
	}

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

		UpdateTrailing(candle);

		var buySignal = false;
		var sellSignal = false;

		if (_high1 is decimal h1 && _high2 is decimal h2 && _high3 is decimal h3 &&
		_open1 is decimal o1 && _open2 is decimal o2 && _open3 is decimal o3)
		{
		var high = candle.HighPrice;
		var open = candle.OpenPrice;

		buySignal = high > h1 && h1 > h2 && h2 > h3 &&
		open > o1 && o1 > o2 && o2 > o3;

		sellSignal = high < h1 && h1 < h2 && h2 < h3 &&
		open < o1 && o1 < o2 && o2 < o3;
		}

		_high3 = _high2;
		_high2 = _high1;
		_high1 = candle.HighPrice;

		_open3 = _open2;
		_open2 = _open1;
		_open1 = candle.OpenPrice;

		if (buySignal)
			TryEnterLong();

		if (sellSignal)
			TryEnterShort();
	}

	private void TryEnterLong()
	{
		if (MaxOrders > 0 && EstimateOrdersCount(Position) >= MaxOrders)
			return;

		var volume = NormalizeEntryVolume(OrderVolume);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
	}

	private void TryEnterShort()
	{
		if (MaxOrders > 0 && EstimateOrdersCount(Position) >= MaxOrders)
			return;

		var volume = NormalizeEntryVolume(OrderVolume);
		if (volume <= 0m)
			return;

		SellMarket(volume);
	}

	private int EstimateOrdersCount(decimal positionVolume)
	{
		var baseVolume = NormalizeEntryVolume(OrderVolume);
		if (baseVolume <= 0m)
			return positionVolume != 0m ? 1 : 0;

		var ratio = Math.Abs(positionVolume) / baseVolume;
		if (ratio <= 0m)
			return 0;

		return (int)Math.Ceiling(ratio);
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		var close = candle.ClosePrice;
		var low = candle.LowPrice;
		var high = candle.HighPrice;

		var stopDistance = StopLossPoints * _priceStep;
		var trailingDistance = TrailingStopPoints * _priceStep;

		if (Position > 0m && _longEntryPrice is decimal entryLong)
		{
			if (stopDistance > 0m && low <= entryLong - stopDistance)
			{
			SellMarket(Position);
			return;
			}

			if (trailingDistance > 0m && close - entryLong >= trailingDistance)
			{
			var desiredStop = close - trailingDistance;
			if (_longTrailingStop is not decimal currentStop || desiredStop > currentStop)
			{
			_longTrailingStop = desiredStop;
			TryReduceLongPosition();
			}

			if (_longTrailingStop is decimal trailingStop && low <= trailingStop)
			SellMarket(Position);
			}
		}
		else if (Position < 0m && _shortEntryPrice is decimal entryShort)
		{
			var positionVolume = Math.Abs(Position);

			if (stopDistance > 0m && high >= entryShort + stopDistance)
			{
			BuyMarket(positionVolume);
			return;
			}

			if (trailingDistance > 0m && entryShort - close >= trailingDistance)
			{
			var desiredStop = close + trailingDistance;
			if (_shortTrailingStop is not decimal currentStop || desiredStop < currentStop)
			{
			_shortTrailingStop = desiredStop;
			TryReduceShortPosition();
			}

			if (_shortTrailingStop is decimal trailingStop && high >= trailingStop)
			BuyMarket(positionVolume);
			}
		}
	}

	private void TryReduceLongPosition()
	{
		if (!UsePartialClose)
			return;

		if (Position <= 0m)
			return;

		var positionVolume = Position;
		var half = positionVolume / 2m;
		var normalizedHalf = NormalizeExitVolume(half, positionVolume);

		if (_minVolumeLimit > 0m && normalizedHalf < _minVolumeLimit)
		{
			SellMarket(positionVolume);
			return;
		}

		if (normalizedHalf > 0m)
		SellMarket(normalizedHalf);
	}

	private void TryReduceShortPosition()
	{
		if (!UsePartialClose)
			return;

		if (Position >= 0m)
			return;

		var positionVolume = Math.Abs(Position);
		var half = positionVolume / 2m;
		var normalizedHalf = NormalizeExitVolume(half, positionVolume);

		if (_minVolumeLimit > 0m && normalizedHalf < _minVolumeLimit)
		{
			BuyMarket(positionVolume);
			return;
		}

		if (normalizedHalf > 0m)
		BuyMarket(normalizedHalf);
	}

	private decimal NormalizeEntryVolume(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		if (_volumeStep > 0m)
		{
			var steps = Math.Round(volume / _volumeStep, MidpointRounding.AwayFromZero);
			if (steps <= 0m)
			steps = 1m;
			volume = steps * _volumeStep;
		}

		if (_minVolumeLimit > 0m && volume < _minVolumeLimit)
		volume = _minVolumeLimit;

		if (_maxVolumeLimit > 0m && volume > _maxVolumeLimit)
		volume = _maxVolumeLimit;

		return volume;
	}

	private decimal NormalizeExitVolume(decimal desired, decimal currentPosition)
	{
		if (desired <= 0m || currentPosition <= 0m)
		return 0m;

		var volume = desired;

		if (_volumeStep > 0m)
		{
			var steps = Math.Round(volume / _volumeStep, MidpointRounding.AwayFromZero);
			if (steps <= 0m)
			steps = 1m;
			volume = steps * _volumeStep;
		}

		if (volume > currentPosition)
		volume = currentPosition;

		return volume;
	}
}