Ambush 策略
Ambush 策略会持续在市场两侧放置一对 Buy Stop 和 Sell Stop 挂单。挂单距离当前最优报价一定的点数,同时根据当前 点差设置最小距离限制。一旦任意方向被触发,策略会立即重新构建两侧的挂单,让市场始终处于「埋伏圈」。此外 还提供基于权益的风控:当盈亏超过设定阈值时会立刻平掉现有仓位。
本转换版本完整复刻了 Zuzabush 编写的 MT5 专家顾问,只依赖 Level 1 行情,不需要蜡烛或指标,非常适合点差较小、 成交活跃的品种。
交易逻辑
- 行情采集
- 订阅 Level 1 更新,保存最新的买一与卖一价格。
- 在任意一侧缺失或策略不允许交易时,停止后续计算。
- 权益保护
- 实现盈亏 (
PnL) 与根据PositionPrice、当前买卖价计算出的浮动盈亏相加得到总权益。 - 当权益达到
EquityTakeProfit或跌破-EquityStopLoss时,通过市价单平掉当前净头寸。挂单保持不变,与原顾问一致。
- 实现盈亏 (
- 挂单布局
- 将当前点差(价格单位)与
MaxSpreadPoints比较,若点差过大则暂不下单。 - 否则计算
max(IndentationPoints * 最小跳动, 点差 * 3)的距离,重现 MT5 中StopsLevel为零时使用三倍点差的逻辑。 - 在
ask + 距离放置 Buy Stop,在bid - 距离放置 Sell Stop,并按最小跳动归一化价格。每侧仅保留一个活动挂单, 已完成或失败的订单会被清理。
- 将当前点差(价格单位)与
- 挂单跟踪
- 当
TrailingStopPoints大于 0 时,策略在不短于Pause的间隔后重新计算 `max((TrailingStopPoints + TrailingStepPoints)- 最小跳动, 点差 * 3)`,若新价格与原价差异超过半个跳动,则取消并重新下单。
- 这种方式既能让挂单紧跟市场,也能避免距离过近导致的误触发。
- 当
最终形成一个对称的突破引擎,始终等待价格向任意方向的突破。
参数说明
| 参数 | 说明 |
|---|---|
IndentationPoints |
挂单与市场之间的基础点数距离。 |
MaxSpreadPoints |
允许的最大点差,点差更大时暂停下单。 |
TrailingStopPoints |
对现有挂单应用的基础跟踪距离,0 表示禁用。 |
TrailingStepPoints |
叠加在基础距离上的额外跟踪步长。 |
Pause |
两次跟踪计算之间的最小时间间隔,默认 1 秒,与原 MT5 顾问一致。 |
EquityTakeProfit |
账户权益达到该利润时立即平仓。 |
EquityStopLoss |
账户权益回撤到该数值时立即平仓。 |
Volume |
下单手数,继承自 Strategy 基类,可设置为券商允许的最小手数。 |
所有以点数表示的偏移都会通过 Security.PriceStep 转换为真实价格单位;若品种没有提供最小跳动,则使用 1 作为后备。
使用提示
- 策略完全基于挂单,可在没有蜡烛历史的回测环境中运行,只要能够获得 Level 1 行情即可。
- 如果券商要求非零的
StopsLevel,应适当增大IndentationPoints,保证最终距离满足交易所规则,三倍点差机制 可作为附加保护。 - 权益风控不会取消挂单,以便在平仓后立即继续等待机会,这一行为符合原始 Ambush 顾问。
- 滑点和撮合由实际券商或仿真撮合器决定,请结合目标市场波动性调整参数与下单数量。
本文档刻意提供尽可能详细的信息,帮助交易员全面了解转换细节并根据自身交易场景做进一步定制。
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()