在 GitHub 上查看

Revised Self Adaptive EA

MetaTrader 5 专家顾问 revised_self_adaptive_ea.mq5 的 StockSharp 高级 API 版本。

策略说明

  • 形态识别:检测最近两根已收盘 K 线是否形成吞没形态。看涨信号要求当前 K 线实体为阳线、开盘价低于前一根收盘价且前一根为阴线;看跌信号要求相反条件。实体大小与平均实体比较以过滤噪声。
  • 动量过滤:RSI 进入超卖区(看涨)或超买区(看跌)后才允许下单。
  • 趋势过滤:短周期简单移动平均必须与进场方向一致,防止逆势交易。
  • 风控机制:依据 ATR 计算止损、止盈与可选的移动止损,若价格触发保护位则立即市价平仓。
  • 点差与风险限制:当前买卖价差超出阈值或 ATR 止损对应的风险比例超过上限时,信号会被跳过。

主要参数

  • CandleType:分析所用的 K 线周期(默认 1 小时)。
  • AverageBodyPeriod:计算平均实体长度的样本数。
  • MovingAveragePeriod:趋势过滤所用的 SMA 周期。
  • RsiPeriodOversoldLevelOverboughtLevel:RSI 相关设定。
  • AtrPeriodStopLossAtrMultiplierTakeProfitAtrMultiplierTrailingStopAtrMultiplierUseTrailingStop:ATR 风控相关配置。
  • MaxSpreadPoints:允许的最大点差(以最小跳动单位表示)。
  • MaxRiskPercent:基于 ATR 止损计算的最大可接受风险百分比。
  • TradeVolume:下单手数。

与原始 MQL 的差异

  • 原脚本只提供信号检测,本移植补充了移动平均确认以及 ATR 风控模块,以充分利用 MQL 中声明的指标。
  • 在 StockSharp 中未实现箭头绘制,仅保留核心交易逻辑。
  • 新增风险比例过滤,避免 ATR 止损过大的信号。

使用步骤

  1. 在 Hydra 或自定义主机中指定投资组合与标的证券。
  2. 根据交易计划设置参数,特别是 ATR 与点差限制。
  3. 启动策略后系统将自动订阅 K 线、计算指标并在条件满足时发送市价单。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Port of the MetaTrader expert revised_self_adaptive_ea.
/// Detects bullish and bearish engulfing patterns confirmed by RSI and a trend moving average.
/// Applies ATR based risk management with optional trailing stop supervision.
/// </summary>
public class RevisedSelfAdaptiveEaStrategy : Strategy
{
	private readonly StrategyParam<int> _averageBodyPeriod;
	private readonly StrategyParam<int> _movingAveragePeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _volume;
	private readonly StrategyParam<decimal> _maxSpreadPoints;
	private readonly StrategyParam<decimal> _maxRiskPercent;
	private readonly StrategyParam<bool> _useTrailingStop;
	private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
	private readonly StrategyParam<decimal> _takeProfitAtrMultiplier;
	private readonly StrategyParam<decimal> _trailingStopAtrMultiplier;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi = null!;
	private SimpleMovingAverage _movingAverage = null!;
	private AverageTrueRange _atr = null!;
	private SimpleMovingAverage _bodyAverage = null!;

	private ICandleMessage _previousCandle;

	private decimal _lastAtrValue;
	private decimal _averageBodyValue;
	private decimal _pipSize;
	private bool _pipSizeInitialized;

	private decimal? _longEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _longTrailingStopPrice;

	private decimal? _shortEntryPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfitPrice;
	private decimal? _shortTrailingStopPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="RevisedSelfAdaptiveEaStrategy"/>.
	/// </summary>
	public RevisedSelfAdaptiveEaStrategy()
	{
		_averageBodyPeriod = Param(nameof(AverageBodyPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Average body period", "Number of candles used to calculate the average body size filter.", "Pattern")
		
		.SetOptimize(2, 10, 1);

		_movingAveragePeriod = Param(nameof(MovingAveragePeriod), 2)
		.SetGreaterThanZero()
		.SetDisplay("MA period", "Simple moving average period used as a directional filter.", "Trend")
		
		.SetOptimize(2, 30, 1);

		_rsiPeriod = Param(nameof(RsiPeriod), 6)
		.SetGreaterThanZero()
		.SetDisplay("RSI period", "Length of the RSI oscillator applied to candle closes.", "Oscillator")
		
		.SetOptimize(3, 30, 1);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("ATR period", "Average True Range period that controls stop distances.", "Risk")
		
		.SetOptimize(7, 50, 1);

		_volume = Param(nameof(TradeVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Trade volume", "Base position size expressed in lots.", "Trading")
		
		.SetOptimize(0.01m, 1m, 0.01m);

		_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 20m)
		.SetNotNegative()
		.SetDisplay("Max spread", "Maximum allowed spread expressed in points.", "Risk");

		_maxRiskPercent = Param(nameof(MaxRiskPercent), 10m)
		.SetNotNegative()
		.SetDisplay("Max risk percent", "Maximum percentage of the portfolio equity accepted per trade.", "Risk");

		_useTrailingStop = Param(nameof(UseTrailingStop), true)
		.SetDisplay("Use trailing stop", "Enable ATR driven trailing stop supervision.", "Risk");

		_stopLossAtrMultiplier = Param(nameof(StopLossAtrMultiplier), 2m)
		.SetNotNegative()
		.SetDisplay("Stop loss ATR multiplier", "Number of ATRs used to place the protective stop.", "Risk")
		
		.SetOptimize(0.5m, 5m, 0.5m);

		_takeProfitAtrMultiplier = Param(nameof(TakeProfitAtrMultiplier), 4m)
		.SetNotNegative()
		.SetDisplay("Take profit ATR multiplier", "Number of ATRs used to place the profit target.", "Risk")
		
		.SetOptimize(0.5m, 8m, 0.5m);

		_trailingStopAtrMultiplier = Param(nameof(TrailingStopAtrMultiplier), 1.5m)
		.SetNotNegative()
		.SetDisplay("Trailing stop ATR multiplier", "ATR distance maintained by the trailing stop logic.", "Risk")
		
		.SetOptimize(0.5m, 5m, 0.5m);

		_oversoldLevel = Param(nameof(OversoldLevel), 40m)
		.SetNotNegative()
		.SetDisplay("Oversold level", "RSI threshold that confirms bullish reversals.", "Oscillator")
		
		.SetOptimize(10m, 50m, 5m);

		_overboughtLevel = Param(nameof(OverboughtLevel), 60m)
		.SetNotNegative()
		.SetDisplay("Overbought level", "RSI threshold that confirms bearish reversals.", "Oscillator")
		
		.SetOptimize(50m, 90m, 5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle type", "Time frame used for pattern detection.", "General");
	}

/// <summary>
/// Rolling period used to evaluate the average candle body size.
/// </summary>
public int AverageBodyPeriod
{
	get => _averageBodyPeriod.Value;
	set => _averageBodyPeriod.Value = value;
}

/// <summary>
/// Period applied to the simple moving average filter.
/// </summary>
public int MovingAveragePeriod
{
	get => _movingAveragePeriod.Value;
	set => _movingAveragePeriod.Value = value;
}

/// <summary>
/// RSI length used to confirm overbought and oversold conditions.
/// </summary>
public int RsiPeriod
{
	get => _rsiPeriod.Value;
	set => _rsiPeriod.Value = value;
}

/// <summary>
/// Average True Range period that controls risk distances.
/// </summary>
public int AtrPeriod
{
	get => _atrPeriod.Value;
	set => _atrPeriod.Value = value;
}

/// <summary>
/// Default trade volume.
/// </summary>
public decimal TradeVolume
{
	get => _volume.Value;
	set => _volume.Value = value;
}

/// <summary>
/// Maximum allowed spread measured in points.
/// </summary>
public decimal MaxSpreadPoints
{
	get => _maxSpreadPoints.Value;
	set => _maxSpreadPoints.Value = value;
}

/// <summary>
/// Maximum portion of portfolio equity that can be exposed per trade.
/// </summary>
public decimal MaxRiskPercent
{
	get => _maxRiskPercent.Value;
	set => _maxRiskPercent.Value = value;
}

/// <summary>
/// Enables the ATR based trailing stop controller.
/// </summary>
public bool UseTrailingStop
{
	get => _useTrailingStop.Value;
	set => _useTrailingStop.Value = value;
}

/// <summary>
/// ATR multiplier used to position the stop loss.
/// </summary>
public decimal StopLossAtrMultiplier
{
	get => _stopLossAtrMultiplier.Value;
	set => _stopLossAtrMultiplier.Value = value;
}

/// <summary>
/// ATR multiplier used to position the take profit target.
/// </summary>
public decimal TakeProfitAtrMultiplier
{
	get => _takeProfitAtrMultiplier.Value;
	set => _takeProfitAtrMultiplier.Value = value;
}

/// <summary>
/// ATR multiplier that defines the trailing stop distance.
/// </summary>
public decimal TrailingStopAtrMultiplier
{
	get => _trailingStopAtrMultiplier.Value;
	set => _trailingStopAtrMultiplier.Value = value;
}

/// <summary>
/// RSI threshold that validates bullish signals.
/// </summary>
public decimal OversoldLevel
{
	get => _oversoldLevel.Value;
	set => _oversoldLevel.Value = value;
}

/// <summary>
/// RSI threshold that validates bearish signals.
/// </summary>
public decimal OverboughtLevel
{
	get => _overboughtLevel.Value;
	set => _overboughtLevel.Value = value;
}

/// <summary>
/// Candle type consumed by the strategy.
/// </summary>
public DataType CandleType
{
	get => _candleType.Value;
	set => _candleType.Value = value;
}

/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
	return [(Security, CandleType)];
}

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

	_previousCandle = null;
	_lastAtrValue = 0m;
	_averageBodyValue = 0m;
	_pipSize = 0m;
	_pipSizeInitialized = false;
	ResetLongRiskLevels();
	ResetShortRiskLevels();
}

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

	Volume = TradeVolume;

	_rsi = new RelativeStrengthIndex
	{
		Length = RsiPeriod
	};

_movingAverage = new SMA
{
	Length = MovingAveragePeriod
};

_atr = new AverageTrueRange
{
	Length = AtrPeriod
};

_bodyAverage = new SMA
{
	Length = AverageBodyPeriod
};

var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, _movingAverage, _atr, ProcessCandle)
.Start();

StartProtection(null, null);
}

/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
	base.OnOwnTradeReceived(trade);

	var order = trade.Order;
	if (order == null)
	return;

	if (order.Side == Sides.Buy)
	{
		if (Position > 0m)
		{
			// Long exposure established, compute fresh protective levels.
			_longEntryPrice = trade.Trade.Price;
			InitializeLongRiskLevels(trade.Trade.Price);
		}
		else if (Position >= 0m)
		{
			// Short exposure was reduced or closed.
			ResetShortRiskLevels();
		}
	}
	else if (order.Side == Sides.Sell)
	{
		if (Position < 0m)
		{
			// Short exposure established, compute protective levels.
			_shortEntryPrice = trade.Trade.Price;
			InitializeShortRiskLevels(trade.Trade.Price);
		}
		else if (Position <= 0m)
		{
			// Long exposure was reduced or closed.
			ResetLongRiskLevels();
		}
	}
}

/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
	base.OnPositionReceived(position);

	if (Position == 0m)
	{
		// Flat state clears pending protective levels.
		ResetLongRiskLevels();
		ResetShortRiskLevels();
	}
}

private void ProcessCandle(ICandleMessage candle, decimal rsiValue, decimal maValue, decimal atrValue)
{
	if (candle.State != CandleStates.Finished)
	return;

	if (!_pipSizeInitialized)
	InitializePipSize();

	_lastAtrValue = atrValue;
	UpdateAverageBody(candle);
	ManageOpenPositions(candle);

	if (!_rsi.IsFormed || !_movingAverage.IsFormed || !_atr.IsFormed)
	{
		_previousCandle = candle;
		return;
	}

if (_previousCandle == null)
{
	_previousCandle = candle;
	return;
}

if (!IsSpreadWithinLimit())
{
	_previousCandle = candle;
	return;
}

var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
var minimumBody = _averageBodyValue > 0m ? _averageBodyValue : 0m;

var bullishEngulfing = candle.ClosePrice > candle.OpenPrice &&
_previousCandle.ClosePrice < _previousCandle.OpenPrice &&
candle.OpenPrice <= _previousCandle.ClosePrice &&
bodySize >= minimumBody;

var bearishEngulfing = candle.ClosePrice < candle.OpenPrice &&
_previousCandle.ClosePrice > _previousCandle.OpenPrice &&
candle.OpenPrice >= _previousCandle.ClosePrice &&
bodySize >= minimumBody;

if (bullishEngulfing && rsiValue <= OversoldLevel && candle.ClosePrice >= maValue)
{
	TryOpenLong(candle.ClosePrice);
}
else if (bearishEngulfing && rsiValue >= OverboughtLevel && candle.ClosePrice <= maValue)
{
	TryOpenShort(candle.ClosePrice);
}

_previousCandle = candle;
}

private void TryOpenLong(decimal price)
{

	if (Position > 0m)
	return;

	if (ShouldBlockByRisk(price))
	return;

	if (Position < 0m)
	{
		// Close the opposing short exposure before flipping direction.
		BuyMarket(Math.Abs(Position));
		return;
	}

var volume = GetTradeVolume();
if (volume <= 0m)
return;

// Market order is used to replicate the MetaTrader execution style.
BuyMarket(volume);
}

private void TryOpenShort(decimal price)
{
	if (Position < 0m)
	return;

	if (ShouldBlockByRisk(price))
	return;

	if (Position > 0m)
	{
		// Close the opposing long exposure before flipping direction.
		SellMarket(Math.Abs(Position));
		return;
	}

var volume = GetTradeVolume();
if (volume <= 0m)
return;

SellMarket(volume);
}

private void ManageOpenPositions(ICandleMessage candle)
{
	if (Position > 0m)
	{
		// Manage protective logic for long exposure.
		var exitVolume = Position;

		if (_longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
		{
			SellMarket(exitVolume);
			return;
		}

	if (_longTakeProfitPrice.HasValue && candle.HighPrice >= _longTakeProfitPrice.Value)
	{
		SellMarket(exitVolume);
		return;
	}

if (UseTrailingStop && _longTrailingStopPrice.HasValue && _lastAtrValue > 0m)
{
	var candidate = candle.ClosePrice - _lastAtrValue * TrailingStopAtrMultiplier;
	if (candidate > _longTrailingStopPrice.Value)
	_longTrailingStopPrice = candidate;

	if (candle.LowPrice <= _longTrailingStopPrice.Value)
	{
		SellMarket(exitVolume);
		return;
	}
}
}
else if (Position < 0m)
{
	// Manage protective logic for short exposure.
	var exitVolume = Math.Abs(Position);

	if (_shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
	{
		BuyMarket(exitVolume);
		return;
	}

if (_shortTakeProfitPrice.HasValue && candle.LowPrice <= _shortTakeProfitPrice.Value)
{
	BuyMarket(exitVolume);
	return;
}

if (UseTrailingStop && _shortTrailingStopPrice.HasValue && _lastAtrValue > 0m)
{
	var candidate = candle.ClosePrice + _lastAtrValue * TrailingStopAtrMultiplier;
	if (candidate < _shortTrailingStopPrice.Value)
	_shortTrailingStopPrice = candidate;

	if (candle.HighPrice >= _shortTrailingStopPrice.Value)
	{
		BuyMarket(exitVolume);
		return;
	}
}
}
}

private decimal GetTradeVolume()
{
	var volume = TradeVolume;
	var security = Security;
	if (security == null)
	return volume;

	var step = security.VolumeStep;
	if (step != null && step.Value > 0m)
	{
		var steps = Math.Max(1m, Math.Round(volume / step.Value, MidpointRounding.AwayFromZero));
		volume = steps * step.Value;
	}

	var minVolume = security.MinVolume;
	if (minVolume.HasValue && volume < minVolume.Value)
		volume = minVolume.Value;

	var maxVolume = security.MaxVolume;
	if (maxVolume.HasValue && volume > maxVolume.Value)
		volume = maxVolume.Value;

return volume;
}

private bool ShouldBlockByRisk(decimal price)
{
	if (MaxRiskPercent <= 0m)
	return false;

	if (_lastAtrValue <= 0m || StopLossAtrMultiplier <= 0m || price <= 0m)
	return false;

	var potentialLoss = _lastAtrValue * StopLossAtrMultiplier;
	var riskPercent = potentialLoss / price * 100m;
	return riskPercent > MaxRiskPercent;
}

private void UpdateAverageBody(ICandleMessage candle)
{
	var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
	var value = _bodyAverage.Process(new DecimalIndicatorValue(_bodyAverage, body, candle.OpenTime));
	if (value.IsFinal)
		_averageBodyValue = value.GetValue<decimal>();
}

private void InitializePipSize()
{
	_pipSize = GetPipSize();
	_pipSizeInitialized = _pipSize > 0m;
}

private decimal GetPipSize()
{
	var security = Security;
	if (security == null)
	return 0.0001m;

	var step = security.PriceStep;
	if (step != null && step.Value > 0m)
		return step.Value;

	var decimals = security.Decimals;
	if (decimals.HasValue)
	{
		var pow = Math.Pow(10, -decimals.Value);
		return Convert.ToDecimal(pow);
	}

return 0.0001m;
}

private bool IsSpreadWithinLimit()
{
	if (MaxSpreadPoints <= 0m)
	return true;

	var security = Security;
	if (security == null)
	return true;

	var bestBid = GetSecurityValue<decimal?>(Level1Fields.BestBidPrice);
	var bestAsk = GetSecurityValue<decimal?>(Level1Fields.BestAskPrice);
	if (!bestBid.HasValue || !bestAsk.HasValue || !_pipSizeInitialized || _pipSize <= 0m)
	return true;

	var spreadPoints = (bestAsk.Value - bestBid.Value) / _pipSize;
	return spreadPoints <= MaxSpreadPoints;
}

private void InitializeLongRiskLevels(decimal entryPrice)
{
	if (_lastAtrValue <= 0m)
	{
		ResetLongRiskLevels();
		return;
	}

_longStopPrice = StopLossAtrMultiplier > 0m ? entryPrice - _lastAtrValue * StopLossAtrMultiplier : null;
_longTakeProfitPrice = TakeProfitAtrMultiplier > 0m ? entryPrice + _lastAtrValue * TakeProfitAtrMultiplier : null;
_longTrailingStopPrice = UseTrailingStop && TrailingStopAtrMultiplier > 0m ? entryPrice - _lastAtrValue * TrailingStopAtrMultiplier : null;
}

private void InitializeShortRiskLevels(decimal entryPrice)
{
	if (_lastAtrValue <= 0m)
	{
		ResetShortRiskLevels();
		return;
	}

_shortStopPrice = StopLossAtrMultiplier > 0m ? entryPrice + _lastAtrValue * StopLossAtrMultiplier : null;
_shortTakeProfitPrice = TakeProfitAtrMultiplier > 0m ? entryPrice - _lastAtrValue * TakeProfitAtrMultiplier : null;
_shortTrailingStopPrice = UseTrailingStop && TrailingStopAtrMultiplier > 0m ? entryPrice + _lastAtrValue * TrailingStopAtrMultiplier : null;
}

private void ResetLongRiskLevels()
{
	_longEntryPrice = null;
	_longStopPrice = null;
	_longTakeProfitPrice = null;
	_longTrailingStopPrice = null;
}

private void ResetShortRiskLevels()
{
	_shortEntryPrice = null;
	_shortStopPrice = null;
	_shortTakeProfitPrice = null;
	_shortTrailingStopPrice = null;
}
}