Ver en GitHub

Ambush Strategy

The Ambush strategy continuously surrounds the market with a pair of buy-stop and sell-stop orders. The pending orders are placed at a configurable indentation above the best ask and below the best bid, with a dynamic override that enforces a minimal distance based on the current spread. Whenever one side is triggered the strategy immediately rebuilds both orders so that the market stays "ambushed" from both directions. A simple equity-based circuit breaker can flatten positions once a daily profit target or loss limit is reached.

This C# implementation replicates the behaviour of the original MetaTrader 5 expert by Zuzabush. It operates purely on Level 1 quotes and does not require candles or indicators. Every decision is driven by real-time bid/ask changes, so the strategy is best suited for liquid instruments with tight spreads.

Trading Logic

  1. Market data intake
    • The strategy subscribes to Level 1 updates and tracks the latest best bid and best ask.
    • Calculations stop until both sides of the order book are available and the strategy is allowed to trade.
  2. Equity safeguards
    • The realised PnL (PnL) and the unrealised component derived from the current bid/ask and PositionPrice are summed.
    • If the combined equity exceeds EquityTakeProfit, or drops below -EquityStopLoss, the current net position is flattened with a market order. Pending orders are left intact, matching the original expert behaviour.
  3. Pending order placement
    • Spread in price units is compared with MaxSpreadPoints. If the spread is too wide, no new orders are placed.
    • Otherwise a distance is calculated as max(IndentationPoints * step, spread * 3). That value replicates the MT5 logic of either respecting the user indentation or enforcing three spreads when the broker StopsLevel is zero.
    • A buy-stop order is placed at ask + distance and a sell-stop at bid - distance. Prices are normalised to the nearest tick. Only one active order per side is allowed; stale orders are cleaned up when their state transitions to Done, Failed, or Canceled.
  4. Trailing of pending orders
    • When TrailingStopPoints is greater than zero, the strategy periodically (no more frequently than Pause) recalculates the stop distance using max((TrailingStopPoints + TrailingStepPoints) * step, spread * 3) and re-registers the orders if the change exceeds half a tick.
    • Trailing keeps the orders close to the market while still respecting the minimum distance that avoids premature triggering.

The end result is a grid-like breakout engine that is constantly waiting for price to move decisively in either direction.

Parameters

Parameter Description
IndentationPoints Base distance in points between the market and each pending stop order.
MaxSpreadPoints Maximum allowed spread (in points). Orders are suspended while the spread is wider.
TrailingStopPoints Base trailing distance in points applied to existing pending orders. Set to zero to disable trailing.
TrailingStepPoints Additional buffer added on top of the trailing base distance.
Pause Minimum time between two trailing recalculations. The default mirrors the one-second pause from the MT5 expert.
EquityTakeProfit Equity profit in account currency that triggers an immediate position flattening.
EquityStopLoss Allowed equity drawdown before the open position is closed.
Volume Order size inherited from the base Strategy class. Use the broker minimum to mimic the MT5 default.

All price offsets are converted from points to actual price units using Security.PriceStep. If the instrument does not expose a price step, a fallback value of 1 is used.

Practical Notes

  • Because the strategy works with stop orders only, no candles or indicators are required. It can run during backtests that do not provide historical candles as long as Level 1 data is available.
  • Brokers that enforce a non-zero StopsLevel should configure IndentationPoints so that the resulting price difference satisfies the exchange rule. The triple-spread safety net acts as a secondary guard.
  • The equity filter is intentionally light-touch and does not cancel pending orders. This mirrors the original Ambush behaviour, allowing new trades after the flattening event without manual intervention.
  • Slippage and order fill tolerance are controlled by the connected broker or simulator. Adjust Volume and parameter values to match the instrument volatility.

This documentation intentionally provides the maximum level of detail so that both discretionary and algorithmic traders can understand the conversion and customise the strategy for their execution venue.

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;
	}
}