在 GitHub 上查看
Exp Amstell 策略
概述
Exp Amstell 是从 MetaTrader 4 智能交易系统 exp_Amstell.mq4 移植而来的网格策略。当价格相对于最近成交价移动了设定的点数时,它会持续发送买入或卖出市价单。每一笔交易都会独立管理:一旦价格达到设定的止盈距离,策略就会下达反向订单,仅对该网格层进行获利了结。
与依赖指标的趋势策略不同,Exp Amstell 始终保持激活状态。它不等待额外确认,而是在市场来回波动时同时累积多头和空头仓位。因此,入场间距和订单手数的设置对策略表现至关重要。
交易逻辑
- 按行情逐笔处理。 策略订阅 Level1 行情,每当最佳买价或最佳卖价发生变化时立即执行,与原始 MQL 中的
start() 行为一致。
- 多空栈独立。 当没有多头仓位,或最新多头仓位与当前卖价之间的距离超过再入场点数时,允许再次买入。空头侧使用对称的条件并基于买价计算。
- 逐笔止盈。 每个打开的网格层都单独跟踪。当买价(多头)或卖价(空头)向有利方向移动指定点数时,仅关闭对应层,其余仓位保持不变。
- FIFO 模拟。 策略按成交顺序记录每笔交易,模拟 MetaTrader 的持仓票据系统,确保部分平仓时优先减少最早的仓位。
- 兼容净持仓模式。 StockSharp 使用净持仓。如果新的买单抵消了现有的空头层,策略会先从空头栈中扣减对应数量,再把剩余部分登记为新的多头仓位。
参数
| 名称 |
类型 |
默认值 |
说明 |
TradeVolume |
decimal |
0.1 |
每次开仓时使用的市价单数量。 |
TakeProfitPoints |
int |
30 |
以 MetaTrader 点数表示的单层止盈距离。 |
ReentryDistancePoints |
int |
10 |
与最近同向成交之间的最小间距,达到后才会加仓。 |
策略会使用品种的 PriceStep 自动将点数转换为实际价格单位。对于三位或五位小数的报价,会套用与 MetaTrader 相同的倍数,使 1 点 等于 0.0001(或日元品种的 0.01)。
实现细节
- 只需 Level1 数据,无需订阅蜡烛。通过重写
GetWorkingSecurities() 请求 (Security, DataType.Level1) 来声明需求。
- 在
OnStarted 中调用 StartProtection(),确保策略停止时平台会自动平掉剩余仓位。
- 源码中的所有注释都保持为英文,符合仓库规范。
- 由于净持仓限制,移植版本无法同时保留相反方向的真实仓位。当买卖信号同时出现时,后续的市价单会先抵消当前仓位,再建立新的层级。
使用建议
- 合理配置点数间隔。 较小的间隔会生成更密集的网格,在高波动时期可能触发过多交易;较大的间隔虽然减少频率,但单层浮动空间会加大。
- 谨慎设定手数。 网格策略会快速累积仓位,建议先在 Designer/Backtester 中用较小手数测试。
- 配合额外的风控措施。 原始 EA 没有全局止损,应结合账户或组合层面的保护来限制极端风险。
- 关注成交偏差。 策略假定市价单以最佳报价成交,明显的滑点会直接影响实际达到的止盈距离。
来源
移植自 MQL/9027/exp_Amstell.mq4。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Exp Amstell: Grid-style strategy that scales into positions
/// on ATR-based price movements and closes on profit targets.
/// </summary>
public class ExpAmstellStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<int> _emaLength;
private decimal _entryPrice;
private decimal _prevEma;
private int _gridCount;
public ExpAmstellStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period for grid distance.", "Indicators");
_emaLength = Param(nameof(EmaLength), 20)
.SetDisplay("EMA Length", "EMA period for trend.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_prevEma = 0;
_gridCount = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_prevEma = 0;
_gridCount = 0;
var atr = new AverageTrueRange { Length = AtrLength };
var ema = new ExponentialMovingAverage { Length = EmaLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal, decimal emaVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0 || _prevEma == 0)
{
_prevEma = emaVal;
return;
}
var close = candle.ClosePrice;
// Grid exit: take profit at 1.5 ATR or stop at 3 ATR
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 1.5m)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (close <= _entryPrice - atrVal * 3m)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (_gridCount < 3 && close <= _entryPrice - atrVal)
{
// Scale in: add to position on pullback
_entryPrice = (_entryPrice + close) / 2m;
_gridCount++;
BuyMarket();
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 1.5m)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (close >= _entryPrice + atrVal * 3m)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (_gridCount < 3 && close >= _entryPrice + atrVal)
{
_entryPrice = (_entryPrice + close) / 2m;
_gridCount++;
SellMarket();
}
}
// Entry: EMA direction determines initial direction
if (Position == 0)
{
if (close > emaVal && emaVal > _prevEma)
{
_entryPrice = close;
_gridCount = 0;
BuyMarket();
}
else if (close < emaVal && emaVal < _prevEma)
{
_entryPrice = close;
_gridCount = 0;
SellMarket();
}
}
_prevEma = emaVal;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class exp_amstell_strategy(Strategy):
"""
Grid-style strategy that scales into positions on ATR-based price movements.
Enters based on EMA direction, scales in on pullbacks, exits on ATR-based TP/SL.
"""
def __init__(self):
super(exp_amstell_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period for grid distance", "Indicators")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "EMA period for trend", "Indicators")
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(exp_amstell_strategy, self).OnReseted()
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
def OnStarted2(self, time):
super(exp_amstell_strategy, self).OnStarted2(time)
self._entry_price = 0.0
self._prev_ema = 0.0
self._grid_count = 0
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
ema = ExponentialMovingAverage()
ema.Length = self._ema_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_val, ema_val):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr_val)
ema_val = float(ema_val)
if atr_val <= 0 or self._prev_ema == 0:
self._prev_ema = ema_val
return
close = float(candle.ClosePrice)
if self.Position > 0:
if close >= self._entry_price + atr_val * 1.5:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
elif close <= self._entry_price - atr_val * 3.0:
self.SellMarket()
self._entry_price = 0.0
self._grid_count = 0
elif self._grid_count < 3 and close <= self._entry_price - atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.BuyMarket()
elif self.Position < 0:
if close <= self._entry_price - atr_val * 1.5:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
elif close >= self._entry_price + atr_val * 3.0:
self.BuyMarket()
self._entry_price = 0.0
self._grid_count = 0
elif self._grid_count < 3 and close >= self._entry_price + atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.SellMarket()
if self.Position == 0:
if close > ema_val and ema_val > self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.BuyMarket()
elif close < ema_val and ema_val < self._prev_ema:
self._entry_price = close
self._grid_count = 0
self.SellMarket()
self._prev_ema = ema_val
def CreateClone(self):
return exp_amstell_strategy()