Ver no GitHub

Estratégia Autotrade com Stops Pendentes

Visão geral

Esta estratégia é uma conversão em C# do consultor especializado MetaTrader Autotrade (edição de barabashkakvn). Ela mantém continuamente duas ordens de entrada stop simétricas ao redor do preço de mercado atual. Sempre que o mercado permanece flat e não há posição aberta, a estratégia atualiza ambas as ordens pendentes. Quando uma ordem stop é executada, a posição é monitorada ativamente: as saídas são acionadas quando a ação do preço se estabiliza ou quando um limiar absoluto de lucro/perda é atingido. A implementação usa a API de alto nível do StockSharp conforme exigido pelas diretrizes do projeto.

Mapeamento dos parâmetros originais

Parâmetro StockSharp Parâmetro MQL5 Descrição
IndentTicks InpIndent Distância (em passos de preço) entre o preço atual e as ordens de entrada stop.
MinProfit MinProfit Lucro flutuante mínimo (moeda da conta) necessário para sair durante uma fase tranquila do mercado.
ExpirationMinutes ExpirationMinutes Tempo de vida das ordens stop pendentes antes de serem canceladas e recriadas.
AbsoluteFixation AbsoluteFixation Nível absoluto de lucro ou perda (moeda) que força o fechamento da posição.
StabilizationTicks InpStabilization Tamanho máximo do corpo da vela anterior que é tratado como zona de consolidação.
OrderVolume Lots Volume usado tanto para o buy stop quanto para o sell stop.
CandleType Period() Série de velas que impulsiona a lógica (período de 1 minuto por padrão).

Todos os parâmetros numéricos que representam distâncias de preço são convertidos de "pontos" para passos de preço reais através do valor Security.PriceStep. Os limiares baseados em lucro são calculados usando Security.StepPrice, o que reflete os cálculos de lucro do MQL que operam na moeda do depósito.

Lógica de trading

Implantação de ordens pendentes

  1. A estratégia reage apenas a velas terminadas (CandleStates.Finished).
  2. A primeira vela é usada para semear dados históricos (open/close anterior) e imediatamente agendar ordens pendentes.
  3. Quando não há posição aberta, referências inativas são limpas e:
    • Um buy stop é colocado em Close + IndentTicks * PriceStep.
    • Um sell stop é colocado em Close - IndentTicks * PriceStep.
  4. Cada ordem pendente recebe um timestamp de expiração igual a CloseTime + ExpirationMinutes minutos. Quando esse tempo é atingido, a ordem é cancelada e recriada na próxima vela.

Gerenciamento de posição

  1. Uma vez executada qualquer ordem stop, a ordem pendente contrária é cancelada para evitar hedge indesejado no modelo de conta baseado em netting do StockSharp.
  2. A estratégia mantém o corpo da vela anterior (|Open - Close|) para detectar condições tranquilas do mercado.
  3. Para cada vela com posição aberta:
    • O lucro não realizado é estimado em moeda usando a diferença de preço em relação a PositionAvgPrice, escalada por Security.PriceStep e Security.StepPrice.
    • Se o lucro exceder MinProfit e o corpo da vela anterior estiver abaixo de StabilizationTicks * PriceStep, a posição é fechada a mercado.
    • Independentemente da estabilização, se o lucro ou perda absoluta exceder AbsoluteFixation, a posição também é fechada a mercado.
  4. Quando a posição retorna a flat, todas as ordens pendentes restantes são eliminadas.

Comportamentos adicionais

  • Apenas uma posição é permitida por vez; os volumes de ordens são compensados usando OrderVolume.
  • Como o StockSharp não expõe bid/ask durante backtests da mesma forma que o MetaTrader, o preço de fechamento da vela completada é usado como nível de referência para novas ordens stop.
  • A estratégia atualiza automaticamente o valor Volume em cache quando OrderVolume é ajustado via parâmetros ou otimização.

Notas de implementação e diferenças

  • Os cálculos de lucro dependem de Security.PriceStep e Security.StepPrice. Garanta que esses campos estejam preenchidos nos metadados do instrumento; caso contrário, o valor 1 é usado como fallback.
  • A versão MQL original permitia hedge temporário (múltiplas ordens em direções opostas). O port StockSharp cancela o stop não utilizado imediatamente após uma execução para cumprir com o modelo de netting da plataforma.
  • A expiração de ordens pendentes usa o CloseTime da vela. Se os dados históricos não tiverem timestamps de fechamento, ajuste o feed para fornecê-los ou estenda o código adequadamente.
  • A estratégia funciona com qualquer tipo de dado de velas ajustando CandleType. As velas padrão são baseadas em período (TimeSpan.FromMinutes(1).TimeFrame()).

Recomendações de uso

  1. Configure a série de velas que corresponda ao período do gráfico usado no MetaTrader.
  2. Defina IndentTicks, StabilizationTicks e os limiares de lucro em relação ao tamanho do tick e ao valor do tick do instrumento.
  3. Verifique se o portfólio usa hedge ou netting conforme desejado. A estratégia assume netting e fechará o livro antes de reativar as ordens stop.
  4. Use os parâmetros fornecidos para otimização no StockSharp Designer ou Backtester para adaptar o comportamento a diferentes mercados.
  5. Monitore a saída do log: o código depende de velas terminadas e disponibilidade do mercado (IsFormedAndOnlineAndAllowTrading()) antes de enviar novas ordens.

Aviso de risco

O trading automatizado envolve riscos substanciais. Faça backtests completos, valide os parâmetros em dados históricos e confirme os requisitos específicos do corretor (como distâncias mínimas para ordens stop) antes de implantar a estratégia em uma conta real.

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>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
	private readonly StrategyParam<int> _indentTicks;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<int> _stabilizationTicks;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevOpen;
	private decimal _prevClose;
	private bool _hasPrevCandle;

	private decimal _tickSize = 1m;
	private decimal _tickValue = 1m;

	/// <summary>
	/// Distance in price steps from the current market to the pending stop entries.
	/// </summary>
	public int IndentTicks
	{
		get => _indentTicks.Value;
		set => _indentTicks.Value = value;
	}

	/// <summary>
	/// Minimal profit in account currency required to exit when price action stabilizes.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Lifetime of pending stop orders in minutes.
	/// </summary>
	public int ExpirationMinutes
	{
		get => _expirationMinutes.Value;
		set => _expirationMinutes.Value = value;
	}

	/// <summary>
	/// Absolute profit or loss that forces the position to close.
	/// </summary>
	public decimal AbsoluteFixation
	{
		get => _absoluteFixation.Value;
		set => _absoluteFixation.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous candle body that is treated as consolidation.
	/// </summary>
	public int StabilizationTicks
	{
		get => _stabilizationTicks.Value;
		set => _stabilizationTicks.Value = value;
	}

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

	/// <summary>
	/// Candle type used to drive the strategy logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AutotradePendingStopsStrategy()
	{
		_indentTicks = Param(nameof(IndentTicks), 200)
		.SetGreaterThanZero()
		.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");

		_minProfit = Param(nameof(MinProfit), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");

		_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
		.SetGreaterThanZero()
		.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");

		_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
		.SetGreaterThanZero()
		.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");

		_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
		.SetGreaterThanZero()
		.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default volume for both stop orders", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");

		Volume = _orderVolume.Value;
	}

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

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

		// Reset runtime state when the strategy is reloaded.
		_prevOpen = 0m;
		_prevClose = 0m;
		_hasPrevCandle = false;
		_entryPrice = 0m;
	}

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

		Volume = _orderVolume.Value;

		// Cache price step and tick value for fast profit calculations.
		_tickSize = Security.PriceStep ?? 1m;
		_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles to stay aligned with the original MQL logic.
		if (candle.State != CandleStates.Finished)
		return;

		if (!_hasPrevCandle)
		{
			// Store the first candle so that stabilization checks have history.
			_prevOpen = candle.OpenPrice;
			_prevClose = candle.ClosePrice;
			_hasPrevCandle = true;

			EnsurePendingOrders(candle);
			return;
		}

		UpdatePendingOrdersLifetime(candle);

		if (Position == 0)
		{
			// Refresh pending orders as soon as the market is flat.
			EnsurePendingOrders(candle);
		}
		else
		{
			// Manage the active position and close it when required.
			ManageOpenPosition(candle);
		}

		// Keep the previous candle body for stabilization checks on the next bar.
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
	}

	private decimal _entryPrice;

	private void EnsurePendingOrders(ICandleMessage candle)
	{
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		var indent = IndentTicks * _tickSize;
		var buyPrice = candle.ClosePrice + indent;
		var sellPrice = candle.ClosePrice - indent;

		// Simulate stop-order breakout: if high breaches buy level, go long
		if (candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(OrderVolume);
			_entryPrice = buyPrice;
		}
		// if low breaches sell level, go short
		else if (candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			SellMarket(OrderVolume);
			_entryPrice = sellPrice;
		}
	}

	private void UpdatePendingOrdersLifetime(ICandleMessage candle)
	{
		// No pending orders in simplified version - nothing to expire.
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice == 0)
			return;

		var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
		var prevBodySize = Math.Abs(_prevClose - _prevOpen);

		// Exit if profitable and market consolidating, or if loss exceeds threshold
		var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
		var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;

		if (Position > 0 && (exitByProfit || exitByLoss))
		{
			SellMarket();
		}
		else if (Position < 0 && (exitByProfit || exitByLoss))
		{
			BuyMarket();
		}
	}

}