在 GitHub 上查看

Ambush 策略

Ambush 策略会持续在市场两侧放置一对 Buy Stop 和 Sell Stop 挂单。挂单距离当前最优报价一定的点数,同时根据当前 点差设置最小距离限制。一旦任意方向被触发,策略会立即重新构建两侧的挂单,让市场始终处于「埋伏圈」。此外 还提供基于权益的风控:当盈亏超过设定阈值时会立刻平掉现有仓位。

本转换版本完整复刻了 Zuzabush 编写的 MT5 专家顾问,只依赖 Level 1 行情,不需要蜡烛或指标,非常适合点差较小、 成交活跃的品种。

交易逻辑

  1. 行情采集
    • 订阅 Level 1 更新,保存最新的买一与卖一价格。
    • 在任意一侧缺失或策略不允许交易时,停止后续计算。
  2. 权益保护
    • 实现盈亏 (PnL) 与根据 PositionPrice、当前买卖价计算出的浮动盈亏相加得到总权益。
    • 当权益达到 EquityTakeProfit 或跌破 -EquityStopLoss 时,通过市价单平掉当前净头寸。挂单保持不变,与原顾问一致。
  3. 挂单布局
    • 将当前点差(价格单位)与 MaxSpreadPoints 比较,若点差过大则暂不下单。
    • 否则计算 max(IndentationPoints * 最小跳动, 点差 * 3) 的距离,重现 MT5 中 StopsLevel 为零时使用三倍点差的逻辑。
    • ask + 距离 放置 Buy Stop,在 bid - 距离 放置 Sell Stop,并按最小跳动归一化价格。每侧仅保留一个活动挂单, 已完成或失败的订单会被清理。
  4. 挂单跟踪
    • 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;
	}
}