Ver no GitHub

Estratégia de Emboscada

A Estratégia de Emboscada cerca continuamente o mercado com um par de ordens buy-stop e sell-stop. As ordens pendentes são colocadas a uma indentação configurável acima do melhor ask e abaixo do melhor bid, com uma substituição dinâmica que impõe uma distância mínima baseada no spread atual. Sempre que um lado é acionado, a estratégia reconstrói imediatamente ambas as ordens para que o mercado permaneça "em emboscada" de ambas as direções. Um disjuntor simples baseado em patrimônio pode nivelar as posições assim que um alvo de lucro diário ou limite de perda é atingido.

Esta implementação em C# replica o comportamento do especialista original do MetaTrader 5 de Zuzabush. Opera puramente com cotações de Nível 1 e não requer velas ou indicadores. Cada decisão é impulsionada por mudanças em tempo real do bid/ask, portanto a estratégia é mais adequada para instrumentos líquidos com spreads apertados.

Lógica de trading

  1. Recepção de dados de mercado
    • A estratégia assina atualizações de Nível 1 e rastreia o último melhor bid e best ask.
    • Os cálculos param até que ambos os lados do livro de ordens estejam disponíveis e a estratégia tenha permissão para negociar.
  2. Salvaguardas de patrimônio
    • O PnL realizado (PnL) e o componente não realizado derivado do bid/ask atual e PositionPrice são somados.
    • Se o patrimônio combinado exceder EquityTakeProfit, ou cair abaixo de -EquityStopLoss, a posição líquida atual é nivelada com uma ordem a mercado. As ordens pendentes são deixadas intactas, correspondendo ao comportamento original do especialista.
  3. Colocação de ordens pendentes
    • O spread em unidades de preço é comparado com MaxSpreadPoints. Se o spread for muito amplo, nenhuma nova ordem é colocada.
    • Caso contrário, uma distância é calculada como max(IndentationPoints * step, spread * 3). Esse valor replica a lógica MT5 de respeitar a indentação do usuário ou impor três spreads quando o StopsLevel do corretor é zero.
    • Uma ordem buy-stop é colocada em ask + distância e uma sell-stop em bid - distância. Os preços são normalizados para o tick mais próximo. Apenas uma ordem ativa por lado é permitida; ordens obsoletas são limpas quando seu estado muda para Done, Failed ou Canceled.
  4. Trailing de ordens pendentes
    • Quando TrailingStopPoints é maior que zero, a estratégia recalcula periodicamente (não mais frequentemente que Pause) a distância de stop usando max((TrailingStopPoints + TrailingStepPoints) * step, spread * 3) e re-registra as ordens se a mudança exceder meio tick.
    • O trailing mantém as ordens próximas ao mercado enquanto ainda respeita a distância mínima que evita o acionamento prematuro.

O resultado final é um motor de rompimento tipo grid que está constantemente esperando que o preço se mova decisivamente em qualquer direção.

Parâmetros

Parâmetro Descrição
IndentationPoints Distância base em pontos entre o mercado e cada ordem stop pendente.
MaxSpreadPoints Spread máximo permitido (em pontos). As ordens são suspensas enquanto o spread for mais amplo.
TrailingStopPoints Distância base de trailing em pontos aplicada a ordens pendentes existentes. Definir como zero para desativar o trailing.
TrailingStepPoints Buffer adicional adicionado acima da distância base de trailing.
Pause Tempo mínimo entre dois recálculos de trailing. O padrão espelha a pausa de um segundo do especialista MT5.
EquityTakeProfit Lucro de patrimônio em moeda da conta que aciona um nivelamento imediato de posição.
EquityStopLoss Queda de patrimônio permitida antes que a posição aberta seja fechada.
Volume Tamanho da ordem herdado da classe base Strategy. Usar o mínimo do corretor para imitar o padrão MT5.

Todos os offsets de preço são convertidos de pontos para unidades de preço reais usando Security.PriceStep. Se o instrumento não expõe um passo de preço, um valor de fallback de 1 é usado.

Notas práticas

  • Como a estratégia trabalha apenas com ordens stop, nenhuma vela ou indicador é necessário. Pode ser executada durante backtests que não fornecem velas históricas desde que os dados de Nível 1 estejam disponíveis.
  • Os corretores que impõem um StopsLevel não nulo devem configurar IndentationPoints para que a diferença de preço resultante satisfaça a regra da bolsa. A rede de segurança de triplo spread atua como guarda secundária.
  • O filtro de patrimônio é intencionalmente leve e não cancela ordens pendentes. Isso espelha o comportamento original de Emboscada, permitindo novas negociações após o evento de nivelamento sem intervenção manual.
  • O deslizamento e a tolerância de preenchimento de ordens são controlados pelo corretor ou simulador conectado. Ajustar Volume e valores de parâmetros para corresponder à volatilidade do instrumento.

Esta documentação fornece intencionalmente o nível máximo de detalhe para que tanto traders discricionários quanto algorítmicos possam entender a conversão e personalizar a estratégia para seu local de execução.

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>
/// Breakout strategy converted from the Ambush MQL5 expert.
/// Enters on breakouts above/below previous candle range with trailing stop management.
/// </summary>
public class AmbushStrategy : Strategy
{
	private readonly StrategyParam<decimal> _indentationPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _equityTakeProfit;
	private readonly StrategyParam<decimal> _equityStopLoss;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal _priceStep;

	/// <summary>
	/// Distance from the market price for breakout detection, in points.
	/// </summary>
	public decimal IndentationPoints
	{
		get => _indentationPoints.Value;
		set => _indentationPoints.Value = value;
	}

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

	/// <summary>
	/// Trailing step added to the base trailing distance, in points.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Target equity profit that triggers position flattening.
	/// </summary>
	public decimal EquityTakeProfit
	{
		get => _equityTakeProfit.Value;
		set => _equityTakeProfit.Value = value;
	}

	/// <summary>
	/// Maximum equity drawdown allowed before flattening positions.
	/// </summary>
	public decimal EquityStopLoss
	{
		get => _equityStopLoss.Value;
		set => _equityStopLoss.Value = value;
	}

	/// <summary>
	/// Candle type used for breakout detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="AmbushStrategy"/> class.
	/// </summary>
	public AmbushStrategy()
	{
		_indentationPoints = Param(nameof(IndentationPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Indentation (points)", "Distance from price for breakout", "Orders");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 10m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (points)", "Base trailing distance", "Orders");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Additional trailing offset", "Orders");

		_equityTakeProfit = Param(nameof(EquityTakeProfit), 15m)
			.SetNotNegative()
			.SetDisplay("Equity Take Profit", "Flatten positions once this profit is reached", "Risk");

		_equityStopLoss = Param(nameof(EquityStopLoss), 5m)
			.SetNotNegative()
			.SetDisplay("Equity Stop Loss", "Flatten positions after this loss", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(6).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for breakout detection", "General");

		Volume = 1;
	}

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

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

		_previousCandle = null;
		_entryPrice = 0m;
		_stopPrice = null;
		_priceStep = 0m;
	}

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

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

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		// Check equity targets.
		var pnl = PnL;
		if (EquityTakeProfit > 0m && pnl >= EquityTakeProfit)
		{
			FlattenPosition();
			_previousCandle = candle;
			return;
		}
		if (EquityStopLoss > 0m && pnl <= -EquityStopLoss)
		{
			FlattenPosition();
			_previousCandle = candle;
			return;
		}

		// Check trailing stop.
		if (Position > 0 && _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			SellMarket(Position);
			ResetTargets();
		}
		else if (Position < 0 && _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			BuyMarket(Math.Abs(Position));
			ResetTargets();
		}

		// Update trailing stop.
		UpdateTrailing(candle);

		// Entry logic - breakout above/below previous candle range.
		if (Position == 0 && _previousCandle != null)
		{
			var indentation = IndentationPoints * _priceStep;
			var buyLevel = _previousCandle.HighPrice + indentation;
			var sellLevel = _previousCandle.LowPrice - indentation;

			if (candle.HighPrice >= buyLevel)
			{
				BuyMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
				_stopPrice = trailDist > 0m ? _entryPrice - trailDist : null;
			}
			else if (candle.LowPrice <= sellLevel)
			{
				SellMarket(Volume);
				_entryPrice = candle.ClosePrice;
				var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
				_stopPrice = trailDist > 0m ? _entryPrice + trailDist : null;
			}
		}

		_previousCandle = candle;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
			return;

		var trailDist = (TrailingStopPoints + TrailingStepPoints) * _priceStep;
		if (trailDist <= 0m)
			return;

		if (Position > 0)
		{
			var newStop = candle.ClosePrice - trailDist;
			if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
				_stopPrice = newStop;
		}
		else if (Position < 0)
		{
			var newStop = candle.ClosePrice + trailDist;
			if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
				_stopPrice = newStop;
		}
	}

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

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = null;
	}
}