Ver no GitHub

Estratégia de Estabilização de Pedidos

Visão geral

A Estratégia de Estabilização de Pedidos é uma conversão do MetaTrader consultor especialista hjueiisyx8lp2o379e_www_forex-instruments_info.mq4. O robô original coloca um par de ordens stop em torno do preço atual e aguarda um rompimento. Depois que uma posição é aberta, o sistema monitora os corpos recentes das velas para determinar se a ação do preço estagnou ("estabilizada") e sai da negociação quando o mercado perde impulso ou quando um limite de lucro predefinido é atingido.

Esta porta C# mantém a mesma lógica usando o StockSharp API de alto nível. Ele se baseia em velas concluídas em vez de ticks brutos, tornando o comportamento determinístico durante backtesting e negociações ao vivo.

Regras de negociação

  1. Quando não há posições abertas ou ordens ativas, a estratégia apresenta um stop de compra acima do mercado e um stop de venda abaixo do mercado. A distância é medida em MetaTrader pontos (geralmente igual a um pip).
  2. Se uma ordem de parada for executada:
    • A ordem preenchida abre uma posição de OrderVolume lotes.
    • A ordem de stop oposta permanece pendente para detectar um rompimento na outra direção.
  3. Enquanto uma posição está aberta, a estratégia verifica o tamanho do corpo das duas velas finalizadas mais recentes:
    • Se o último corpo da vela for menor que StabilizationPoints e o lucro flutuante for maior que ProfitThreshold, a posição será fechada e a ordem pendente oposta será cancelada.
    • Se duas velas consecutivas forem menores que StabilizationPoints, a negociação será fechada independentemente do lucro atual.
    • Se o lucro atingir AbsoluteFixation, a negociação será encerrada imediatamente.
  4. Os pedidos pendentes são cancelados e recriados após ExpirationMinutes, a menos que o valor seja definido como zero (vida útil infinita).

Parâmetros

Nome Descrição Padrão
OrderVolume Volume de negociação em lotes utilizados para ambas as entradas de stop. 0.1
OrderDistancePoints Distância entre o preço de fechamento atual e cada ordem de stop, expressa em MetaTrader pontos. 20
ProfitThreshold Lucro flutuante mínimo (moeda da conta) exigido antes que uma saída desencadeada pela estabilização seja permitida. -2
AbsoluteFixation Nível de lucro (moeda da conta) que força uma saída imediata. 30
StabilizationPoints Tamanho máximo do corpo da vela (pontos) que sinaliza um mercado estável. 25
ExpirationMinutes Vida útil das ordens stop pendentes em minutos. 0 desativa a expiração. 20
CandleType Tipo de vela usado para avaliar a estabilização (o padrão é um período de 5 minutos). TimeFrame(5m)

Notas de conversão

  • O consultor especialista original operava com base nos ticks do gráfico. Esta porta avalia apenas velas finalizadas, preservando a lógica e garantindo backtests reproduzíveis.
  • MetaTrader "pontos" são mapeados para StockSharp PriceStep. Se o instrumento não tiver um passo de preço, um passo de 1 será assumido.
  • O lucro é aproximado usando PriceStep e StepPrice para traduzir o movimento do preço na moeda da conta.
  • Todos os comentários do código foram reescritos em inglês e os metadados dos parâmetros incluem descrições fáceis de usar com agrupamento.

Uso

  1. Adicione a estratégia à sua solução StockSharp e atribua a segurança e o portfólio desejados.
  2. Configure os parâmetros, especialmente o intervalo de tempo da vela e a distância em pontos, para corresponder às características do instrumento.
  3. Comece a estratégia. Ele enviará ordens de stop emparelhadas e gerenciará posições de acordo com a lógica de estabilização descrita acima.

Mais ideias

  • Experimente diferentes intervalos de velas para equilibrar a capacidade de resposta e a filtragem de ruído.
  • Combine a estratégia com filtros de volatilidade (ATR, Bollinger Bandas) para evitar negociações durante sessões extremamente silenciosas.
  • Estenda a lógica com trailing stops ou saídas parciais de posição quando a meta de lucro absoluto for atingida.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Paired breakout stop orders with stabilization-based exits.
/// </summary>
public class OrderStabilizationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _orderDistancePoints;
	private readonly StrategyParam<decimal> _profitThreshold;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<decimal> _stabilizationPoints;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<DataType> _candleType;

	private Order _buyStop;
	private Order _sellStop;
	private DateTime? _createdAt;
	private decimal _filledPosition;
	private decimal _entryValue;
	private decimal _previousBody;
	private bool _hasPreviousBody;

	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal OrderDistancePoints { get => _orderDistancePoints.Value; set => _orderDistancePoints.Value = value; }
	public decimal ProfitThreshold { get => _profitThreshold.Value; set => _profitThreshold.Value = value; }
	public decimal AbsoluteFixation { get => _absoluteFixation.Value; set => _absoluteFixation.Value = value; }
	public decimal StabilizationPoints { get => _stabilizationPoints.Value; set => _stabilizationPoints.Value = value; }
	public int ExpirationMinutes { get => _expirationMinutes.Value; set => _expirationMinutes.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public OrderStabilizationStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero()
			.SetDisplay("Order Volume", "Trade volume in lots used for both stop entries", "Trading");
		_orderDistancePoints = Param(nameof(OrderDistancePoints), 20m).SetGreaterThanZero()
			.SetDisplay("Order Distance", "Distance between the current close price and each stop order, in points", "Trading");
		_profitThreshold = Param(nameof(ProfitThreshold), -2m)
			.SetDisplay("Profit Threshold", "Minimum floating profit (account currency) required before an exit triggered by stabilization is allowed", "Exit");
		_absoluteFixation = Param(nameof(AbsoluteFixation), 30m).SetNotNegative()
			.SetDisplay("Absolute Fixation", "Profit level (account currency) that forces an immediate exit", "Exit");
		_stabilizationPoints = Param(nameof(StabilizationPoints), 25m).SetGreaterThanZero()
			.SetDisplay("Stabilization", "Maximum candle body size (points) that signals a flat market", "Exit");
		_expirationMinutes = Param(nameof(ExpirationMinutes), 20).SetNotNegative()
			.SetDisplay("Expiration", "Lifetime of pending stop orders in minutes, 0 disables expiration", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to evaluate stabilization", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Ticks)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_buyStop = null;
		_sellStop = null;
		_createdAt = null;
		_filledPosition = 0m;
		_entryValue = 0m;
		_previousBody = 0m;
		_hasPreviousBody = false;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		// Stop orders are matched against trade prints, so they fire between candle closes.
		Subscribe(new Subscription(DataType.Ticks, Security));
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

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

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

		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);

		if (Position != 0m && IsExitDue(candle.ClosePrice, body, point))
		{
			CancelIfActive(_buyStop);
			CancelIfActive(_sellStop);
			Flatten();
		}
		else if ((Position == 0m && !IsWorking(_buyStop) && !IsWorking(_sellStop)) || IsExpired(candle.OpenTime))
		{
			ArmStops(candle.ClosePrice, candle.OpenTime, point);
		}

		_previousBody = body;
		_hasPreviousBody = true;
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;
		var signed = trade.Order.Side == Sides.Buy ? volume : -volume;
		var previous = _filledPosition;

		_filledPosition += signed;

		if (_filledPosition == 0m)
			_entryValue = 0m;
		else if (previous == 0m || Math.Sign(previous) == Math.Sign(signed))
			_entryValue += price * volume;
		else if (Math.Sign(_filledPosition) != Math.Sign(previous))
			_entryValue = price * Math.Abs(_filledPosition);
		else
			_entryValue -= _entryValue / Math.Abs(previous) * volume;
	}

	private bool IsExitDue(decimal price, decimal body, decimal point)
	{
		var bodyLimit = StabilizationPoints * point;
		var pnl = FloatingPnL(price, point);
		var oneSmall = body <= bodyLimit;
		var twoSmall = oneSmall && _hasPreviousBody && _previousBody <= bodyLimit;

		return (oneSmall && pnl > ProfitThreshold) ||
			twoSmall ||
			(AbsoluteFixation > 0m && pnl >= AbsoluteFixation);
	}

	private bool IsExpired(DateTime time)
		=> ExpirationMinutes > 0 &&
			_createdAt is DateTime created &&
			time - created >= TimeSpan.FromMinutes(ExpirationMinutes);

	// Flat: both stops are placed around the close. With a position open only the pending
	// opposite stop is moved; the side that opened the position is never placed again.
	private void ArmStops(decimal center, DateTime time, decimal point)
	{
		var distance = OrderDistancePoints * point;

		if (Position == 0m || (Position < 0m && IsWorking(_buyStop)))
			_buyStop = RenewStop(_buyStop, Sides.Buy, center + distance);

		if (Position == 0m || (Position > 0m && IsWorking(_sellStop)))
			_sellStop = RenewStop(_sellStop, Sides.Sell, center - distance);

		_createdAt = IsWorking(_buyStop) || IsWorking(_sellStop) ? time : null;
	}

	private Order RenewStop(Order order, Sides side, decimal activationPrice)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
		else if (IsWorking(order))
			return order;

		var stop = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Side = side,
			Volume = OrderVolume,
			Type = OrderTypes.Conditional,
			Condition = new StopOrderCondition { ActivationPrice = activationPrice },
		};

		RegisterOrder(stop);
		return stop;
	}

	private static bool IsWorking(Order order)
		=> order is not null && order.State is not (OrderStates.Done or OrderStates.Failed);

	private void CancelIfActive(Order order)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
	}

	private decimal FloatingPnL(decimal price, decimal point)
	{
		if (Position == 0m || _filledPosition == 0m)
			return 0m;

		var entryPrice = _entryValue / Math.Abs(_filledPosition);
		var direction = Position > 0m ? 1m : -1m;
		var move = (price - entryPrice) * direction;
		var stepPrice = Security?.StepPrice ?? 0m;

		return stepPrice > 0m
			? move / point * stepPrice * Math.Abs(Position)
			: move * (Security?.Multiplier ?? 1m) * Math.Abs(Position);
	}

	private void Flatten()
	{
		if (Position > 0m)
			SellMarket(Math.Abs(Position));
		else if (Position < 0m)
			BuyMarket(Math.Abs(Position));
	}
}