在 GitHub 上查看

MarsiEaStrategy

概览

MarsiEaStrategy 在 StockSharp 高级 API 中复刻了 MetaTrader 上的 MARSIEA 专家顾问。策略以简单移动平均线(SMA)配合相对强弱指标(RSI)判定方向,并且在任意时刻仅持有一笔仓位。止损与止盈均以点(pip)为单位,与原版保持一致;下单手数按账户权益和风险百分比动态计算。

交易逻辑

  1. 数据准备

    • 在所选 K 线序列上计算可配置周期的 SMA。
    • 使用相同的 K 线计算可配置周期的 RSI。
    • K 线类型通过 CandleType 参数设置,默认使用 1 分钟 K 线。
  2. 入场条件

    • 只有在两个指标都完成计算且当前没有持仓时才会评估信号。
    • 做多: 收盘价位于 SMA 之上,同时 RSI 低于超卖阈值。
    • 做空: 收盘价位于 SMA 之下,同时 RSI 高于超买阈值。
    • 为保持与原版一致,策略在任何仓位未平仓时不会再次开仓。
  3. 离场条件

    • 每次开仓后立即登记以点数定义的固定止损和止盈。
    • 不设额外离场规则,保护单会负责平仓。

风险控制与仓位管理

  • RiskPercent 决定每笔交易愿意承担的账户权益百分比。
  • Pip 数值根据 Security.PriceStepSecurity.StepPrice 以及品种的小数位计算,复刻 MQL 中 _Digits 的判断方式。
  • 手数会按照 Security.VolumeStep 四舍五入,并遵守 Security.VolumeMin 指定的最小交易量。
  • 若因缺少品种信息或止损距离为零而无法完成风险计算,策略会退回到 Volume 属性(默认 1 手)。

参数说明

参数 说明
CandleType 指标使用的 K 线序列。
MaPeriod SMA 的计算周期。
RsiPeriod RSI 的计算周期。
RsiOverbought 触发做空的 RSI 超买阈值。
RsiOversold 触发做多的 RSI 超卖阈值。
RiskPercent 每笔交易承担的权益百分比。
StopLossPips 以点数表示的止损距离。
TakeProfitPips 以点数表示的止盈距离。

转换说明

  • 原始 EA 在买卖价上开仓;由于高级 API 不提供逐笔报价,这个移植版本使用 K 线收盘价作为入场参考。
  • Pip 计算遵循原版逻辑:当品种保留 5 或 3 位小数时,pip 等于价格步长的 10 倍。
  • 调用 StartProtection() 以便框架自动为持仓附加止损/止盈订单。
  • 策略完全保留“持仓期间不再开新仓”的原始行为。
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 ported from the MARSIEA MetaTrader expert.
/// Executes a single position at a time with fixed stop-loss and take-profit levels measured in pips.
/// </summary>
public class MarsiEaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;

	private SimpleMovingAverage _sma;
	private RelativeStrengthIndex _rsi;
	private decimal? _virtualStopPrice;
	private decimal? _virtualTakePrice;

	/// <summary>
	/// Candle type used to feed indicators.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

	/// <summary>
	/// Overbought threshold for RSI.
	/// </summary>
	public decimal RsiOverbought
	{
		get => _rsiOverbought.Value;
		set => _rsiOverbought.Value = value;
	}

	/// <summary>
	/// Oversold threshold for RSI.
	/// </summary>
	public decimal RsiOversold
	{
		get => _rsiOversold.Value;
		set => _rsiOversold.Value = value;
	}

	/// <summary>
	/// Risk percentage used to size the entry volume.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MarsiEaStrategy"/> class.
	/// </summary>
	public MarsiEaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Series used for indicator calculations", "General");

		_maPeriod = Param(nameof(MaPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Simple moving average length", "Indicators")
			
			.SetOptimize(5, 50, 1);

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI lookback length", "Indicators")
			
			.SetOptimize(5, 50, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 55m)
			.SetDisplay("RSI Overbought", "Upper RSI threshold", "Signals");

		_rsiOversold = Param(nameof(RsiOversold), 45m)
			.SetDisplay("RSI Oversold", "Lower RSI threshold", "Signals");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Percent", "Equity percentage risked per trade", "Money Management");

		_stopLossPips = Param(nameof(StopLossPips), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 300m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = null;
		_rsi = null;
		_virtualStopPrice = null;
		_virtualTakePrice = null;
	}

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

		_sma = new SimpleMovingAverage { Length = MaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _sma);
			DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}
	}

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

		if (!_sma.IsFormed || !_rsi.IsFormed)
			return;

		// Check virtual SL/TP
		if (Position > 0m)
		{
			if (_virtualStopPrice.HasValue && candle.LowPrice <= _virtualStopPrice.Value)
			{
				SellMarket(Position);
				_virtualStopPrice = null;
				_virtualTakePrice = null;
				return;
			}
			if (_virtualTakePrice.HasValue && candle.HighPrice >= _virtualTakePrice.Value)
			{
				SellMarket(Position);
				_virtualStopPrice = null;
				_virtualTakePrice = null;
				return;
			}
		}
		else if (Position < 0m)
		{
			if (_virtualStopPrice.HasValue && candle.HighPrice >= _virtualStopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				_virtualStopPrice = null;
				_virtualTakePrice = null;
				return;
			}
			if (_virtualTakePrice.HasValue && candle.LowPrice <= _virtualTakePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				_virtualStopPrice = null;
				_virtualTakePrice = null;
				return;
			}
		}

		// Only one position can be active at the same time
		if (Position != 0m)
			return;

		var closePrice = candle.ClosePrice;

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

		var pipSize = GetPipSize();
		if (pipSize <= 0m)
			pipSize = 1m;

		if (closePrice > maValue && rsiValue < RsiOversold)
		{
			BuyMarket(volume);
			_virtualStopPrice = closePrice - StopLossPips * pipSize;
			_virtualTakePrice = closePrice + TakeProfitPips * pipSize;
		}
		else if (closePrice < maValue && rsiValue > RsiOverbought)
		{
			SellMarket(volume);
			_virtualStopPrice = closePrice + StopLossPips * pipSize;
			_virtualTakePrice = closePrice - TakeProfitPips * pipSize;
		}
	}

	private decimal CalculateTradeVolume()
	{
		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = 1m;
		var pipSize = GetPipSize();

		if (RiskPercent <= 0m || portfolioValue <= 0m || priceStep <= 0m || stepPrice <= 0m || pipSize <= 0m)
			return NormalizeVolume(Volume > 0m ? Volume : 1m);

		var riskAmount = portfolioValue * RiskPercent / 100m;
		var perUnitRisk = StopLossPips * pipSize / priceStep * stepPrice;

		if (StopLossPips <= 0m || perUnitRisk <= 0m)
			return NormalizeVolume(Volume > 0m ? Volume : 1m);

		var volume = riskAmount / perUnitRisk;
		return NormalizeVolume(volume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			volume = 1m;

		var volumeStep = Security?.VolumeStep ?? 0m;
		if (volumeStep > 0m)
		{
			var steps = Math.Max(1m, Math.Round(volume / volumeStep, MidpointRounding.AwayFromZero));
			volume = steps * volumeStep;
		}

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

		return volume;
	}

	private decimal CalculatePriceSteps(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;
		var pipSize = GetPipSize();

		if (priceStep <= 0m || pipSize <= 0m)
			return 0m;

		var steps = pips * pipSize / priceStep;
		return steps > 0m ? steps : 0m;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 0m;

		var decimals = Security?.Decimals ?? 0;
		return decimals == 3 || decimals == 5 ? priceStep * 10m : priceStep;
	}
}