在 GitHub 上查看

Two MA RSI 策略

概述

Two MA RSI 策略来自 MetaTrader 专家顾问“2MA_RSI”的移植版本。策略结合了一条快 EMA 和一条慢 EMA 的金叉/死叉,并用 RSI 过滤信号。下单量采用类似马丁格尔的资金管理:每次亏损后会扩大下一笔订单的数量。StockSharp 版本仅在每根 K 线收盘后运行,并按照点数重新计算原策略中的止盈与止损。

数据与指标

  • 只订阅一个由 CandleType 指定的蜡烛序列(默认 5 分钟)。
  • 每根完成的 K 线都会更新三个指标:
    • FastLength 长度的 EMA(使用收盘价)。
    • SlowLength 长度的 EMA。
    • RsiLength 长度的 RSI。
  • 策略内部保存上一根 K 线的 EMA 值,用于检测金叉/死叉,无需访问指标缓冲区。

入场逻辑

  1. 必须在上一根 K 线收盘后评估信号,避免盘中重复触发。
  2. 当前必须没有持仓(Position == 0)。
  3. 做多条件:
    • 快 EMA 从下往上穿越慢 EMA(当前快 EMA > 慢 EMA,上一根快 EMA < 慢 EMA)。
    • RSI 低于 RsiOversold,显示市场超卖。
  4. 做空条件:
    • 快 EMA 从上往下穿越慢 EMA(当前快 EMA < 慢 EMA,上一根快 EMA > 慢 EMA)。
    • RSI 高于 RsiOverbought,显示市场超买。
  5. 满足条件时发送市价单,数量由马丁格尔模块决定。

出场逻辑

  • 入场后立即根据点数计算止损和止盈,点数会乘以标的的 PriceStep 转成价格:
    • 多头:
      • 止损 = 入场价 - StopLossPoints * PriceStep
      • 止盈 = 入场价 + TakeProfitPoints * PriceStep
    • 空头:
      • 止损 = 入场价 + StopLossPoints * PriceStep
      • 止盈 = 入场价 - TakeProfitPoints * PriceStep
  • 只有触发这些保护价位才会平仓。策略在下一根 K 线检查最高价和最低价是否触碰目标,并调用 ClosePosition() 发出市价离场。
  • 如果同一根 K 线同时覆盖止盈和止损区间,会优先判定止损,保持与原有 EA 相同的保守行为。

仓位管理与马丁格尔

  1. 每次入场前计算基础下单量:floor(balance / BalanceDivider) * VolumeStep。余额优先使用投资组合的 CurrentValue,若不可用则使用 BeginValue,并确保不低于一个成交量步长。
  2. 每次亏损后马丁格尔阶段加一,但不超过 MaxDoublings,下一次下单量乘以 2^stage
  3. 任意盈利或达到最大加倍次数都会将阶段归零,恢复基础下单量。
  4. MaxDoublings 小于或等于零时,策略不会放大仓位,始终采用基础下单量。

其他行为

  • 策略内部保存所需的 EMA 值,不使用额外的数据结构。
  • 只有在指标已形成且允许交易 (IsFormedAndOnlineAndAllowTrading) 时才会发送订单。
  • 图表会绘制价格 K 线、自己的成交记录以及三个指标曲线,便于可视化分析。

参数

参数 说明 默认值
FastLength 快速 EMA 的周期。 5
SlowLength 慢速 EMA 的周期。 20
RsiLength RSI 的计算周期。 14
RsiOverbought 允许做空的 RSI 超买阈值。 70
RsiOversold 允许做多的 RSI 超卖阈值。 30
StopLossPoints 以价格步长表示的止损距离。 500
TakeProfitPoints 以价格步长表示的止盈距离。 1500
BalanceDivider 将账户价值除以该系数得到基础下单量。 1000
MaxDoublings 连续亏损后允许的最大加倍次数。 1
CandleType 使用的蜡烛类型。 5 分钟

使用提示

  • 请确保证券的 PriceStepVolumeStep 已设置,否则点数和下单量无法正确换算。
  • 由于采用市价平仓,实际成交仍可能出现滑点,但止损/止盈的逻辑与原 EA 保持一致。
  • 本次仅提供 C# 实现,未创建 Python 版本或对应目录。
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 crossover strategy with RSI confirmation and martingale sizing.
/// </summary>
public class TwoMaRsiStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _balanceDivider;
	private readonly StrategyParam<int> _maxDoublings;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private RelativeStrengthIndex _rsi;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private int _martingaleStage;
	private bool _isClosing;

	/// <summary>
	/// Initializes a new instance of the <see cref="TwoMaRsiStrategy"/> class.
	/// </summary>
	public TwoMaRsiStrategy()
	{
		_fastLength = Param(nameof(FastLength), 5)
			.SetDisplay("Fast EMA Length", "Length of the fast exponential moving average", "Indicators")
			
			.SetOptimize(2, 20, 1);

		_slowLength = Param(nameof(SlowLength), 20)
			.SetDisplay("Slow EMA Length", "Length of the slow exponential moving average", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "Number of bars for the RSI calculation", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 50m)
			.SetDisplay("RSI Overbought", "Upper RSI threshold for short entries", "Signals");

		_rsiOversold = Param(nameof(RsiOversold), 50m)
			.SetDisplay("RSI Oversold", "Lower RSI threshold for long entries", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 1500m)
			.SetDisplay("Take Profit (points)", "Take profit distance in price steps", "Risk");

		_balanceDivider = Param(nameof(BalanceDivider), 1000m)
			.SetDisplay("Balance Divider", "Divides portfolio value to estimate base order volume", "Money Management");

		_maxDoublings = Param(nameof(MaxDoublings), 1)
			.SetDisplay("Max Doublings", "Maximum number of martingale doublings", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series for the strategy", "General");
	}

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// RSI period.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.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>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value to calculate the base order volume.
	/// </summary>
	public decimal BalanceDivider
	{
		get => _balanceDivider.Value;
		set => _balanceDivider.Value = value;
	}

	/// <summary>
	/// Maximum number of martingale doublings.
	/// </summary>
	public int MaxDoublings
	{
		get => _maxDoublings.Value;
		set => _maxDoublings.Value = value;
	}

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

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

		_fastEma = null;
		_slowEma = null;
		_rsi = null;
		_previousFast = null;
		_previousSlow = null;
		_entryPrice = default;
		_stopPrice = default;
		_takeProfitPrice = default;
		_martingaleStage = 0;
		_isClosing = false;
	}

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

		_fastEma = new ExponentialMovingAverage
		{
			Length = FastLength
		};

		_slowEma = new ExponentialMovingAverage
		{
			Length = SlowLength
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

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

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

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

		if (Position == 0 && _isClosing)
		{
			_isClosing = false;
			_entryPrice = default;
			_stopPrice = default;
			_takeProfitPrice = default;
		}

		var fastResult = _fastEma.Process(candle);
		var slowResult = _slowEma.Process(candle);
		var rsiResult = _rsi.Process(candle);

		if (fastResult.IsEmpty || slowResult.IsEmpty || rsiResult.IsEmpty)
		{
			return;
		}

		if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_rsi.IsFormed)
		{
			_previousFast = fastResult.GetValue<decimal>();
			_previousSlow = slowResult.GetValue<decimal>();
			return;
		}

		var fast = fastResult.GetValue<decimal>();
		var slow = slowResult.GetValue<decimal>();
		var rsi = rsiResult.GetValue<decimal>();
		var point = GetPoint();

		if (Position > 0)
		{
			var stopHit = candle.LowPrice <= _stopPrice;
			var takeHit = candle.HighPrice >= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (Position < 0)
		{
			var stopHit = candle.HighPrice >= _stopPrice;
			var takeHit = candle.LowPrice <= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (!_isClosing)
		{
			if (_previousFast is null || _previousSlow is null)
			{
				_previousFast = fast;
				_previousSlow = slow;
				return;
			}

			var prevFast = _previousFast.Value;
			var prevSlow = _previousSlow.Value;

			var crossUp = prevFast < prevSlow && fast > slow && rsi < RsiOversold;
			var crossDown = prevFast > prevSlow && fast < slow && rsi > RsiOverbought;

			if (crossUp)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice - StopLossPoints * point;
					_takeProfitPrice = _entryPrice + TakeProfitPoints * point;
				}
			}
			else if (crossDown)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					SellMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice + StopLossPoints * point;
					_takeProfitPrice = _entryPrice - TakeProfitPoints * point;
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
	}

	private decimal GetPoint()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal CalculateOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var baseVolume = step;
		var divider = BalanceDivider;
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (divider > 0m && balance > 0m)
		{
			var count = Math.Floor((double)(balance / divider));
			baseVolume = (decimal)count * step;
			if (baseVolume < step)
				baseVolume = step;
		}

		var multiplier = CalculateMartingaleMultiplier();
		var volume = baseVolume * multiplier;

		if (volume < step)
			volume = step;

		var ratio = volume / step;
		volume = Math.Ceiling(ratio) * step;

		return volume;
	}

	private decimal CalculateMartingaleMultiplier()
	{
		if (MaxDoublings <= 0 || _martingaleStage <= 0)
			return 1m;

		var stage = Math.Min(_martingaleStage, MaxDoublings);
		return (decimal)Math.Pow(2d, stage);
	}

	private void RegisterWin()
	{
		_martingaleStage = 0;
	}

	private void ClosePosition()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(-Position);
	}

	private void RegisterLoss()
	{
		if (MaxDoublings <= 0)
		{
			_martingaleStage = 0;
			return;
		}

		if (_martingaleStage < MaxDoublings)
		{
			_martingaleStage++;
		}
		else
		{
			_martingaleStage = 0;
		}
	}
}