在 GitHub 上查看

MA + RSI 巫师策略

概述

该策略是 MetaTrader 5 中 MQL/17489 文件夹内 "MQL5 Wizard MA RSI" 专家的 StockSharp 版本。原始 EA 将移动平均线过滤器与 RSI 过滤器结合,通过加权得分跨越阈值时开平仓。C# 改写后的策略保持相同结构,同时利用 StockSharp 的高级 API 与风控工具。

策略适用于任意提供 OHLCV 蜡烛数据的交易品种。它计算一个可按用户设定周期与平移的移动平均线,以及一个可选择价格源的 RSI。两个指标共同构成一个复合得分;当得分超过开仓阈值时建立仓位,当反向得分达到平仓阈值时离场。额外的距离、止损、止盈设置复刻了原 EA 中的资金管理。

指标与得分

  • 移动平均线:周期、算法(简单、指数、平滑、线性加权)、价格源与平移量均可配置。收盘价高于平移后的均线时得分为 100,否则为 0。
  • 相对强弱指标(RSI):周期与价格源可配置。RSI 从 50 上升至 100 的区间内,长方向得分线性增长到 100;当 RSI 低于 50 时,短方向得分同样线性增长。
  • 复合得分:使用 MaWeightRsiWeight 对两个指标得分做加权平均 score = (maScore * MaWeight + rsiScore * RsiWeight) / (MaWeight + RsiWeight),保证结果保持在 0 到 100 之间,与原版保持一致。
  • 价格距离过滤PriceLevelPoints 指定收盘价与平移均线之间的最小距离(按价格步长转换)。距离不足的信号被忽略。

交易规则

  1. 仅在蜡烛收盘时更新指标与得分。
  2. 当反向得分超过 ThresholdClose 时,立即市价平掉现有仓位。
  3. 做多:在当前没有多头敞口的情况下,当多头得分 ≥ ThresholdOpen、冷却期 (ExpirationBars) 已结束并满足距离过滤时,按 Volume + |Position| 的数量下多单,可自动反手空头。
  4. 做空:逻辑与做多对称。
  5. StartProtection 根据点数参数设置止损与止盈。

风险控制

策略启动后立即调用 StartProtectionStopLevelPointsTakeLevelPoints 按价格点数定义,并使用当前品种的 Security.PriceStep 转换为实际价格。设置为 0 可禁用相应保护。ExpirationBars 充当同向再次开仓前的冷却时间,对应原 EA 中挂单过期的概念。

参数

参数 说明 默认值
CandleType 分析使用的蜡烛类型。 15 分钟 K 线
ThresholdOpen 开仓所需的最小加权得分。 55
ThresholdClose 平仓所需的反向得分。 100
PriceLevelPoints 价格与平移均线的最小距离(点)。 0
StopLevelPoints 止损距离(点)。 50
TakeLevelPoints 止盈距离(点)。 50
ExpirationBars 同向再次开仓的冷却周期(根)。 4
MaPeriod 移动平均线周期。 20
MaShift 均线平移的蜡烛数量。 3
MaMethods 均线算法(Simple、Exponential、Smoothed、LinearWeighted)。 Simple
MaAppliedPrice 均线使用的价格。 Close
MaWeight 均线得分的权重。 0.8
RsiPeriod RSI 周期。 3
RsiAppliedPrice RSI 使用的价格。 Close
RsiWeight RSI 得分的权重。 0.5

说明

  • 策略只处理已完成的蜡烛,忽略实时形成中的数据。
  • 当两个权重同时为 0 时,将不会再有信号触发。
  • ExpirationBars 设为 0 时,允许在同一方向上连续进场。
  • 由于 StockSharp 默认使用市价单,原 EA 的挂单过期逻辑在此版本中通过冷却机制体现。
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Moving average plus RSI strategy converted from the MQL5 Wizard template.
/// The strategy computes weighted scores from a shifted moving average and RSI momentum.
/// </summary>
public class MaRsiWizardStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources compatible with the indicators used in the strategy.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _thresholdOpen;
	private readonly StrategyParam<int> _thresholdClose;
	private readonly StrategyParam<decimal> _priceLevelPoints;
	private readonly StrategyParam<int> _stopLevelPoints;
	private readonly StrategyParam<int> _takeLevelPoints;
	private readonly StrategyParam<int> _expirationBars;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MaMethods> _maMethod;
	private readonly StrategyParam<AppliedPrices> _maAppliedPrice;
	private readonly StrategyParam<decimal> _maWeight;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<AppliedPrices> _rsiAppliedPrice;
	private readonly StrategyParam<decimal> _rsiWeight;

	private DecimalLengthIndicator _ma = null!;
	private RelativeStrengthIndex _rsi = null!;
	private readonly Queue<decimal> _maShiftBuffer = new();

	private int _barIndex;
	private int? _lastLongEntryBar;
	private int? _lastShortEntryBar;

	/// <summary>
	/// Initializes a new instance of the <see cref="MaRsiWizardStrategy"/>.
	/// </summary>
	public MaRsiWizardStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for incoming candles", "General");

		_thresholdOpen = Param(nameof(ThresholdOpen), 75)
			.SetRange(0, 100)
			.SetDisplay("Open Threshold", "Weighted score required to open a position", "Signals")
			;

		_thresholdClose = Param(nameof(ThresholdClose), 100)
			.SetRange(0, 100)
			.SetDisplay("Close Threshold", "Weighted score required to exit an existing position", "Signals")
			;

		_priceLevelPoints = Param(nameof(PriceLevelPoints), 0m)
			.SetDisplay("Price Level (points)", "Minimum distance between price and moving average", "Signals")
			;

		_stopLevelPoints = Param(nameof(StopLevelPoints), 50)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
			;

		_takeLevelPoints = Param(nameof(TakeLevelPoints), 50)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
			;

		_expirationBars = Param(nameof(ExpirationBars), 24)
			.SetDisplay("Signal Cooldown (bars)", "Bars to wait before allowing a new trade in the same direction", "Signals")
			;

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average period", "Moving Average")
			;

		_maShift = Param(nameof(MaShift), 3)
			.SetRange(0, 100)
			.SetDisplay("MA Shift", "Lag applied to the moving average output", "Moving Average")
			;

		_maMethod = Param(nameof(MaMethods), MaMethods.Simple)
			.SetDisplay("MA Method", "Moving average calculation method", "Moving Average");

		_maAppliedPrice = Param(nameof(MaAppliedPrice), AppliedPrices.Close)
			.SetDisplay("MA Source", "Price type used for the moving average", "Moving Average");

		_maWeight = Param(nameof(MaWeight), 0.8m)
			.SetDisplay("MA Weight", "Contribution of the moving average score", "Signals")
			.SetRange(0m, 1m)
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "RSI")
			;

		_rsiAppliedPrice = Param(nameof(RsiAppliedPrice), AppliedPrices.Close)
			.SetDisplay("RSI Source", "Price type used for RSI", "RSI");

		_rsiWeight = Param(nameof(RsiWeight), 0.5m)
			.SetDisplay("RSI Weight", "Contribution of the RSI score", "Signals")
			.SetRange(0m, 1m)
			;
	}

	/// <summary>
	/// Type of candles used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Weighted score required to open a new position.
	/// </summary>
	public int ThresholdOpen
	{
		get => _thresholdOpen.Value;
		set => _thresholdOpen.Value = value;
	}

	/// <summary>
	/// Weighted score required to close the current position.
	/// </summary>
	public int ThresholdClose
	{
		get => _thresholdClose.Value;
		set => _thresholdClose.Value = value;
	}

	/// <summary>
	/// Minimum price distance from the moving average expressed in points.
	/// </summary>
	public decimal PriceLevelPoints
	{
		get => _priceLevelPoints.Value;
		set => _priceLevelPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in points.
	/// </summary>
	public int StopLevelPoints
	{
		get => _stopLevelPoints.Value;
		set => _stopLevelPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in points.
	/// </summary>
	public int TakeLevelPoints
	{
		get => _takeLevelPoints.Value;
		set => _takeLevelPoints.Value = value;
	}

	/// <summary>
	/// Cooldown measured in bars before a new trade in the same direction is allowed.
	/// </summary>
	public int ExpirationBars
	{
		get => _expirationBars.Value;
		set => _expirationBars.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Number of bars used to lag the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MaMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Price source used for the moving average.
	/// </summary>
	public AppliedPrices MaAppliedPrice
	{
		get => _maAppliedPrice.Value;
		set => _maAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the moving average score in the weighted decision.
	/// </summary>
	public decimal MaWeight
	{
		get => _maWeight.Value;
		set => _maWeight.Value = value;
	}

	/// <summary>
	/// RSI calculation length.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Price source used for the RSI indicator.
	/// </summary>
	public AppliedPrices RsiAppliedPrice
	{
		get => _rsiAppliedPrice.Value;
		set => _rsiAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the RSI score in the weighted decision.
	/// </summary>
	public decimal RsiWeight
	{
		get => _rsiWeight.Value;
		set => _rsiWeight.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;
	}

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var step = Security.PriceStep ?? 1m;

		Unit takeProfit = TakeLevelPoints > 0
			? new Unit(TakeLevelPoints * step, UnitTypes.Absolute)
			: null;

		Unit stopLoss = StopLevelPoints > 0
			? new Unit(StopLevelPoints * step, UnitTypes.Absolute)
			: null;

		if (stopLoss != null || takeProfit != null)
			StartProtection(stopLoss ?? new Unit(), takeProfit ?? new Unit());

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawIndicator(priceArea, _ma);
			DrawOwnTrades(priceArea);
		}

		var rsiArea = CreateChartArea();
		if (rsiArea != null)
		{
			rsiArea.Title = "RSI";
			DrawIndicator(rsiArea, _rsi);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// removed IsFormedAndOnlineAndAllowTrading for backtesting

		_barIndex++;

		var maInput = SelectAppliedPrice(candle, MaAppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, maInput, candle.OpenTime) { IsFinal = true });
		if (!maValue.IsFinal || maValue is not DecimalIndicatorValue maResult)
			return;

		var rsiInput = SelectAppliedPrice(candle, RsiAppliedPrice);
		var rsiValue = _rsi.Process(new DecimalIndicatorValue(_rsi, rsiInput, candle.OpenTime) { IsFinal = true });
		if (!rsiValue.IsFinal || rsiValue is not DecimalIndicatorValue rsiResult)
			return;

		var referenceMa = UpdateAndGetShiftedMa(maResult.Value);
		if (referenceMa == null)
			return;

		var currentPrice = candle.ClosePrice;
		var step = Security.PriceStep ?? 1m;
		var priceOffset = PriceLevelPoints * step;

		if (PriceLevelPoints > 0 && Math.Abs(currentPrice - referenceMa.Value) < priceOffset)
			return;

		var maLongSignal = currentPrice > referenceMa.Value ? 100m : 0m;
		var maShortSignal = currentPrice < referenceMa.Value ? 100m : 0m;

		var rsi = rsiResult.Value;
		var rsiLongSignal = rsi > 50m ? Math.Min(100m, (rsi - 50m) * 2m) : 0m;
		var rsiShortSignal = rsi < 50m ? Math.Min(100m, (50m - rsi) * 2m) : 0m;

		var weightSum = MaWeight + RsiWeight;
		if (weightSum <= 0m)
			return;

		var longScore = (MaWeight * maLongSignal + RsiWeight * rsiLongSignal) / weightSum;
		var shortScore = (MaWeight * maShortSignal + RsiWeight * rsiShortSignal) / weightSum;

		if (Position > 0 && shortScore >= ThresholdClose)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && longScore >= ThresholdClose)
		{
			BuyMarket(Math.Abs(Position));
		}

		var allowLong = ExpirationBars <= 0 || _lastLongEntryBar == null || _barIndex - _lastLongEntryBar >= ExpirationBars;
		var allowShort = ExpirationBars <= 0 || _lastShortEntryBar == null || _barIndex - _lastShortEntryBar >= ExpirationBars;

		if (Position <= 0 && longScore >= ThresholdOpen && allowLong)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				BuyMarket(volume);
				_lastLongEntryBar = _barIndex;
			}
			return;
		}

		if (Position >= 0 && shortScore >= ThresholdOpen && allowShort)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				SellMarket(volume);
				_lastShortEntryBar = _barIndex;
			}
		}
	}

	private decimal? UpdateAndGetShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		if (shift == 0)
		{
			return maValue;
		}

		_maShiftBuffer.Enqueue(maValue);

		if (_maShiftBuffer.Count <= shift)
			return null;

		if (_maShiftBuffer.Count > shift + 1)
			_maShiftBuffer.Dequeue();

		return _maShiftBuffer.Count == shift + 1 ? _maShiftBuffer.Peek() : (decimal?)null;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SMA { Length = period },
			MaMethods.Exponential => new EMA { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SMA { Length = period }
		};
	}

	private static decimal SelectAppliedPrice(ICandleMessage candle, AppliedPrices price)
	{
		return price switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}