Стратегия Ambush постоянно окружает рынок парой отложенных ордеров Buy Stop и Sell Stop. Заявки ставятся на заданном отступе от
лучших Ask и Bid, а также проверяются на минимально допустимое расстояние, зависящее от текущего спреда. Как только один ордер
исполняется, стратегия сразу восстанавливает оба стоп-ордера, поэтому рынок остаётся «в засаде» с двух сторон. Дополнительно
реализован эквити-фильтр, который закрывает открытую позицию при достижении целевой прибыли или допустимого убытка.
Данный перевод воспроизводит оригинальный эксперт MetaTrader 5 от Zuzabush. Алгоритм работает только с котировками уровня Level 1,
не требует свечей и индикаторов, поэтому особенно хорошо подходит для ликвидных инструментов с узким спредом.
Логика работы
Получение данных
Подписка на изменения Level 1 и хранение последних значений лучшего Bid и Ask.
Пока один из котировок отсутствует или торговля запрещена, расчёты не выполняются.
Эквити-фильтры
Складываются реализованная прибыль (PnL) и плавающий результат, рассчитанный через PositionPrice и текущие Bid/Ask.
При превышении EquityTakeProfit либо снижении ниже -EquityStopLoss открытая позиция закрывается рыночным ордером.
Отложенные заявки специально не отменяются, что полностью повторяет поведение исходного советника.
Выставление отложенных ордеров
Расчёт текущего спреда в ценовых единицах и сравнение с MaxSpreadPoints. Если спред выше порога, новые заявки не
выставляются.
В противном случае вычисляется расстояние max(IndentationPoints * шаг, спред * 3). Оно повторяет MT5-логику: берётся либо
пользовательский отступ, либо тройной спред при нулевом StopsLevel брокера.
Buy Stop размещается по цене Ask + расстояние, Sell Stop — Bid - расстояние. Значения нормализуются по размеру тика.
Одновременно может существовать только по одной активной заявке каждого типа; выполненные или отменённые ордера очищаются.
Трейлинг отложенных ордеров
Если TrailingStopPoints больше нуля, стратегия не чаще чем раз в интервал Pause пересчитывает дистанцию `max((TrailingStopPoints
TrailingStepPoints) * шаг, спред * 3)` и перевыставляет ордера, когда изменение превышает половину тика.
Трейлинг удерживает стоп-заявки вблизи рынка, но при этом соблюдает минимальное расстояние, исключая случайные срабатывания.
В итоге получается симметричный пробойный движок, который постоянно ожидает решительного импульса цены.
Параметры
Параметр
Описание
IndentationPoints
Базовый отступ в пунктах между рынком и каждым отложенным ордером.
MaxSpreadPoints
Максимально допустимый спред в пунктах. Пока спред выше, новые заявки не выставляются.
TrailingStopPoints
Базовое расстояние трейлинга отложенных заявок. Ноль отключает трейлинг.
TrailingStepPoints
Дополнительный шаг, который прибавляется к базовому расстоянию трейлинга.
Pause
Минимальный интервал между двумя пересчётами трейлинга. Значение по умолчанию соответствует паузе в MT5 (1 секунда).
EquityTakeProfit
Эквити-прибыль в валюте счёта, при которой позиция закрывается.
EquityStopLoss
Допустимая просадка эквити перед принудительным закрытием позиции.
Volume
Размер ордера, унаследованный от базового класса Strategy. Для повторения MT5 имеет смысл использовать минимальный лот.
Все отступы в пунктах переводятся в реальные ценовые значения через Security.PriceStep. Если тик не задан, используется значение 1.
Практические замечания
Стратегия не требует свечей и индикаторов, поэтому легко тестируется на потоке Level 1, даже если история свечей недоступна.
При наличии у брокера ненулевого StopsLevel следует подобрать IndentationPoints так, чтобы конечное расстояние удовлетворяло
требованиям площадки. Тройной спред служит дополнительной страховкой.
Эквити-фильтр не отменяет отложенные ордера — это сделано намеренно, чтобы сразу возобновить торговлю после закрытия позиции.
Контроль проскальзывания и допустимых отклонений полностью зависит от брокера или тестового движка. Параметры и Volume
необходимо адаптировать под волатильность инструмента.
Документация составлена максимально подробно, чтобы трейдер мог полностью понять перенос стратегии и адаптировать её под свою
торговую площадку.
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()