在 GitHub 上查看

Waddah Attar Win Grid Strategy

Waddah Attar Win Grid Strategy 重现了 MQL/8210 中的 MetaTrader 4 专家顾问。策略在当前买价和卖价周围维持对称的限价单网格,当价格逼近最近的网格层级时,会在更远一个步长的位置自动补挂新的限价单,并且可以选择对每个新订单增加手数。每次收到盘口更新都会重新计算浮动盈亏,一旦账户权益相对于基准值的增长达到设定阈值,系统会同时平掉所有仓位并撤销全部挂单。

工作流程

  1. 初始化阶段
    • 订阅盘口数据,以便即时获取最新的买价/卖价。
    • 记录当前账户权益,作为后续比较的基准值。
    • 启用 StockSharp 内置的风险保护模块。
  2. 基准权益管理
    • 当没有任何挂单且净头寸为零时,最新的账户权益会写入基准值,模拟原始 EA 在每个 tick 上保存账户余额的行为。
  3. 建立初始网格
    • 在允许交易且没有挂单的情况下,同时放置两笔限价单:
      • 在当前卖价下方 Step Points 点位挂出买入限价单。
      • 在当前买价上方 Step Points 点位挂出卖出限价单。
    • 两笔订单都使用 First Volume 作为手数。
  4. 扩展网格
    • 当卖价距离最近的买入限价单不足五个最小价格步长时,在上一个买单下方再挂一个新的买入限价单。
    • 当买价距离最近的卖出限价单不足五个最小价格步长时,在上一个卖单上方再挂一个新的卖出限价单。
    • 新挂出的订单手数在上一笔的基础上增加 Increment Volume,从而实现类似马丁格尔的加仓方式。
  5. 获利退出
    • 浮动盈亏等于当前账户权益减去基准值。
    • 当该数值超过 Min Profit 时,策略会撤销所有挂单并通过 CloseAll 平掉全部持仓。
    • 随后刷新基准值,以便开始下一轮循环。

策略特性

  • 数据来源:仅依赖盘口第一档(最佳买价/卖价)。
  • 订单类型:只生成限价单,不会主动发送市价单或止损单。
  • 持仓结构:在支持对冲的账户中可以同时持有多头和空头。
  • 风险控制:没有硬性止损,风险需要依靠浮动盈亏目标或外部风控措施。
  • 自动重启:当仓位被平掉或挂单被人工取消后,下一次循环会重新建立初始网格。

参数说明

参数 默认值 说明
Step Points 120 网格层级之间的距离,单位为价格点(最小价格步长的倍数)。
First Volume 0.1 第一对限价单使用的下单手数。
Increment Volume 0.0 每次新增订单时增加的手数;设为 0 表示所有订单手数一致。
Min Profit 450 触发全部平仓的浮动利润阈值(账户货币)。

注意事项

  • 请确认交易品种的 PriceStep 设置正确,策略会用它与 Step Points 相乘来计算具体价格。
  • 频繁的撤单与改挂可能受到经纪商或交易所挂单数量限制的影响。
  • 策略对极端行情缺乏自我保护,建议结合组合层面的风险控制工具使用。
  • 如果市场单边趋势明显,网格可能持续扩张;应谨慎设置 Increment Volume 以控制保证金占用。

文件结构

  • CS/WaddahAttarWinGridStrategy.cs — C# 版本的策略源码。
  • README.md — 英文说明。
  • README_ru.md — 俄文说明。
  • README_zh.md — 本中文说明。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Grid strategy converted from the "Waddah Attar Win" MetaTrader 4 expert advisor.
/// Places paired orders around the market, pyramids positions with an optional volume increment,
/// and closes the entire exposure once the floating profit target is achieved.
/// </summary>
public class WaddahAttarWinGridStrategy : Strategy
{
	private readonly StrategyParam<int> _stepPoints;
	private readonly StrategyParam<decimal> _firstVolume;
	private readonly StrategyParam<decimal> _incrementVolume;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _lastBuyGridPrice;
	private decimal _lastSellGridPrice;
	private decimal _currentBuyVolume;
	private decimal _currentSellVolume;
	private decimal _referenceBalance;
	private bool _gridActive;

	/// <summary>
	/// Distance in price points between consecutive grid levels.
	/// </summary>
	public int StepPoints
	{
		get => _stepPoints.Value;
		set => _stepPoints.Value = value;
	}

	/// <summary>
	/// Volume for the very first pair of orders.
	/// </summary>
	public decimal FirstVolume
	{
		get => _firstVolume.Value;
		set => _firstVolume.Value = value;
	}

	/// <summary>
	/// Volume increment applied to each newly stacked order.
	/// </summary>
	public decimal IncrementVolume
	{
		get => _incrementVolume.Value;
		set => _incrementVolume.Value = value;
	}

	/// <summary>
	/// Floating profit target in account currency that closes all positions.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Candle type for price data.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public WaddahAttarWinGridStrategy()
	{
		_stepPoints = Param(nameof(StepPoints), 1500)
			.SetGreaterThanZero()
			.SetDisplay("Step (Points)", "Distance between grid levels in points", "Grid")
			.SetOptimize(20, 400, 10);

		_firstVolume = Param(nameof(FirstVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("First Volume", "Volume for the initial orders", "Trading");

		_incrementVolume = Param(nameof(IncrementVolume), 0m)
			.SetDisplay("Increment Volume", "Additional volume added when stacking new orders", "Trading");

		_minProfit = Param(nameof(MinProfit), 450m)
			.SetNotNegative()
			.SetDisplay("Min Profit", "Floating profit target in account currency", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for price data", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_lastBuyGridPrice = 0m;
		_lastSellGridPrice = 0m;
		_currentBuyVolume = 0m;
		_currentSellVolume = 0m;
		_referenceBalance = 0m;
		_gridActive = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_referenceBalance = Portfolio?.CurrentValue ?? 0m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var priceStep = Security?.PriceStep ?? 0.01m;
		if (priceStep <= 0m)
			priceStep = 0.01m;

		var stepOffset = StepPoints * priceStep;
		if (stepOffset <= 0m)
			return;

		var price = candle.ClosePrice;

		// Check profit target
		var floatingProfit = (Portfolio?.CurrentValue ?? 0m) - _referenceBalance;
		if (MinProfit > 0m && floatingProfit >= MinProfit && _gridActive)
		{
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(Math.Abs(Position));

			_referenceBalance = Portfolio?.CurrentValue ?? _referenceBalance;
			_gridActive = false;
			_lastBuyGridPrice = 0m;
			_lastSellGridPrice = 0m;
			_currentBuyVolume = 0m;
			_currentSellVolume = 0m;
			return;
		}

		// Initialize grid on first candle
		if (!_gridActive)
		{
			_lastBuyGridPrice = price;
			_lastSellGridPrice = price;
			_currentBuyVolume = FirstVolume;
			_currentSellVolume = FirstVolume;
			_gridActive = true;
			_referenceBalance = Portfolio?.CurrentValue ?? _referenceBalance;
			return;
		}

		// Check if price dropped enough to trigger a buy grid level
		if (price <= _lastBuyGridPrice - stepOffset)
		{
			BuyMarket(_currentBuyVolume);
			_lastBuyGridPrice = price;
			_currentBuyVolume += IncrementVolume;
		}

		// Check if price rose enough to trigger a sell grid level
		if (price >= _lastSellGridPrice + stepOffset)
		{
			SellMarket(_currentSellVolume);
			_lastSellGridPrice = price;
			_currentSellVolume += IncrementVolume;
		}
	}
}