Ver no GitHub

Estratégia de Trailing Stop com Ponto de Equilíbrio por Ticks

Visão geral

  • Gerenciador de trailing stop baseado em ticks convertido do consultor especialista MetaTrader e_Breakeven_v4.
  • Monitora cada tick de negociação para mover um stop-loss virtual quando o preço se afasta o suficiente da entrada.
  • Fecha posições compradas ou vendidas a mercado quando o nível de trailing é atingido, replicando o comportamento de ponto de equilíbrio mais passo do EA original.
  • Inclui um modo demo opcional que abre posições aleatórias durante os testes para demonstrar a lógica de trailing sem uma fonte de sinal externa.

Como funciona

  1. A estratégia assina ticks de negociação (DataType.Ticks) para emular o callback OnTick usado no MQL5.
  2. Quando uma posição existe e o trailing stop (em pips) mais o passo de trailing foram excedidos, o nível de stop é deslocado mais perto do preço.
  3. Para posições compradas, o stop é colocado em preço atual - trailing stop se o movimento desde a entrada exceder trailing stop + trailing step.
  4. Para posições vendidas, o stop é colocado em preço atual + trailing stop quando o preço se move para baixo a mesma distância.
  5. Se o preço ao vivo tocar ou cruzar o nível de stop armazenado, a estratégia sai de toda a posição a mercado e reinicia o estado de trailing.
  6. Uma conversão interna de pips multiplica o passo de preço do broker por 10 quando o instrumento tem 3 ou 5 casas decimais, correspondendo ao ajuste ponto-a-pip do MQL5.
  7. Quando o modo demo está habilitado, a estratégia abre uma negociação comprada ou vendida aleatoriamente (usando o Volume configurado) na primeira vez que um novo tick chega após a entrada anterior ser fechada.

Parâmetros

Nome Descrição Padrão Notas
TrailingStopPips Distância em pips entre o preço atual e o trailing stop. 10 Definir como 0 para desabilitar o trailing completamente.
TrailingStepPips Distância adicional em pips necessária antes que o stop avance novamente. 1 Deve ser maior que zero quando o trailing stop está ativo, reproduzindo a regra de validação do EA.
EnableDemoEntries Habilita entradas aleatórias para backtests sem um sinal externo. false Quando true, a estratégia lança uma moeda em cada tick enquanto está zerada para decidir a direção.

Regras de gerenciamento de posição

  • A estratégia não abre posições por si mesma a menos que EnableDemoEntries esteja em true.
  • O trailing é simétrico para posições compradas e vendidas e funciona com qualquer tamanho de volume.
  • Os níveis de stop são gerenciados internamente (virtuais) e aplicados com saídas a mercado, evitando ordens stop explícitas que podem não ser suportadas por todos os conectores.
  • Qualquer negociação manual ou estratégia externa pode fornecer as entradas; este componente só gerenciará o trailing stop.

Notas de uso

  • Funciona melhor com instrumentos que fornecem ticks de negociação para que o trailing reaja imediatamente.
  • Certifique-se de que Volume esteja configurado para o tamanho de lote que corresponde às posições recebidas se o modo demo for usado.
  • A conversão de pips assume preços no estilo FX onde símbolos com 3 ou 5 decimais precisam de um multiplicador ×10 para converter pontos em pips.
  • A saída é acionada no primeiro tick que cruza o preço de stop armazenado, correspondendo ao fluxo imediato de modificação e fechamento da lógica MQL.

Diferenças em relação ao especialista MQL5 original

  • Usa stops virtuais com saídas a mercado em vez de modificar ordens stop-loss do lado do broker porque as estratégias StockSharp tipicamente gerenciam saídas através da lógica da estratégia.
  • Substitui o bloco de entrada aleatória do testador MetaTrader pelo flag configurável EnableDemoEntries.
  • Converte a lógica ponto-a-pip usando Security.PriceStep e contagem de decimais em vez de Symbol().Digits().
  • Todos os comentários e registros agora estão em inglês de acordo com as diretrizes do repositório.
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>
/// Trailing stop manager that moves stops to breakeven and beyond once price advances.
/// Designed to trail any manually opened position using pip based distances.
/// </summary>
public class BreakevenTrailingStopTickStrategy : Strategy
{
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<bool> _enableDemoEntries;

	private readonly StrategyParam<DataType> _candleType;
	private decimal _pointValue;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private bool _exitOrderPending;
	private decimal _entryPrice;
	private DateTimeOffset? _lastDemoEntryTime;

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing step in pips before the stop is moved again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Enable random demo entries to showcase the trailing behaviour in testing.
	/// </summary>
	public bool EnableDemoEntries
	{
		get => _enableDemoEntries.Value;
		set => _enableDemoEntries.Value = value;
	}

	/// <summary>
/// Initializes a new instance of <see cref="BreakevenTrailingStopTickStrategy"/>.
/// </summary>
public BreakevenTrailingStopTickStrategy()
	{
		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Trailing")
			
			.SetOptimize(5m, 30m, 5m);

		_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step", "Additional pips required before stop moves again", "Trailing")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_enableDemoEntries = Param(nameof(EnableDemoEntries), true)
			.SetDisplay("Enable Demo Entries", "Automatically open random trades in testing", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for candles", "General");
	}

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

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

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

		_pointValue = 0m;
		_longStopPrice = null;
		_shortStopPrice = null;
		_exitOrderPending = false;
		_lastDemoEntryTime = null;
		_entryPrice = 0;
	}

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
			throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		_pointValue = CalculateAdjustedPoint();

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var price = candle.ClosePrice;

		if (EnableDemoEntries)
			TryCreateDemoEntry(candle, price);

		if (Position == 0)
		{
			ResetTrailingState();
			return;
		}

		if (TrailingStopPips <= 0m || _pointValue <= 0m)
			return;

		if (Position > 0)
			UpdateLongTrailing(price);
		else if (Position < 0)
			UpdateShortTrailing(price);
	}

	private void TryCreateDemoEntry(ICandleMessage candle, decimal price)
	{
		if (Position != 0 || _exitOrderPending)
			return;

		var serverTime = candle.CloseTime;
		if (_lastDemoEntryTime.HasValue && (serverTime - _lastDemoEntryTime.Value).TotalMinutes < 30)
			return;

		var volume = Volume;
		if (volume <= 0m)
			return;

		if (Random.Shared.NextDouble() < 0.5)
		{
			BuyMarket(volume);
			_entryPrice = price;
		}
		else
		{
			SellMarket(volume);
			_entryPrice = price;
		}

		_lastDemoEntryTime = serverTime;
	}

	private void UpdateLongTrailing(decimal currentPrice)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var stopOffset = TrailingStopPips * _pointValue;
		var stepOffset = TrailingStepPips * _pointValue;
		if (stopOffset <= 0m)
			return;

		var activationOffset = stopOffset + stepOffset;
		if (currentPrice - entryPrice <= activationOffset)
			return;

		var threshold = currentPrice - activationOffset;
		if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
		{
			var newStop = currentPrice - stopOffset;
			if (newStop > 0m)
			{
				_longStopPrice = newStop;
				// log($"Long trailing stop moved to {newStop}.");
			}
		}

		if (_longStopPrice.HasValue && currentPrice <= _longStopPrice.Value)
			ExitLongPosition();
	}

	private void UpdateShortTrailing(decimal currentPrice)
	{
		var entryPrice = _entryPrice;
		if (entryPrice <= 0m)
			return;

		var stopOffset = TrailingStopPips * _pointValue;
		var stepOffset = TrailingStepPips * _pointValue;
		if (stopOffset <= 0m)
			return;

		var activationOffset = stopOffset + stepOffset;
		if (entryPrice - currentPrice <= activationOffset)
			return;

		var threshold = currentPrice + activationOffset;
		if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold)
		{
			var newStop = currentPrice + stopOffset;
			_shortStopPrice = newStop;
			// log($"Short trailing stop moved to {newStop}.");
		}

		if (_shortStopPrice.HasValue && currentPrice >= _shortStopPrice.Value)
			ExitShortPosition();
	}

	private void ExitLongPosition()
	{
		if (_exitOrderPending)
			return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_exitOrderPending = true;
		// log("Long position closed by trailing stop.");
	}

	private void ExitShortPosition()
	{
		if (_exitOrderPending)
			return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_exitOrderPending = true;
		// log("Short position closed by trailing stop.");
	}


	private void ResetTrailingState()
	{
		_longStopPrice = null;
		_shortStopPrice = null;
		_exitOrderPending = false;
		_entryPrice = 0m;
	}

	private decimal CalculateAdjustedPoint()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		return decimals is 3 or 5 ? step * 10m : step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var decimals = 0;

		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}

		return decimals;
	}
}