Waddah Attar Win 策略
该策略复刻了 Waddah Attar Win 智能顾问的思路。它始终在当前买卖价附近保持对称的买入限价单和卖出限价单网格,间距为固定的点数。当价格靠近最新挂出的订单时,策略会在相同距离处再堆叠一个限价单,并可按参数增加手数。每次收到盘口更新时都会检查浮动利润,只要账户权益高于保存的余额 Min Profit 数额,就会立即平掉所有仓位并撤销挂单。
细节
- 入场条件:
- 初始买入限价单放在买价下方
Step Points个点,卖出限价单放在卖价上方相同距离。 - 当价格距离最近的限价单不超过五个最小跳动单位时,在该方向再挂一个新单。
- 初始买入限价单放在买价下方
- 多空方向:双向对冲网格。
- 出场条件:
- 当权益超过基准余额
Min Profit时,关闭所有仓位并撤销全部挂单。
- 当权益超过基准余额
- 止损:无。
- 默认参数:
Step Points= 20First Volume= 0.1Increment Volume= 0.0Min Profit= 910
- 备注:
- 需要支持对冲的账户才能同时持有多头和空头。
- 通过盘口数据即时响应 bid/ask 变化。
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>
/// Grid strategy that places symmetric market orders around grid levels and takes profit when equity target is reached.
/// Based on Waddah Attar grid concept: define a grid step, enter in the direction of price movement
/// when price crosses a grid level, and close all when profit target is met.
/// </summary>
public class WaddahAttarWinStrategy : Strategy
{
private readonly StrategyParam<int> _stepPoints;
private readonly StrategyParam<decimal> _firstVolume;
private readonly StrategyParam<decimal> _incrementVolume;
private readonly StrategyParam<decimal> _minProfit;
private decimal _gridOrigin;
private int _lastGridIndex;
private decimal _currentVolume;
private decimal _entryPrice;
private bool _initialized;
private int _totalOrders;
/// <summary>
/// Distance in points between grid levels.
/// </summary>
public int StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
/// <summary>
/// Initial volume for orders.
/// </summary>
public decimal FirstVolume
{
get => _firstVolume.Value;
set => _firstVolume.Value = value;
}
/// <summary>
/// Volume increment added on each new grid level entry.
/// </summary>
public decimal IncrementVolume
{
get => _incrementVolume.Value;
set => _incrementVolume.Value = value;
}
/// <summary>
/// Profit target in price points to close the position.
/// </summary>
public decimal MinProfit
{
get => _minProfit.Value;
set => _minProfit.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public WaddahAttarWinStrategy()
{
_stepPoints = Param(nameof(StepPoints), 500)
.SetGreaterThanZero()
.SetDisplay("Step (Points)", "Distance between grid levels in price steps", "General")
.SetOptimize(100, 2000, 100);
_firstVolume = Param(nameof(FirstVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("First Volume", "Volume for grid orders", "General");
_incrementVolume = Param(nameof(IncrementVolume), 0m)
.SetDisplay("Increment Volume", "Additional volume on subsequent grid entries", "General");
_minProfit = Param(nameof(MinProfit), 200m)
.SetNotNegative()
.SetDisplay("Min Profit", "Price movement profit target to close position", "Risk");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_gridOrigin = 0m;
_lastGridIndex = 0;
_currentVolume = 0m;
_entryPrice = 0m;
_initialized = false;
_totalOrders = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var tf = TimeSpan.FromMinutes(5).TimeFrame();
SubscribeCandles(tf)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormed)
return;
// cap total orders to avoid exceeding limits
if (_totalOrders >= 80)
return;
var close = candle.ClosePrice;
var priceStep = Security?.PriceStep ?? 0.01m;
if (priceStep <= 0m)
priceStep = 0.01m;
var stepOffset = StepPoints * priceStep;
if (stepOffset <= 0m)
return;
// initialize grid origin on first candle
if (!_initialized)
{
_gridOrigin = close;
_lastGridIndex = 0;
_currentVolume = FirstVolume;
_initialized = true;
return;
}
// calculate which grid index the price is at
var gridIndex = (int)Math.Floor((close - _gridOrigin) / stepOffset);
// check profit target: close position if in profit
if (Position != 0 && _entryPrice > 0m)
{
var pnl = Position > 0
? close - _entryPrice
: _entryPrice - close;
if (pnl >= MinProfit * priceStep)
{
if (Position > 0)
{
SellMarket();
_totalOrders++;
}
else
{
BuyMarket();
_totalOrders++;
}
// reset grid around current price
_gridOrigin = close;
_lastGridIndex = 0;
_currentVolume = FirstVolume;
_entryPrice = 0m;
return;
}
}
// price crossed to a new grid level
if (gridIndex != _lastGridIndex)
{
if (gridIndex > _lastGridIndex)
{
// price moved up - buy (or add to long / close short)
if (Position < 0)
{
// close short first
BuyMarket();
_totalOrders++;
_entryPrice = close;
_gridOrigin = close;
_lastGridIndex = 0;
_currentVolume = FirstVolume;
}
else
{
BuyMarket();
_totalOrders++;
if (Position <= 0)
_entryPrice = close;
_currentVolume += IncrementVolume;
}
}
else
{
// price moved down - sell (or add to short / close long)
if (Position > 0)
{
// close long first
SellMarket();
_totalOrders++;
_entryPrice = close;
_gridOrigin = close;
_lastGridIndex = 0;
_currentVolume = FirstVolume;
}
else
{
SellMarket();
_totalOrders++;
if (Position >= 0)
_entryPrice = close;
_currentVolume += IncrementVolume;
}
}
_lastGridIndex = gridIndex;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class waddah_attar_win_strategy(Strategy):
"""Grid strategy that places symmetric market orders around grid levels and takes profit when equity target is reached."""
def __init__(self):
super(waddah_attar_win_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 500) \
.SetGreaterThanZero() \
.SetDisplay("Step (Points)", "Distance between grid levels in price steps", "General")
self._first_volume = self.Param("FirstVolume", 1) \
.SetGreaterThanZero() \
.SetDisplay("First Volume", "Volume for grid orders", "General")
self._increment_volume = self.Param("IncrementVolume", 0) \
.SetDisplay("Increment Volume", "Additional volume on subsequent grid entries", "General")
self._min_profit = self.Param("MinProfit", 200) \
.SetNotNegative() \
.SetDisplay("Min Profit", "Price movement profit target to close position", "Risk")
self._grid_origin = 0.0
self._last_grid_index = 0
self._current_volume = 0.0
self._entry_price = 0.0
self._initialized = False
self._total_orders = 0
@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
def OnStarted2(self, time):
super(waddah_attar_win_strategy, self).OnStarted2(time)
tf = DataType.TimeFrame(TimeSpan.FromMinutes(5))
self.SubscribeCandles(tf) \
.Bind(self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
# cap total orders to avoid exceeding limits
if self._total_orders >= 80:
return
close = float(candle.ClosePrice)
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.01
if price_step <= 0:
price_step = 0.01
step_offset = float(self.StepPoints) * price_step
if step_offset <= 0:
return
# initialize grid origin on first candle
if not self._initialized:
self._grid_origin = close
self._last_grid_index = 0
self._current_volume = float(self.FirstVolume)
self._initialized = True
return
# calculate which grid index the price is at
grid_index = int(math.floor((close - self._grid_origin) / step_offset))
# check profit target: close position if in profit
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl = close - self._entry_price
else:
pnl = self._entry_price - close
if pnl >= float(self.MinProfit) * price_step:
if self.Position > 0:
self.SellMarket()
self._total_orders += 1
else:
self.BuyMarket()
self._total_orders += 1
# reset grid around current price
self._grid_origin = close
self._last_grid_index = 0
self._current_volume = float(self.FirstVolume)
self._entry_price = 0.0
return
# price crossed to a new grid level
if grid_index != self._last_grid_index:
if grid_index > self._last_grid_index:
# price moved up - buy
if self.Position < 0:
# close short first
self.BuyMarket()
self._total_orders += 1
self._entry_price = close
self._grid_origin = close
self._last_grid_index = 0
self._current_volume = float(self.FirstVolume)
else:
self.BuyMarket()
self._total_orders += 1
if self.Position <= 0:
self._entry_price = close
self._current_volume = self._current_volume + float(self.IncrementVolume)
else:
# price moved down - sell
if self.Position > 0:
# close long first
self.SellMarket()
self._total_orders += 1
self._entry_price = close
self._grid_origin = close
self._last_grid_index = 0
self._current_volume = float(self.FirstVolume)
else:
self.SellMarket()
self._total_orders += 1
if self.Position >= 0:
self._entry_price = close
self._current_volume = self._current_volume + float(self.IncrementVolume)
self._last_grid_index = grid_index
def OnReseted(self):
super(waddah_attar_win_strategy, self).OnReseted()
self._grid_origin = 0.0
self._last_grid_index = 0
self._current_volume = 0.0
self._entry_price = 0.0
self._initialized = False
self._total_orders = 0
def CreateClone(self):
return waddah_attar_win_strategy()