在 GitHub 上查看

4218 RSI MA 策略

概览

该策略是 MQL/9925 中 MetaTrader 专家的 C# 移植版本。它通过 (High + Low + 2 * Close) / 4 的加权价格计算 EMA 斜率,并将其与传统 RSI 相乘,从而重建 RSI_MA 动量指标。仅使用收盘完成的 K 线参与计算,以保持与原始 EA 一致的信号节奏。

默认配置针对 EURUSD 日线 (D1),任意时刻只允许存在一笔仓位。如果需要,也可以通过调整蜡烛类型与阈值,将策略应用到其他品种与周期。

交易逻辑

  1. 指标计算
    • 使用可配置周期的 RSI 处理收盘价。
    • 使用相同周期的 EMA 处理加权价格。
    • 指标值为 RSI * (EMA(当前) - EMA(前一根)) / 点值,并限制在 [1, 99] 区间。
  2. 做多条件
    • 前一根指标值低于超卖极值(默认 5)。
    • 最新指标值高于多头激活阈值(默认 20)。
    • 当前没有多头仓位;若存在空头仓位会先行平仓再入场做多。
  3. 做空条件
    • 前一根指标值高于超买极值(默认 95)。
    • 最新指标值低于空头激活阈值(默认 80)。
    • 当前没有空头仓位;若存在多头仓位会先行平仓再入场做空。
  4. 指标出场
    • 多头:指标从超买极值回落到激活阈值以下(95 → 80)。
    • 空头:指标从超卖极值回升到激活阈值以上(5 → 20)。
  5. 防护机制
    • 止损、止盈与追踪止损以点数表示,并按照标的的 PriceStep(缺省为 0.0001)换算成价格。
    • 追踪止损仅在浮盈超过设置距离后才会向有利方向推进,永不放宽。

参数说明

参数 说明
RsiPeriod RSI 与 EMA 的周期。
OversoldActivationLevel 超卖极值出现后触发多头的激活阈值。
OversoldExtremeLevel 允许做多前必须达到的超卖极值。
OverboughtActivationLevel 超买极值出现后触发空头的激活阈值。
OverboughtExtremeLevel 允许做空前必须达到的超买极值。
StopLossPips 止损距离(点),可通过 UseStopLoss 启用/禁用。
TakeProfitPips 止盈距离(点),可通过 UseTakeProfit 启用/禁用。
TrailingStopPips 追踪止损距离(点),可通过 UseTrailingStop 启用/禁用。
UseStopLoss 是否启用止损管理。
UseTakeProfit 是否启用止盈管理。
UseTrailingStop 是否启用追踪止损。
UseMoneyManagement 是否使用按风险百分比计算仓位。
RiskPercent 启用资金管理时的单笔风险百分比。
TradeVolume 未启用资金管理时的固定下单量。
CandleType 策略处理的蜡烛类型(默认日线)。

使用建议

  • 若需完全复制原 EA 的行为,请使用 EURUSD 日线数据;更换品种或周期时请同步调整 CandleType 与阈值。
  • 策略始终保持单一仓位,开仓前会自动平掉反向单。
  • 当无法获取组合价值或计算出的仓位小于等于 0 时,会退回到固定 TradeVolume
  • 请确保标的的 PriceStep 与实际点值一致(大多数外汇品种为 0.0001),否则需要重新评估止损/止盈距离。

风险控制

  • 每根完成的 K 线都会根据最高价/最低价判断是否触发止损或止盈。
  • 追踪止损只在盈利状态下收紧,永不反向放宽。
  • 即使关闭防护参数,指标条件仍会执行平仓,保持与 MQL 版本相同的退场逻辑。
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.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// RSI and EMA based strategy converted from the original MQL implementation.
/// Combines a custom RSI*EMA momentum oscillator with basic risk management.
/// </summary>
public class RsiMaStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _oversoldActivationLevel;
	private readonly StrategyParam<decimal> _oversoldExtremeLevel;
	private readonly StrategyParam<decimal> _overboughtActivationLevel;
	private readonly StrategyParam<decimal> _overboughtExtremeLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<bool> _useStopLoss;
	private readonly StrategyParam<bool> _useTakeProfit;
	private readonly StrategyParam<bool> _useTrailingStop;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	
	private RelativeStrengthIndex _rsi;
	private ExponentialMovingAverage _ema;
	
	private decimal? _previousIndicatorValue;
	
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryPrice;
	
	public RsiMaStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetDisplay("RSI Period", string.Empty, "Oscillator")
		;
		
		_oversoldActivationLevel = Param(nameof(OversoldActivationLevel), 40m)
		.SetDisplay("Oversold Activation", string.Empty, "Oscillator")
		;
		
		_oversoldExtremeLevel = Param(nameof(OversoldExtremeLevel), 30m)
		.SetDisplay("Oversold Extreme", string.Empty, "Oscillator");
		
		_overboughtActivationLevel = Param(nameof(OverboughtActivationLevel), 60m)
		.SetDisplay("Overbought Activation", string.Empty, "Oscillator")
		;
		
		_overboughtExtremeLevel = Param(nameof(OverboughtExtremeLevel), 70m)
		.SetDisplay("Overbought Extreme", string.Empty, "Oscillator");
		
		_stopLossPips = Param(nameof(StopLossPips), 399m)
		.SetDisplay("Stop Loss (pips)", string.Empty, "Risk");
		
		_takeProfitPips = Param(nameof(TakeProfitPips), 999m)
		.SetDisplay("Take Profit (pips)", string.Empty, "Risk");
		
		_trailingStopPips = Param(nameof(TrailingStopPips), 299m)
		.SetDisplay("Trailing Stop (pips)", string.Empty, "Risk");
		
		_useStopLoss = Param(nameof(UseStopLoss), true)
		.SetDisplay("Use Stop Loss", string.Empty, "Risk");
		
		_useTakeProfit = Param(nameof(UseTakeProfit), true)
		.SetDisplay("Use Take Profit", string.Empty, "Risk");
		
		_useTrailingStop = Param(nameof(UseTrailingStop), true)
		.SetDisplay("Use Trailing Stop", string.Empty, "Risk");
		
		_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
		.SetDisplay("Use Risk Percent Position Sizing", string.Empty, "Position");
		
		_riskPercent = Param(nameof(RiskPercent), 10m)
		.SetDisplay("Risk Percent", string.Empty, "Position");
		
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
		.SetDisplay("Fixed Volume", string.Empty, "Position");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle TimeFrame", string.Empty, "General");
	}
	
	/// <summary>
	/// RSI calculation period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}
	
	/// <summary>
	/// Activation threshold after an oversold extreme.
	/// </summary>
	public decimal OversoldActivationLevel
	{
		get => _oversoldActivationLevel.Value;
		set => _oversoldActivationLevel.Value = value;
	}
	
	/// <summary>
	/// Oversold extreme required before a long setup becomes valid.
	/// </summary>
	public decimal OversoldExtremeLevel
	{
		get => _oversoldExtremeLevel.Value;
		set => _oversoldExtremeLevel.Value = value;
	}
	
	/// <summary>
	/// Activation threshold after an overbought extreme.
	/// </summary>
	public decimal OverboughtActivationLevel
	{
		get => _overboughtActivationLevel.Value;
		set => _overboughtActivationLevel.Value = value;
	}
	
	/// <summary>
	/// Overbought extreme required before a short setup becomes valid.
	/// </summary>
	public decimal OverboughtExtremeLevel
	{
		get => _overboughtExtremeLevel.Value;
		set => _overboughtExtremeLevel.Value = value;
	}
	
	/// <summary>
	/// Stop-loss distance measured in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}
	
	/// <summary>
	/// Take-profit distance measured in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}
	
	/// <summary>
	/// Trailing-stop distance measured in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}
	
	/// <summary>
	/// Enable or disable stop-loss management.
	/// </summary>
	public bool UseStopLoss
	{
		get => _useStopLoss.Value;
		set => _useStopLoss.Value = value;
	}
	
	/// <summary>
	/// Enable or disable take-profit management.
	/// </summary>
	public bool UseTakeProfit
	{
		get => _useTakeProfit.Value;
		set => _useTakeProfit.Value = value;
	}
	
	/// <summary>
	/// Enable or disable trailing stop adjustments.
	/// </summary>
	public bool UseTrailingStop
	{
		get => _useTrailingStop.Value;
		set => _useTrailingStop.Value = value;
	}
	
	/// <summary>
	/// Enable or disable percent based position sizing.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}
	
	/// <summary>
	/// Portfolio risk percentage when money management is enabled.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}
	
	/// <summary>
	/// Fixed volume used when money management is disabled.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}
	
	/// <summary>
	/// Candle type used for signal generation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}
	
	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_rsi = null;
		_ema = null;
		_previousIndicatorValue = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};
		
		_ema = new ExponentialMovingAverage
		{
			Length = RsiPeriod
		};
		
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();
		
		StartProtection(null, null);
	}
	
	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_rsi.IsFormed)
			return;

		var indicatorValue = rsiValue;

		if (_previousIndicatorValue is decimal previousValue)
		{
			ManageOpenPosition(candle, previousValue, indicatorValue);
			EvaluateEntries(candle, previousValue, indicatorValue);
		}

		_previousIndicatorValue = indicatorValue;
	}
	
	private void ManageOpenPosition(ICandleMessage candle, decimal previousValue, decimal currentValue)
	{
		if (Position > 0)
		{
			var exitSignal = previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel;
			if (exitSignal)
			{
				SellMarket();
				ResetRiskLevels();
				return;
			}

			UpdateTrailingStopForLong(candle);

			if (ShouldCloseLong(candle))
			{
				SellMarket();
				ResetRiskLevels();
			}
		}
		else if (Position < 0)
		{
			var exitSignal = previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel;
			if (exitSignal)
			{
				BuyMarket();
				ResetRiskLevels();
				return;
			}

			UpdateTrailingStopForShort(candle);

			if (ShouldCloseShort(candle))
			{
				BuyMarket();
				ResetRiskLevels();
			}
		}
	}
	
	private void EvaluateEntries(ICandleMessage candle, decimal previousValue, decimal currentValue)
	{
		var price = candle.ClosePrice;
		if (price <= 0m)
			return;
		
		if (previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel && Position <= 0)
		{
			_entryPrice = price;
			InitializeRiskLevelsForLong(price);
			BuyMarket();
		}
		else if (previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel && Position >= 0)
		{
			_entryPrice = price;
			InitializeRiskLevelsForShort(price);
			SellMarket();
		}
	}
	
	private void InitializeRiskLevelsForLong(decimal price)
	{
		var pipDistance = GetPipSize();
		
		if (UseStopLoss && StopLossPips > 0m)
			_stopLossPrice = price - pipDistance * StopLossPips;
		else
			_stopLossPrice = null;
		
		if (UseTakeProfit && TakeProfitPips > 0m)
			_takeProfitPrice = price + pipDistance * TakeProfitPips;
		else
			_takeProfitPrice = null;
	}
	
	private void InitializeRiskLevelsForShort(decimal price)
	{
		var pipDistance = GetPipSize();
		
		if (UseStopLoss && StopLossPips > 0m)
			_stopLossPrice = price + pipDistance * StopLossPips;
		else
			_stopLossPrice = null;
		
		if (UseTakeProfit && TakeProfitPips > 0m)
			_takeProfitPrice = price - pipDistance * TakeProfitPips;
		else
			_takeProfitPrice = null;
	}
	
	private void UpdateTrailingStopForLong(ICandleMessage candle)
	{
		if (!UseTrailingStop || TrailingStopPips <= 0m)
			return;
		
		var pipDistance = GetPipSize() * TrailingStopPips;
		if (pipDistance <= 0m)
			return;
		
		var profit = candle.ClosePrice - _entryPrice;
		if (profit <= pipDistance)
			return;
		
		var newStop = candle.ClosePrice - pipDistance;
		if (_stopLossPrice is null || newStop > _stopLossPrice)
			_stopLossPrice = newStop;
	}
	
	private void UpdateTrailingStopForShort(ICandleMessage candle)
	{
		if (!UseTrailingStop || TrailingStopPips <= 0m)
			return;
		
		var pipDistance = GetPipSize() * TrailingStopPips;
		if (pipDistance <= 0m)
			return;
		
		var profit = _entryPrice - candle.ClosePrice;
		if (profit <= pipDistance)
			return;
		
		var newStop = candle.ClosePrice + pipDistance;
		if (_stopLossPrice is null || newStop < _stopLossPrice)
			_stopLossPrice = newStop;
	}
	
	private bool ShouldCloseLong(ICandleMessage candle)
	{
		var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.LowPrice <= stop;
		var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit;
		return stopHit || takeProfitHit;
	}
	
	private bool ShouldCloseShort(ICandleMessage candle)
	{
		var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.HighPrice >= stop;
		var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit;
		return stopHit || takeProfitHit;
	}
	
	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
	}
	
	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep;
		if (priceStep is null || priceStep == 0m)
			return 0.0001m;
		
		return priceStep.Value;
	}
	
	private decimal GetOrderVolume(decimal price)
	{
		var volume = TradeVolume;

		if (!UseMoneyManagement || Portfolio is null || price <= 0m)
			return volume;

		var portfolioValue = Portfolio.CurrentValue ?? 0m;
		if (portfolioValue <= 0m)
			return volume;

		var riskAmount = portfolioValue * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return volume;

		var estimatedVolume = riskAmount / price;

		var volumeStep = Security?.VolumeStep ?? 0m;
		if (volumeStep > 0m)
		{
			estimatedVolume = Math.Floor(estimatedVolume / volumeStep) * volumeStep;
		}

		if (estimatedVolume <= 0m)
			estimatedVolume = volume;

		return estimatedVolume;
	}
}