在 GitHub 上查看

EuroSurge Simplified 策略

概述

  • 将 MetaTrader 4 智能交易系统 “EuroSurge Simplified” 迁移到 StockSharp 高层 API。
  • 仅处理收盘 K 线,通过 MA、RSI、MACD、布林带与随机指标的组合来筛选信号。
  • 强制执行可配置的交易冷却时间,并以价格步长设置止盈与止损。
  • 提供固定手数、账户余额百分比、账户权益百分比三种仓位管理模式。

信号逻辑

  1. 均线趋势(可选):20 周期快线需高于/低于可配置的慢线。
  2. RSI 滤网(可选):RSI 小于多头阈值允许做多,大于空头阈值允许做空。
  3. MACD 确认(可选):MACD 主线需高于/低于信号线。
  4. 布林带过滤(可选):收盘价向下/向上突破布林带下轨/上轨。
  5. 随机指标过滤(可选):%K 与 %D 同时低于 50(多头)或高于 50(空头)。

所有启用的条件均满足后才会下单。若存在反向仓位,会先平仓再开新仓,与原始 EA 的替换逻辑保持一致。

风险控制

  • 止盈与止损距离以价格步长(MetaTrader 的 “点”)定义。
  • 进场后立即通过 SetStopLossSetTakeProfit 设置保护单。
  • 自上次成交以来未达到设定的分钟间隔前不会重新交易。

仓位管理

  • FixedSize:直接使用 FixedVolume 指定的手数。
  • BalancePercent:按照账户初始余额的百分比估算资金,并用最新收盘价换算手数。
  • EquityPercent:同上,但基于当前权益计算。
  • 最终手数会按交易品种的最小/最大手数限制进行裁剪,并对齐到交易步长。

参数

名称 说明
TradeSizeType 仓位模式(固定、余额百分比、权益百分比)。
FixedVolume 固定仓位模式下的手数。
TradeSizePercent 百分比仓位模式使用的比例。
TakeProfitPoints / StopLossPoints 止盈/止损的价格步长。
MinTradeIntervalMinutes 连续交易之间的冷却时间。
MaPeriod 慢速均线长度(快速均线固定为 20)。
RsiPeriod, RsiBuyLevel, RsiSellLevel RSI 周期与阈值。
MacdFast, MacdSlow, MacdSignal MACD 参数。
BollingerLength, BollingerWidth 布林带周期与宽度。
StochasticLength, StochasticK, StochasticD 随机指标参数。
UseMa, UseRsi, UseMacd, UseBollinger, UseStochastic 独立开关。
CandleType 计算所用的 K 线类型。

与原 EA 的差异

  • 原 EA 在下单前对手数做严格校验,移植版通过最小/最大手数以及步长对齐来复现这一行为。
  • 止盈止损不再手动计算价格,而是借助 StockSharp 的 SetStopLoss/SetTakeProfit 将“点数”转换成价格。
  • 通过 BindEx 接收指标输出,完全避免直接调用 GetValue

使用建议

  1. 绑定好账户与交易品种,并通过 CandleType 设定时间框架。
  2. 根据需求启用或关闭各个指标开关,以还原或精简原策略。
  3. 如需减少交易次数可增大 MinTradeIntervalMinutes,反之则减小。
  4. 确认 TakeProfitPointsStopLossPoints 符合标的的最小跳动。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// High-level port of the "EuroSurge Simplified" MetaTrader strategy.
/// Combines MA trend detection with optional RSI, MACD, Bollinger Bands, and Stochastic filters.
/// Enforces a minimum waiting period between entries and supports several position sizing modes.
/// </summary>
public class EuroSurgeSimplifiedStrategy : Strategy
{
	private readonly StrategyParam<TradeSizeTypes> _tradeSizeType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _tradeSizePercent;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _minTradeIntervalMinutes;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiBuyLevel;
	private readonly StrategyParam<decimal> _rsiSellLevel;
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<int> _bollingerLength;
	private readonly StrategyParam<decimal> _bollingerWidth;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticK;
	private readonly StrategyParam<int> _stochasticD;
	private readonly StrategyParam<bool> _useMa;
	private readonly StrategyParam<bool> _useRsi;
	private readonly StrategyParam<bool> _useMacd;
	private readonly StrategyParam<bool> _useBollinger;
	private readonly StrategyParam<bool> _useStochastic;
	private readonly StrategyParam<DataType> _candleType;

	private DateTimeOffset _lastTradeTime;

	private SimpleMovingAverage _fastMa = null!;
	private SimpleMovingAverage _slowMa = null!;
	private RelativeStrengthIndex _rsi = null!;

	private decimal _fastMaValue;
	private decimal _slowMaValue;
	private decimal _rsiValue;

	/// <summary>
	/// Gets or sets the trade size calculation mode.
	/// </summary>
	public TradeSizeTypes TradeSizeType
	{
		get => _tradeSizeType.Value;
		set => _tradeSizeType.Value = value;
	}

	/// <summary>
	/// Gets or sets the fixed trading volume.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets the percentage used by percent-based position sizing modes.
	/// </summary>
	public decimal TradeSizePercent
	{
		get => _tradeSizePercent.Value;
		set => _tradeSizePercent.Value = value;
	}

	/// <summary>
	/// Gets or sets the take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Gets or sets the stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Gets or sets the minimum delay between consecutive entries in minutes.
	/// </summary>
	public int MinTradeIntervalMinutes
	{
		get => _minTradeIntervalMinutes.Value;
		set => _minTradeIntervalMinutes.Value = value;
	}

	/// <summary>
	/// Gets or sets the longer moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Gets or sets the RSI averaging period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Gets or sets the RSI threshold that enables long trades.
	/// </summary>
	public decimal RsiBuyLevel
	{
		get => _rsiBuyLevel.Value;
		set => _rsiBuyLevel.Value = value;
	}

	/// <summary>
	/// Gets or sets the RSI threshold that enables short trades.
	/// </summary>
	public decimal RsiSellLevel
	{
		get => _rsiSellLevel.Value;
		set => _rsiSellLevel.Value = value;
	}

	/// <summary>
	/// Gets or sets the fast MACD EMA period.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Gets or sets the slow MACD EMA period.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Gets or sets the MACD signal SMA period.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

	/// <summary>
	/// Gets or sets the Bollinger Bands length.
	/// </summary>
	public int BollingerLength
	{
		get => _bollingerLength.Value;
		set => _bollingerLength.Value = value;
	}

	/// <summary>
	/// Gets or sets the Bollinger Bands width measured in deviations.
	/// </summary>
	public decimal BollingerWidth
	{
		get => _bollingerWidth.Value;
		set => _bollingerWidth.Value = value;
	}

	/// <summary>
	/// Gets or sets the Stochastic oscillator smoothing length.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// Gets or sets the Stochastic %K period.
	/// </summary>
	public int StochasticK
	{
		get => _stochasticK.Value;
		set => _stochasticK.Value = value;
	}

	/// <summary>
	/// Gets or sets the Stochastic %D period.
	/// </summary>
	public int StochasticD
	{
		get => _stochasticD.Value;
		set => _stochasticD.Value = value;
	}

	/// <summary>
	/// Gets or sets the flag that enables moving average filtering.
	/// </summary>
	public bool UseMa
	{
		get => _useMa.Value;
		set => _useMa.Value = value;
	}

	/// <summary>
	/// Gets or sets the flag that enables RSI filtering.
	/// </summary>
	public bool UseRsi
	{
		get => _useRsi.Value;
		set => _useRsi.Value = value;
	}

	/// <summary>
	/// Gets or sets the flag that enables MACD filtering.
	/// </summary>
	public bool UseMacd
	{
		get => _useMacd.Value;
		set => _useMacd.Value = value;
	}

	/// <summary>
	/// Gets or sets the flag that enables Bollinger Bands filtering.
	/// </summary>
	public bool UseBollinger
	{
		get => _useBollinger.Value;
		set => _useBollinger.Value = value;
	}

	/// <summary>
	/// Gets or sets the flag that enables Stochastic oscillator filtering.
	/// </summary>
	public bool UseStochastic
	{
		get => _useStochastic.Value;
		set => _useStochastic.Value = value;
	}

	/// <summary>
	/// Gets or sets the candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public EuroSurgeSimplifiedStrategy()
	{
		_tradeSizeType = Param(nameof(TradeSizeType), TradeSizeTypes.FixedSize)
		.SetDisplay("Trade Size Mode", "How trading volume is calculated", "Money Management");

		_fixedVolume = Param(nameof(FixedVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Fixed Volume", "Lot size used when TradeSizeTypes is FixedSize", "Money Management");

		_tradeSizePercent = Param(nameof(TradeSizePercent), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Trade Size %", "Percentage used for balance/equity sizing", "Money Management");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 1400)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit (pts)", "Distance in price steps for take-profit", "Risk Management");

		_stopLossPoints = Param(nameof(StopLossPoints), 900)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (pts)", "Distance in price steps for stop-loss", "Risk Management");

		_minTradeIntervalMinutes = Param(nameof(MinTradeIntervalMinutes), 600)
		.SetNotNegative()
		.SetDisplay("Min Trade Interval", "Minimum minutes between entries", "Execution");

		_maPeriod = Param(nameof(MaPeriod), 52)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Length of the long moving average", "Indicators")
		
		.SetOptimize(30, 150, 10);

		_rsiPeriod = Param(nameof(RsiPeriod), 13)
		.SetGreaterThanZero()
		.SetDisplay("RSI Period", "Length of the RSI filter", "Indicators")
		
		.SetOptimize(5, 30, 1);

		_rsiBuyLevel = Param(nameof(RsiBuyLevel), 50m)
		.SetDisplay("RSI Buy Level", "Maximum RSI value that allows long trades", "Indicators");

		_rsiSellLevel = Param(nameof(RsiSellLevel), 50m)
		.SetDisplay("RSI Sell Level", "Minimum RSI value that allows short trades", "Indicators");

		_macdFast = Param(nameof(MacdFast), 8)
		.SetGreaterThanZero()
		.SetDisplay("MACD Fast", "Fast EMA length", "Indicators");

		_macdSlow = Param(nameof(MacdSlow), 24)
		.SetGreaterThanZero()
		.SetDisplay("MACD Slow", "Slow EMA length", "Indicators");

		_macdSignal = Param(nameof(MacdSignal), 13)
		.SetGreaterThanZero()
		.SetDisplay("MACD Signal", "Signal SMA length", "Indicators");

		_bollingerLength = Param(nameof(BollingerLength), 25)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Length", "Period of Bollinger Bands", "Indicators");

		_bollingerWidth = Param(nameof(BollingerWidth), 2.5m)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators");

		_stochasticLength = Param(nameof(StochasticLength), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Length", "Smoothing length of the oscillator", "Indicators");

		_stochasticK = Param(nameof(StochasticK), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "%K averaging period", "Indicators");

		_stochasticD = Param(nameof(StochasticD), 2)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "%D averaging period", "Indicators");

		_useMa = Param(nameof(UseMa), true)
		.SetDisplay("Use MA", "Enable moving average trend filter", "Filters");

		_useRsi = Param(nameof(UseRsi), true)
		.SetDisplay("Use RSI", "Enable RSI filter", "Filters");

		_useMacd = Param(nameof(UseMacd), true)
		.SetDisplay("Use MACD", "Enable MACD filter", "Filters");

		_useBollinger = Param(nameof(UseBollinger), false)
		.SetDisplay("Use Bollinger", "Enable Bollinger Bands filter", "Filters");

		_useStochastic = Param(nameof(UseStochastic), true)
		.SetDisplay("Use Stochastic", "Enable Stochastic oscillator filter", "Filters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for signal calculations", "Execution");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_lastTradeTime = DateTimeOffset.MinValue;
		_fastMaValue = 0m;
		_slowMaValue = 0m;
		_rsiValue = 0m;
	}

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

		_fastMa = new SimpleMovingAverage { Length = 20 };
		_slowMa = new SimpleMovingAverage { Length = MaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, _slowMa, _rsi, ProcessCandle)
			.Start();

		StartProtection(null, null);
	}

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

		_fastMaValue = fastValue;
		_slowMaValue = slowValue;
		_rsiValue = rsiValue;

		if (!TryBuildSignals(candle, out var isBuySignal, out var isSellSignal))
			return;

		if (!isBuySignal && !isSellSignal)
			return;

		var now = candle.CloseTime;
		var minInterval = TimeSpan.FromMinutes(MinTradeIntervalMinutes);
		if (_lastTradeTime != DateTimeOffset.MinValue && now - _lastTradeTime < minInterval)
			return;

		var volume = CalculateTradeVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		var currentPosition = Position;

		if (isBuySignal && currentPosition <= 0m)
		{
			var orderVolume = volume;
			if (currentPosition < 0m)
				orderVolume += Math.Abs(currentPosition);

			BuyMarket(orderVolume);

			_lastTradeTime = now;
		}
		else if (isSellSignal && currentPosition >= 0m)
		{
			var orderVolume = volume;
			if (currentPosition > 0m)
				orderVolume += Math.Abs(currentPosition);

			SellMarket(orderVolume);

			_lastTradeTime = now;
		}
	}

	private bool TryBuildSignals(ICandleMessage candle, out bool isBuySignal, out bool isSellSignal)
	{
		isBuySignal = false;
		isSellSignal = false;

		if (UseMa && (!_fastMa.IsFormed || !_slowMa.IsFormed))
			return false;

		if (UseRsi && !_rsi.IsFormed)
			return false;

		var fast = _fastMaValue;
		var slow = _slowMaValue;
		var rsi = _rsiValue;

		var maConditionBuy = !UseMa || fast > slow;
		var maConditionSell = !UseMa || fast < slow;

		var rsiConditionBuy = !UseRsi || rsi <= RsiBuyLevel;
		var rsiConditionSell = !UseRsi || rsi >= RsiSellLevel;

		isBuySignal = maConditionBuy && rsiConditionBuy;
		isSellSignal = maConditionSell && rsiConditionSell;

		return true;
	}

	private decimal CalculateTradeVolume(decimal referencePrice)
	{
		var volume = FixedVolume;

		switch (TradeSizeType)
		{
			case TradeSizeTypes.BalancePercent when Portfolio?.BeginValue is decimal balance && balance > 0m && referencePrice > 0m:
			{
				var moneyToUse = balance * TradeSizePercent / 100m;
				var estimatedVolume = moneyToUse / referencePrice;
				if (estimatedVolume > 0m)
					volume = estimatedVolume;
				break;
			}

			case TradeSizeTypes.EquityPercent when Portfolio?.CurrentValue is decimal equity && equity > 0m && referencePrice > 0m:
			{
				var moneyToUse = equity * TradeSizePercent / 100m;
				var estimatedVolume = moneyToUse / referencePrice;
				if (estimatedVolume > 0m)
					volume = estimatedVolume;
				break;
			}
		}

		var minVolume = Security?.MinVolume;
		if (minVolume is decimal min && min > 0m && volume < min)
			volume = min;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume is decimal max && max > 0m && volume > max)
			volume = max;

		var step = Security?.VolumeStep;
		if (step is decimal s && s > 0m)
		{
			var steps = Math.Round(volume / s);
			volume = steps * s;
		}

		return volume > 0m ? volume : 0m;
	}

	public enum TradeSizeTypes
	{
		FixedSize,
		BalancePercent,
		EquityPercent,
	}
}