GitHub で見る

RSI EA クロスオーバー戦略

RSI EA戦略はMetaTrader 5の「RSI EA」エキスパートアドバイザーを再現します。選択されたローソク足シリーズでRSI(相対力指数)を監視し、モメンタムが設定可能な売られ過ぎまたは買われ過ぎのレベルをクロスしたときに反応します。この変換はStockSharpの高レベル戦略APIに適応しながら、オリジナルシステムのストップロス、テイクプロフィット、トレーリングストップ、自動資金管理の考え方を維持しています。

戦略ロジック

インジケーター

  • 選択したローソク足タイプに適用された設定可能な期間のRSI

エントリー条件

  • ロング: RSIがRsiBuyLevel上方クロス(前の値がしきい値以下、現在の値がしきい値超)かつロング取引が有効。
  • ショート: RSIがRsiSellLevel下方クロス(前の値がしきい値超、現在の値がしきい値以下)かつショート取引が有効。

1つのネットポジションのみ維持されます。戦略がすでにマーケットに参入している場合、追加のヘッジポジションは開かれません。

エグジット条件

  • シグナルによる終了: CloseBySignalが有効な場合、反対のRSIクロスオーバーがすぐにアクティブなポジションを閉じます。
  • 保護的なストップ: StopLossがゼロより大きい場合、戦略は平均エントリー価格からの価格距離を監視し、損失が指定した金額に達すると終了します。
  • テイクプロフィット: TakeProfitがゼロより大きい場合、目標距離に達するとすぐにポジションが閉じられます。
  • トレーリングストップ: TrailingStopがゼロより大きい場合、ストップレベルが価格に追従します。ロングポジションでは、価格が現在のストップから少なくともTrailingStop前進すると、ストップがClose - TrailingStopに引き上げられます。ショートは対称的に動作します。

ポジションサイジング

  • UseAutoVolumetrueの場合、ボリュームは口座資本とリスクから計算されます:Volume = Equity * RiskPercent / (100 * stopDistance)、ここでstopDistanceは利用可能な場合StopLossを使用し、そうでない場合TrailingStopを使用します。保護距離が設定されていない場合、戦略は手動ボリュームにフォールバックします。
  • UseAutoVolumefalseの場合、すべての注文に固定のManualVolumeパラメーターが使用されます。

パラメーター

  • CandleType: インジケーター計算に使用されるローソク足シリーズ(デフォルト: 1分足時間軸)。
  • RsiPeriod: RSI計算ウィンドウのバー数(デフォルト: 14)。
  • RsiBuyLevel: ロングエントリーとショート終了をトリガーする売られ過ぎ境界(デフォルト: 30)。
  • RsiSellLevel: ショートエントリーとロング終了をトリガーする買われ過ぎ境界(デフォルト: 70)。
  • EnableLong: ロング取引を有効または無効にする(デフォルト: true)。
  • EnableShort: ショート取引を有効または無効にする(デフォルト: true)。
  • CloseBySignal: RSIが反対のしきい値をクロスしたときにポジションを閉じる(デフォルト: true)。
  • StopLoss: 価格単位でのストップロス距離(デフォルト: 0、無効)。
  • TakeProfit: 価格単位でのテイクプロフィット距離(デフォルト: 0、無効)。
  • TrailingStop: 価格単位でのトレーリングストップ距離(デフォルト: 0、無効)。
  • UseAutoVolume: リスクベースのポジションサイジングをオンにする(デフォルト: true)。
  • RiskPercent: 自動サイジングがアクティブなときにリスクにさらす資本のパーセント(デフォルト: 10)。
  • ManualVolume: 自動サイジングが無効のときの固定注文サイズ(デフォルト: 0.1)。

実装上の注意

  • StockSharpの実装は高レベルワークフローSubscribeCandles(...).Bind(...)を使用し、RSIインジケーターが手動バッファー管理なしで値を戦略に直接配信できるようにしています。
  • 戦略はポジションがゼロに戻ったときにすべての保護レベルをリセットし、古いストップやテイクプロフィット値を避けます。
  • トレーリングロジックはMLQコードを反映しています:ストップは現在のストップレベルを超えてトレーリング距離の2倍以上価格が移動した後にのみ調整され、早期の引き締めを防ぎます。
  • StockSharp戦略はネッティング環境で動作するため、元のヘッジEAのように同時にロングとショートポジションを保持することはできません。代わりに、戦略は反対方向に開く前に現在のポジションが閉じるのを待ちます。
  • 自動サイジングにはStopLossまたはTrailingStopのいずれかが定義されている必要があります。それ以外の場合は、リスク距離が不明なため手動ボリュームが使用されます。

デフォルト設定

  • 時間軸: 1分足ローソク足。
  • RSI: 期間14、レベル30/70。
  • 資金管理: 自動ボリューム有効、10%資本リスク、手動フォールバックボリューム0.1。
  • リスク管理: デフォルトではストップロス、テイクプロフィット、トレーリングストップなし(ライブ取引のために設定が必要)。

使用のヒント

  • 取引しようとするインストゥルメントと時間軸に合わせてCandleTypeを設定してください。戦略はStockSharpのローソク足がサポートする任意のインターバルで動作します。
  • 自動サイジングを有効にする前に現実的なストップロスまたはトレーリングストップ距離を指定し、リスク計算が意味のある値を使用するようにしてください。
  • StartProtection()(コードですでに呼び出されています)と戦略を組み合わせて、フレームワークが予期しない切断や孤立したポジションを管理できるようにしてください。
  • 異なる市場に戦略を適用する際には、最適なしきい値が異なる可能性があるため、実行を監視してRSIレベルを調整してください。
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>
/// Relative Strength Index crossover strategy translated from the MetaTrader RSI EA.
/// </summary>
public class RsiCrossoverEaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiBuyLevel;
	private readonly StrategyParam<decimal> _rsiSellLevel;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<bool> _useAutoVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _manualVolume;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _longStop;
	private decimal? _shortStop;
	private decimal? _longTakeProfit;
	private decimal? _shortTakeProfit;

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

	/// <summary>
	/// Period of the RSI indicator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Oversold level that triggers long entries.
	/// </summary>
	public decimal RsiBuyLevel
	{
		get => _rsiBuyLevel.Value;
		set => _rsiBuyLevel.Value = value;
	}

	/// <summary>
	/// Overbought level that triggers short entries.
	/// </summary>
	public decimal RsiSellLevel
	{
		get => _rsiSellLevel.Value;
		set => _rsiSellLevel.Value = value;
	}

	/// <summary>
	/// Enable or disable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable or disable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Close positions when the RSI crosses the opposite level.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price units.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Automatically size orders by account risk.
	/// </summary>
	public bool UseAutoVolume
	{
		get => _useAutoVolume.Value;
		set => _useAutoVolume.Value = value;
	}

	/// <summary>
	/// Risk percentage applied when auto volume is enabled.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Fixed order volume used when auto sizing is disabled.
	/// </summary>
	public decimal ManualVolume
	{
		get => _manualVolume.Value;
		set => _manualVolume.Value = value;
	}

	/// <summary>
    /// Initializes a new instance of the <see cref="RsiCrossoverEaStrategy"/> class.
    /// </summary>
    public RsiCrossoverEaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for RSI", "General");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Number of bars used for RSI", "RSI")
			
			.SetOptimize(8, 28, 2);

		_rsiBuyLevel = Param(nameof(RsiBuyLevel), 30m)
			.SetRange(0m, 100m)
			.SetDisplay("RSI Buy Level", "Cross above this level opens longs", "RSI")
			
			.SetOptimize(20m, 40m, 5m);

		_rsiSellLevel = Param(nameof(RsiSellLevel), 70m)
			.SetRange(0m, 100m)
			.SetDisplay("RSI Sell Level", "Cross below this level opens shorts", "RSI")
			
			.SetOptimize(60m, 80m, 5m);

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow bullish trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow bearish trades", "Trading");

		_closeBySignal = Param(nameof(CloseBySignal), true)
			.SetDisplay("Close By Signal", "Exit when RSI flips", "Trading");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Stop Loss", "Distance from entry for stop loss", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Take Profit", "Distance from entry for take profit", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_trailingStop = Param(nameof(TrailingStop), 0m)
			.SetRange(0m, 1000m)
			.SetDisplay("Trailing Stop", "Trailing distance after price moves", "Risk")
			
			.SetOptimize(0m, 200m, 20m);

		_useAutoVolume = Param(nameof(UseAutoVolume), true)
			.SetDisplay("Auto Volume", "Size positions by risk percent", "Money Management");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetRange(0m, 100m)
			.SetDisplay("Risk Percent", "Percentage of equity risked per trade", "Money Management");

		_manualVolume = Param(nameof(ManualVolume), 0.1m)
			.SetRange(0.01m, 100m)
			.SetDisplay("Manual Volume", "Fixed volume when auto sizing is off", "Money Management");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousRsi = null;
		_longStop = null;
		_shortStop = null;
		_longTakeProfit = null;
		_shortTakeProfit = null;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

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

		StartProtection(null, null);
	}

	private void Process(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return; // Wait for completed candles only.

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			return; // Indicator still gathering enough data.
		}

		var previous = _previousRsi;
		_previousRsi = rsiValue;

		if (ManageOpenPosition(candle))
			return; // Exit orders were submitted, wait for fills before new decisions.

		var crossAboveBuy = previous.HasValue && previous.Value < RsiBuyLevel && rsiValue > RsiBuyLevel;
		var crossBelowSell = previous.HasValue && previous.Value > RsiSellLevel && rsiValue < RsiSellLevel;

		if (CloseBySignal)
		{
			if (Position > 0 && crossBelowSell)
			{
				SellMarket();
				ResetProtection();
				return; // Close long trades when RSI drops below the sell level.
			}

			if (Position < 0 && crossAboveBuy)
			{
				BuyMarket();
				ResetProtection();
				return; // Close short trades when RSI rises above the buy level.
			}
		}

		if (Position != 0)
			return; // Do not add hedged positions in the netted environment.

		if (EnableShort && crossBelowSell)
		{
			var volume = CalculateVolume();
			if (volume > 0m)
				SellMarket();
			return;
		}

		if (EnableLong && crossAboveBuy)
		{
			var volume = CalculateVolume();
			if (volume > 0m)
				BuyMarket();
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0)
			ResetProtection();
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var entryPrice = candle.ClosePrice;

			if (_longStop is null && StopLoss > 0m)
				_longStop = entryPrice - StopLoss; // Initial protective stop below entry.

			if (_longTakeProfit is null && TakeProfit > 0m)
				_longTakeProfit = entryPrice + TakeProfit; // Profit target above entry.

			if (TrailingStop > 0m && candle.ClosePrice > entryPrice)
			{
				var candidate = candle.ClosePrice - TrailingStop;
				if (!_longStop.HasValue || candle.ClosePrice - 2m * TrailingStop > _longStop.Value)
					_longStop = candidate; // Trail only when price advances enough.
			}

			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetProtection();
				return true;
			}

			if (_longTakeProfit.HasValue && candle.HighPrice >= _longTakeProfit.Value)
			{
				SellMarket();
				ResetProtection();
				return true;
			}
		}
		else if (Position < 0)
		{
			var entryPrice = candle.ClosePrice;

			if (_shortStop is null && StopLoss > 0m)
				_shortStop = entryPrice + StopLoss; // Protective stop above entry.

			if (_shortTakeProfit is null && TakeProfit > 0m)
				_shortTakeProfit = entryPrice - TakeProfit; // Profit target below entry.

			if (TrailingStop > 0m && candle.ClosePrice < entryPrice)
			{
				var candidate = candle.ClosePrice + TrailingStop;
				if (!_shortStop.HasValue || candle.ClosePrice + 2m * TrailingStop < _shortStop.Value)
					_shortStop = candidate; // Trail short stops only after favorable move.
			}

			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetProtection();
				return true;
			}

			if (_shortTakeProfit.HasValue && candle.LowPrice <= _shortTakeProfit.Value)
			{
				BuyMarket();
				ResetProtection();
				return true;
			}
		}
		else
		{
			ResetProtection(); // Ensure cached levels are cleared when flat.
		}

		return false;
	}

	private decimal CalculateVolume()
	{
		if (!UseAutoVolume)
			return ManualVolume; // Use fixed size when auto sizing is disabled.

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return ManualVolume; // Fallback if equity information is unavailable.

		var stopDistance = StopLoss > 0m ? StopLoss : TrailingStop;
		if (stopDistance <= 0m)
			return ManualVolume; // Cannot compute risk-based size without a stop.

		var riskAmount = equity * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return ManualVolume;

		var volume = riskAmount / stopDistance;
		return volume > 0m ? volume : ManualVolume;
	}

	private void ResetProtection()
	{
		_longStop = null;
		_shortStop = null;
		_longTakeProfit = null;
		_shortTakeProfit = null;
	}
}