ErrorEA 策略
概述
ErrorEA 是 MetaTrader 专家顾问 errorEA.mq4 的 StockSharp 移植版本。原始 EA 通过比较 Average Directional Index 指标的 +DI 与 -DI 线,在确认趋势方向后不断加仓,同时放置一个非常远的止损与一个很小的剥头皮止盈。C# 版本沿用了同样的思路,使用 StockSharp 的高级 API 实现下单,并在文档中详细说明风险控制规则。
交易逻辑
- 订阅参数
CandleType指定的时间框架,并将蜡烛数据传入AverageDirectionalIndex指标。 - 仅在蜡烛收盘后处理信号,确保 ADX 返回最终值。
- 比较 +DI 与 -DI:
- +DI > -DI 视为多头趋势;
- -DI > +DI 视为空头趋势;
- 数值相等时不生成新信号。
- 多头信号触发时:
- 先平掉已有的空头净头寸(StockSharp 采用净额模式,不允许双向锁仓);
- 若多头加仓次数尚未达到
MaxTrades,按照风险控制计算的手数再买入一笔市价单。
- 空头信号触发时:
- 平掉已有多头净头寸;
- 若空头加仓次数未超过
MaxTrades,按同样的仓位计算规则卖出一笔市价单。
StartProtection负责保护单:StopLossPoints按价格步长转换为止损距离,对应原 EA 中的StopLoss;- 当
EnableTakeProfit为真时,TakeProfitPoints重现了ScalpeProfit的短期止盈逻辑。
_longTrades与_shortTrades计数器在仓位归零或方向反转时重置,保证累积次数不会超过MaxTrades。
风险与仓位管理
BaseVolume等同于原 EA 的MiniLots,定义基础下单手数。EnableRiskControl启用时,会执行原始公式PowerRisk:volume = BaseVolume * max(1, PortfolioValue / RiskDivider),默认除数10000与 MQL 程序保持一致。- 计算出的仓位会被限制在
MinVolume与MaxVolume范围内,并根据交易所参数 (Security.MinVolume、Security.MaxVolume、Security.VolumeStep) 对齐,避免提交无效手数。 - 只要方向未触及
MaxTrades限制,每次加仓都会使用同一风险模型得出的数量。
参数
| 名称 | 类型 | 默认值 | MetaTrader 对应项 | 说明 |
|---|---|---|---|---|
AdxPeriod |
int |
14 |
iADX(..., 14, ...) |
ADX 平滑周期。 |
CandleType |
DataType |
15 分钟 | 图表时间框架 | 用于计算的蜡烛类型。 |
MaxTrades |
int |
9 |
MaxTrades |
同方向允许的最大加仓次数。 |
EnableRiskControl |
bool |
true |
RiskControl |
是否按账户价值动态计算手数。 |
BaseVolume |
decimal |
0.15 |
MiniLots |
风险计算前的基础手数。 |
RiskDivider |
decimal |
10000 |
PowerRisk 中的除数 |
控制风险倍率的分母。 |
MaxVolume |
decimal |
3 |
MaxLot |
自动计算后允许的最大手数。 |
MinVolume |
decimal |
0.01 |
MODE_MINLOT |
市场允许的最小手数。 |
StopLossPoints |
int |
1000 |
StopLoss |
止损距离(价格步数,0 表示禁用)。 |
EnableTakeProfit |
bool |
true |
ScalpeControl |
是否启用剥头皮式止盈。 |
TakeProfitPoints |
int |
10 |
ScalpeProfit |
止盈距离(价格步数)。 |
与原版 EA 的差异
- 原 MQL 代码存在 bug,会把 +DI 的值覆盖成 -DI。本移植修正了该问题,使交易逻辑符合作者意图。
- MetaTrader 支持锁仓,StockSharp 使用净额模式,因此在开仓前会平掉反向持仓。
GetSlippage与Comment输出被移除,因为在 StockSharp 中它们只提供装饰信息,不影响交易。OrderModify的止损/止盈修改由一次StartProtection调用取代,同时考虑了交易所的最小步长与限制。
使用建议
- 确认品种的
PriceStep、VolumeStep、MinVolume、MaxVolume信息完整,以便正确对齐手数。 - 根据交易所规则调整
BaseVolume、MinVolume、MaxVolume。构造函数也会把基础手数写入Strategy.Volume,方便界面上的手工操作。 - 如果 +DI/-DI 信号过于嘈杂,可适当调高
AdxPeriod或选择更长时间框架。 - 假如更倾向于只使用止损离场,可以关闭
EnableTakeProfit。
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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the "errorEA" MetaTrader strategy that compares +DI and -DI lines of ADX.
/// </summary>
public class ErrorEaStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<int> _maxTrades;
private readonly StrategyParam<bool> _enableRiskControl;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _minVolume;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _riskDivider;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<bool> _enableTakeProfit;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private AverageDirectionalIndex _adx;
private int _longTrades;
private int _shortTrades;
/// <summary>
/// ADX averaging period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Maximum number of scale-in entries per direction.
/// </summary>
public int MaxTrades
{
get => _maxTrades.Value;
set => _maxTrades.Value = value;
}
/// <summary>
/// Enables dynamic position sizing based on the portfolio value.
/// </summary>
public bool EnableRiskControl
{
get => _enableRiskControl.Value;
set => _enableRiskControl.Value = value;
}
/// <summary>
/// Maximum order volume allowed by the strategy.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Minimum order volume that should be used.
/// </summary>
public decimal MinVolume
{
get => _minVolume.Value;
set => _minVolume.Value = value;
}
/// <summary>
/// Base volume multiplier that matches the MiniLots parameter from MQL.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Divider applied to the portfolio value when risk control is enabled.
/// </summary>
public decimal RiskDivider
{
get => _riskDivider.Value;
set => _riskDivider.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Enables the scalping take-profit mode from the original EA.
/// </summary>
public bool EnableTakeProfit
{
get => _enableTakeProfit.Value;
set => _enableTakeProfit.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for data subscription.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorEaStrategy"/> class.
/// </summary>
public ErrorEaStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetRange(5, 50)
.SetDisplay("ADX Period", "Smoothing period for the Average Directional Index", "Indicators")
;
_maxTrades = Param(nameof(MaxTrades), 9)
.SetRange(1, 15)
.SetDisplay("Max Trades", "Maximum number of simultaneous entries per direction", "Risk")
;
_enableRiskControl = Param(nameof(EnableRiskControl), true)
.SetDisplay("Enable Risk Control", "Adjust volume by portfolio value similar to the MQL version", "Risk");
_maxVolume = Param(nameof(MaxVolume), 3m)
.SetNotNegative()
.SetDisplay("Max Volume", "Upper limit for market orders", "Risk");
_minVolume = Param(nameof(MinVolume), 0.01m)
.SetNotNegative()
.SetDisplay("Min Volume", "Lower limit for market orders", "Risk");
_baseVolume = Param(nameof(BaseVolume), 0.15m)
.SetNotNegative()
.SetDisplay("Base Volume", "Base lot used before applying risk control", "Risk")
;
_riskDivider = Param(nameof(RiskDivider), 10000m)
.SetNotNegative()
.SetDisplay("Risk Divider", "Portfolio divider used to scale volume when risk control is enabled", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Stop distance converted to price steps", "Protection")
;
_enableTakeProfit = Param(nameof(EnableTakeProfit), true)
.SetDisplay("Enable Take Profit", "Activate the small scalping take profit from the EA", "Protection");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 10)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Take-profit distance converted to price steps", "Protection")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_adx = null;
_longTrades = 0;
_shortTrades = 0;
Volume = BaseVolume;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_adx = new AverageDirectionalIndex { Length = AdxPeriod };
// Subscribe to the configured candle series and calculate ADX on the fly.
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_adx, ProcessCandle)
.Start();
var takeProfitUnit = EnableTakeProfit && TakeProfitPoints > 0
? new Unit(TakeProfitPoints, UnitTypes.Absolute)
: null;
var stopLossUnit = StopLossPoints > 0
? new Unit(StopLossPoints, UnitTypes.Absolute)
: null;
// Mirror the original stop-loss and scalping take-profit distances.
StartProtection(
takeProfit: takeProfitUnit,
stopLoss: stopLossUnit,
useMarketOrders: true);
// Preload the base volume so manual actions in the UI use the same size.
var adjustedVolume = AdjustVolume(BaseVolume);
Volume = adjustedVolume > 0m ? adjustedVolume : BaseVolume;
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
if (_adx != null)
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
{
// Only evaluate completed candles.
if (candle.State != CandleStates.Finished)
return;
// Wait until ADX indicator is formed.
if (!_adx.IsFormed)
return;
// Ensure ADX produced a final value for this bar.
if (adxValue is not AverageDirectionalIndexValue adx || !adxValue.IsFinal)
return;
var plusDi = adx.Dx.Plus ?? 0m;
var minusDi = adx.Dx.Minus ?? 0m;
// Compare +DI and -DI components to determine the signal.
var direction = CalculateDirection(plusDi, minusDi);
switch (direction)
{
case > 0:
HandleLongSignal();
break;
case < 0:
HandleShortSignal();
break;
default:
break;
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
// Reset scaling counters once the net position flips or becomes flat.
if (Position == 0)
{
_longTrades = 0;
_shortTrades = 0;
}
else if (Position > 0)
{
_shortTrades = 0;
}
else
{
_longTrades = 0;
}
}
private int CalculateDirection(decimal plusDi, decimal minusDi)
{
if (plusDi > minusDi)
return 1;
if (minusDi > plusDi)
return -1;
return 0;
}
private void HandleLongSignal()
{
if (Security is null)
return;
// Netting accounts cannot keep opposite positions, so close shorts first.
if (Position < 0)
{
BuyMarket(Math.Abs(Position));
_shortTrades = 0;
}
// Respect the scaling cap inherited from the original EA.
if (_longTrades >= MaxTrades)
return;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return;
// Add one more market order using the calculated lot size.
BuyMarket(volume);
_longTrades++;
}
private void HandleShortSignal()
{
if (Security is null)
return;
// Flat the long exposure before opening new short trades.
if (Position > 0)
{
SellMarket(Math.Abs(Position));
_longTrades = 0;
}
if (_shortTrades >= MaxTrades)
return;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return;
SellMarket(volume);
_shortTrades++;
}
private decimal CalculateOrderVolume()
{
// Start from the base lot size defined by BaseVolume.
var volume = BaseVolume;
if (EnableRiskControl)
{
// Reproduce the PowerRisk logic: balance / divider with a floor of 1.
var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (portfolioValue <= 0m)
portfolioValue = 0m;
var riskFactor = RiskDivider > 0m ? portfolioValue / RiskDivider : 0m;
if (riskFactor < 1m)
riskFactor = 1m;
volume *= riskFactor;
}
// Apply user-defined caps before exchange-specific adjustments.
if (MaxVolume > 0m && volume > MaxVolume)
volume = MaxVolume;
if (MinVolume > 0m && volume < MinVolume)
volume = MinVolume;
// Align with exchange volume constraints.
var adjusted = AdjustVolume(volume);
if (MaxVolume > 0m && adjusted > MaxVolume)
adjusted = MaxVolume;
if (adjusted <= 0m && MinVolume > 0m)
adjusted = MinVolume;
return adjusted;
}
private decimal AdjustVolume(decimal volume)
{
if (Security is null)
return volume;
var step = Security.VolumeStep ?? 0m;
if (step > 0m)
{
// Round the value to the nearest allowed volume step.
var rounded = step * Math.Floor(volume / step);
volume = rounded > 0m ? rounded : step;
}
var minVolume = Security.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = Security.MaxVolume;
if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
volume = maxVolume.Value;
return volume;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class error_ea_strategy(Strategy):
"""
Port of the errorEA MetaTrader strategy that compares +DI and -DI lines of ADX.
Buys when +DI > -DI, sells when -DI > +DI, with scaling and risk controls.
"""
def __init__(self):
super(error_ea_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14) \
.SetDisplay("ADX Period", "Smoothing period for ADX", "Indicators")
self._max_trades = self.Param("MaxTrades", 9) \
.SetDisplay("Max Trades", "Maximum entries per direction", "Risk")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss Points", "Stop distance in price steps", "Protection")
self._take_profit_points = self.Param("TakeProfitPoints", 10) \
.SetDisplay("Take Profit Points", "Take profit in price steps", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._adx = None
self._long_trades = 0
self._short_trades = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(error_ea_strategy, self).OnReseted()
self._adx = None
self._long_trades = 0
self._short_trades = 0
def OnStarted2(self, time):
super(error_ea_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = self._adx_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._adx, self._process_candle).Start()
tp = self._take_profit_points.Value
sl = self._stop_loss_points.Value
tp_unit = Unit(float(tp), UnitTypes.Absolute) if tp > 0 else None
sl_unit = Unit(float(sl), UnitTypes.Absolute) if sl > 0 else None
self.StartProtection(tp_unit, sl_unit, useMarketOrders=True)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._adx)
self.DrawOwnTrades(area)
def _process_candle(self, candle, adx_value):
if candle.State != CandleStates.Finished:
return
if not self._adx.IsFormed:
return
if adx_value.IsEmpty:
return
plus_di = 0.0
minus_di = 0.0
try:
dx = adx_value.Dx
if dx.Plus is not None:
plus_di = float(dx.Plus)
if dx.Minus is not None:
minus_di = float(dx.Minus)
except:
return
if plus_di > minus_di:
self._handle_long_signal()
elif minus_di > plus_di:
self._handle_short_signal()
def OnPositionReceived(self, position):
super(error_ea_strategy, self).OnPositionReceived(position)
if self.Position == 0:
self._long_trades = 0
self._short_trades = 0
elif self.Position > 0:
self._short_trades = 0
else:
self._long_trades = 0
def _handle_long_signal(self):
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self._short_trades = 0
if self._long_trades >= self._max_trades.Value:
return
self.BuyMarket()
self._long_trades += 1
def _handle_short_signal(self):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self._long_trades = 0
if self._short_trades >= self._max_trades.Value:
return
self.SellMarket()
self._short_trades += 1
def CreateClone(self):
return error_ea_strategy()