Waddah Attar Win Grid Strategy
Waddah Attar Win Grid Strategy 重现了 MQL/8210 中的 MetaTrader 4 专家顾问。策略在当前买价和卖价周围维持对称的限价单网格,当价格逼近最近的网格层级时,会在更远一个步长的位置自动补挂新的限价单,并且可以选择对每个新订单增加手数。每次收到盘口更新都会重新计算浮动盈亏,一旦账户权益相对于基准值的增长达到设定阈值,系统会同时平掉所有仓位并撤销全部挂单。
工作流程
- 初始化阶段
- 订阅盘口数据,以便即时获取最新的买价/卖价。
- 记录当前账户权益,作为后续比较的基准值。
- 启用 StockSharp 内置的风险保护模块。
- 基准权益管理
- 当没有任何挂单且净头寸为零时,最新的账户权益会写入基准值,模拟原始 EA 在每个 tick 上保存账户余额的行为。
- 建立初始网格
- 在允许交易且没有挂单的情况下,同时放置两笔限价单:
- 在当前卖价下方
Step Points点位挂出买入限价单。 - 在当前买价上方
Step Points点位挂出卖出限价单。
- 在当前卖价下方
- 两笔订单都使用
First Volume作为手数。
- 在允许交易且没有挂单的情况下,同时放置两笔限价单:
- 扩展网格
- 当卖价距离最近的买入限价单不足五个最小价格步长时,在上一个买单下方再挂一个新的买入限价单。
- 当买价距离最近的卖出限价单不足五个最小价格步长时,在上一个卖单上方再挂一个新的卖出限价单。
- 新挂出的订单手数在上一笔的基础上增加
Increment Volume,从而实现类似马丁格尔的加仓方式。
- 获利退出
- 浮动盈亏等于当前账户权益减去基准值。
- 当该数值超过
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;
}
}
}
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 waddah_attar_win_grid_strategy(Strategy):
def __init__(self):
super(waddah_attar_win_grid_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 1500).SetDisplay("Step (Points)", "Distance between grid levels in points", "Grid")
self._first_volume = self.Param("FirstVolume", 0.1).SetDisplay("First Volume", "Volume for the initial orders", "Trading")
self._increment_volume = self.Param("IncrementVolume", 0.0).SetDisplay("Increment Volume", "Additional volume added when stacking new orders", "Trading")
self._min_profit = self.Param("MinProfit", 450.0).SetDisplay("Min Profit", "Floating profit target in account currency", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle type for price data", "General")
self._last_buy_grid_price = 0.0
self._last_sell_grid_price = 0.0
self._current_buy_volume = 0.0
self._current_sell_volume = 0.0
self._reference_balance = 0.0
self._grid_active = False
@property
def StepPoints(self): return self._step_points.Value
@property
def FirstVolume(self): return self._first_volume.Value
@property
def IncrementVolume(self): return self._increment_volume.Value
@property
def MinProfit(self): return self._min_profit.Value
@property
def CandleType(self): return self._candle_type.Value
def OnStarted2(self, time):
super(waddah_attar_win_grid_strategy, self).OnStarted2(time)
self._reference_balance = float(self.Portfolio.CurrentValue) if self.Portfolio is not None else 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
price_step = 0.01
if self.Security is not None and self.Security.PriceStep is not None and float(self.Security.PriceStep) > 0:
price_step = float(self.Security.PriceStep)
step_offset = int(self.StepPoints) * price_step
if step_offset <= 0:
return
price = float(candle.ClosePrice)
current_value = float(self.Portfolio.CurrentValue) if self.Portfolio is not None else 0.0
floating_profit = current_value - self._reference_balance
if float(self.MinProfit) > 0 and floating_profit >= float(self.MinProfit) and self._grid_active:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
self._reference_balance = float(self.Portfolio.CurrentValue) if self.Portfolio is not None else self._reference_balance
self._grid_active = False
self._last_buy_grid_price = 0.0
self._last_sell_grid_price = 0.0
self._current_buy_volume = 0.0
self._current_sell_volume = 0.0
return
if not self._grid_active:
self._last_buy_grid_price = price
self._last_sell_grid_price = price
self._current_buy_volume = float(self.FirstVolume)
self._current_sell_volume = float(self.FirstVolume)
self._grid_active = True
self._reference_balance = float(self.Portfolio.CurrentValue) if self.Portfolio is not None else self._reference_balance
return
if price <= self._last_buy_grid_price - step_offset:
self.BuyMarket(self._current_buy_volume)
self._last_buy_grid_price = price
self._current_buy_volume += float(self.IncrementVolume)
if price >= self._last_sell_grid_price + step_offset:
self.SellMarket(self._current_sell_volume)
self._last_sell_grid_price = price
self._current_sell_volume += float(self.IncrementVolume)
def OnReseted(self):
super(waddah_attar_win_grid_strategy, self).OnReseted()
self._last_buy_grid_price = 0.0
self._last_sell_grid_price = 0.0
self._current_buy_volume = 0.0
self._current_sell_volume = 0.0
self._reference_balance = 0.0
self._grid_active = False
def CreateClone(self):
return waddah_attar_win_grid_strategy()