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
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.
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.
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class ambush_strategy(Strategy):
def __init__(self):
super(ambush_strategy, self).__init__()
self._indentation_points = self.Param("IndentationPoints", 10.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 10.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 1.0)
self._equity_take_profit = self.Param("EquityTakeProfit", 15.0)
self._equity_stop_loss = self.Param("EquityStopLoss", 5.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(6)))
self.Volume = 1
self._previous_candle = None
self._entry_price = 0.0
self._stop_price = None
self._price_step = 1.0
@property
def IndentationPoints(self):
return self._indentation_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def TrailingStepPoints(self):
return self._trailing_step_points.Value
@property
def EquityTakeProfit(self):
return self._equity_take_profit.Value
@property
def EquityStopLoss(self):
return self._equity_stop_loss.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ambush_strategy, self).OnStarted2(time)
sec = self.Security
self._price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
pos = float(self.Position)
pnl = float(self.PnL)
if float(self.EquityTakeProfit) > 0 and pnl >= float(self.EquityTakeProfit):
self._flatten_position()
self._previous_candle = candle
return
if float(self.EquityStopLoss) > 0 and pnl <= -float(self.EquityStopLoss):
self._flatten_position()
self._previous_candle = candle
return
if pos > 0 and self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(pos)
self._reset_targets()
elif pos < 0 and self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(pos))
self._reset_targets()
self._update_trailing(candle)
pos = float(self.Position)
if pos == 0 and self._previous_candle is not None:
indentation = float(self.IndentationPoints) * self._price_step
buy_level = float(self._previous_candle.HighPrice) + indentation
sell_level = float(self._previous_candle.LowPrice) - indentation
if float(candle.HighPrice) >= buy_level:
self.BuyMarket(float(self.Volume))
self._entry_price = float(candle.ClosePrice)
trail_dist = (float(self.TrailingStopPoints) + float(self.TrailingStepPoints)) * self._price_step
self._stop_price = self._entry_price - trail_dist if trail_dist > 0 else None
elif float(candle.LowPrice) <= sell_level:
self.SellMarket(float(self.Volume))
self._entry_price = float(candle.ClosePrice)
trail_dist = (float(self.TrailingStopPoints) + float(self.TrailingStepPoints)) * self._price_step
self._stop_price = self._entry_price + trail_dist if trail_dist > 0 else None
self._previous_candle = candle
def _update_trailing(self, candle):
if float(self.TrailingStopPoints) <= 0:
return
trail_dist = (float(self.TrailingStopPoints) + float(self.TrailingStepPoints)) * self._price_step
if trail_dist <= 0:
return
pos = float(self.Position)
if pos > 0:
new_stop = float(candle.ClosePrice) - trail_dist
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
elif pos < 0:
new_stop = float(candle.ClosePrice) + trail_dist
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
def _flatten_position(self):
pos = float(self.Position)
if pos > 0:
self.SellMarket(pos)
elif pos < 0:
self.BuyMarket(abs(pos))
self._reset_targets()
def _reset_targets(self):
self._entry_price = 0.0
self._stop_price = None
def OnReseted(self):
super(ambush_strategy, self).OnReseted()
self._previous_candle = None
self._entry_price = 0.0
self._stop_price = None
self._price_step = 0.0
def CreateClone(self):
return ambush_strategy()