GitHub で見る

RSI テスト戦略

概要

RsiTestStrategy は、MetaTrader 4 エキスパート アドバイザー RSI_Test を StockSharp の高レベルの API に変換します。この戦略は、高速な RSI モメンタム フィルターと、シンプルなローソク足の確認およびリスクを意識したポジションサイジングを組み合わせたものです。ホスト戦略によって定義された単一の商品を取引し、完成したローソク足のみを使用し、元のコードのティックからクローズまでのロジックを反映しています。

取引ルール

  1. 構成可能な RsiPeriod を使用して相対強度指数を計算します。
  2. RSI が売られすぎ領域 (BuyLevel) から上昇しているとき、かつ 現在のローソク足が前のローソク足よりも上で始まる場合はロングになります。
  3. RSI が買われすぎの領域 (SellLevel) から下落し、かつ 現在のローソク足が前のローソク足よりも下で開くときは空売りします。
  4. MaxOpenPositions の制限を尊重してください。 0 の値は上限を無効にします。それ以外の場合、正味エクスポージャーは MaxOpenPositions * Volume を超えることはできません。
  5. 価格が平均エントリー価格を超えて TrailingDistanceSteps ティック進んだ場合にアクティブになる、階段状のトレーリング ストップを通じて出口を管理します。
  6. 明示的なテイクプロフィットは使用されません。トレーリングストップがトリガーされるか、取引セッションが戦略を終了すると、ポジションは終了します。

ポジションサイジングとリスク

  • この戦略は、ポートフォリオの現在値の RiskPercentage から暫定的な注文サイズを導き出します。商品が証拠金データ (Security.MarginBuy/Security.MarginSell) を提供する場合、ロットごとに必要な資本が尊重されます。それ以外の場合は、保守的なフォールバックとして金額が最新の終値で除算されます。
  • ボリュームは Security.VolumeStep (ステップが不明な場合は小数点以下 2 桁) に四捨五入され、Security.MinVolume/Security.MaxVolume の範囲内に固定されます。
  • 動的サイジングを無効にするには、RiskPercentage をゼロに設定し、常に設定された Volume をトレードします。

トレーリングストップの動作

  • TrailingDistanceSteps は距離を価格ステップで表します (Security.PriceStep)。商品にステップが存在しない場合、距離は直接価格オフセットとして扱われます。
  • 終値またはバー内の高値がアクティブ化レベル(ロングの場合は entry + distance、ショートの場合は entry - distance)を超えると、戦略はエントリー価格を超えた同じオフセットでトレーリングストップを準備します。
  • 保護ストップは、ストップを損益分岐点から最初の階段に移動してそこに保持するオリジナルの EA とまったく同じように、位置ごとに 1 回だけ適用されます。

パラメーター

名前 説明 デフォルト
RsiPeriod RSI のルックバック期間。 14
BuyLevel 長いセットアップを準備する売られすぎのしきい値。 12
SellLevel 短いセットアップを準備する買われすぎのしきい値。 88
RiskPercentage ポジションサイジングに使用されるポートフォリオシェア。 0 を無視するように設定します。 10
TrailingDistanceSteps トレーリングストップを作動させるために必要な距離 (価格ステップ)。 50
MaxOpenPositions 最大同時ポジション。 0 は制限を削除します。 1
CandleType 計算の主な時間枠。 15
Volume リスクサイジングを解決できない場合の基本ボリューム。 1

使用上の注意

  1. MQL の動作と最もよく一致する正確な PriceStepVolumeStep、およびマージンのメタデータを公開する証券に戦略をアタッチします。
  2. このアルゴリズムは完了したローソク足 (CandleStates.Finished) のみをチェックするため、バックテストでは本番と同じタイムフレームを使用する必要があります。
  3. 基本クラスの StartProtection()OnStarted で有効になり、StockSharp の組み込みリスク制御が予期しないポジションの残りを管理できるようになります。
  4. 元のエキスパート アドバイザーは GlobalVariableGet を通じて MetaTrader の最適化を開始したため、その動作は意図的に省略されています。 StockSharp 内でパラメータを直接設定します。
  5. この戦略を、動的なリスクサイジングのために Portfolio.CurrentValue を更新するポートフォリオと組み合わせます。これがなければ、戦略は静的な Volume に正常にフォールバックします。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// RSI-based strategy with volume sizing and stair-like trailing stop.
/// </summary>
public class RsiTestStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _buyLevel;
	private readonly StrategyParam<decimal> _sellLevel;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<int> _trailingDistanceSteps;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _previousOpen;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private bool _trailingArmed;
	private decimal _priceStep;

	/// <summary>
	/// Initialize <see cref="RsiTestStrategy"/>.
	/// </summary>
	public RsiTestStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI", "Indicators")

			.SetOptimize(7, 28, 1);

		_buyLevel = Param(nameof(BuyLevel), 40m)
			.SetDisplay("RSI Buy Level", "Oversold threshold for long entries", "Trading");

		_sellLevel = Param(nameof(SellLevel), 60m)
			.SetDisplay("RSI Sell Level", "Overbought threshold for short entries", "Trading");

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetDisplay("Risk Percentage", "Portfolio percentage used for sizing", "Risk");

		_trailingDistanceSteps = Param(nameof(TrailingDistanceSteps), 50)
			.SetDisplay("Trailing Distance Steps", "Steps before activating trailing stop", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions. 0 disables the limit.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Data");
	}

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public decimal BuyLevel
	{
		get => _buyLevel.Value;
		set => _buyLevel.Value = value;
	}

	public decimal SellLevel
	{
		get => _sellLevel.Value;
		set => _sellLevel.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public int TrailingDistanceSteps
	{
		get => _trailingDistanceSteps.Value;
		set => _trailingDistanceSteps.Value = value;
	}

	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousRsi = null;
		_previousOpen = null;
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
		_priceStep = 0m;
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_priceStep = Security?.PriceStep ?? 0m;

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

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

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		// Only react to fully formed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
		return;

		// Manage trailing logic and exits before attempting fresh entries.
		ManagePosition(candle);

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (_previousRsi is null || _previousOpen is null)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (rsiValue < BuyLevel && Position <= 0)
		{
			TryEnterLong(candle);
		}
		else if (rsiValue > SellLevel && Position >= 0)
		{
			TryEnterShort(candle);
		}

		_previousRsi = rsiValue;
		_previousOpen = candle.OpenPrice;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		// Close short position first if needed
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		// Close long position first if needed
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var avgPrice = _entryPrice;
		if (avgPrice > 0m)
		_entryPrice = avgPrice;

		if (Position > 0)
		{
			UpdateTrailingForLong(candle);
			TryExitLong(candle);
		}
		else if (Position < 0)
		{
			UpdateTrailingForShort(candle);
			TryExitShort(candle);
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value + trailingDistance;
		if (candle.HighPrice < activationPrice)
		return;

		_stopPrice = _entryPrice.Value + trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated long trailing stop at {_stopPrice:0.#####}.");
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value - trailingDistance;
		if (candle.LowPrice > activationPrice)
		return;

		_stopPrice = _entryPrice.Value - trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated short trailing stop at {_stopPrice:0.#####}.");
	}

	private void TryExitLong(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.LowPrice > _stopPrice.Value)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void TryExitShort(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.HighPrice < _stopPrice.Value)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = Volume;

		if (RiskPercentage > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			var riskCapital = portfolioValue * RiskPercentage / 100m;

			if (riskCapital > 0m)
			{
				var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;

				if (margin > 0m)
				{
					volume = riskCapital / margin;
				}
				else if (referencePrice > 0m)
				{
					volume = riskCapital / referencePrice;
				}
			}
		}

		volume = RoundVolume(volume);

		// Ensure volume is at least the base Volume when calculation produces too small a value
		if (volume <= 0m)
			volume = Volume;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
		{
			volume = minVolume.Value;
		}

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
		{
			volume = maxVolume.Value;
		}

		return volume;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
		{
			return 0m;
		}

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
			{
				return step;
			}

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private bool HasCapacityForNewPosition(decimal volume)
	{
		if (MaxOpenPositions <= 0)
		{
			return true;
		}

		if (volume <= 0m)
		{
			return false;
		}

		var exposure = Math.Abs(Position);
		var maxExposure = MaxOpenPositions * volume;

		return exposure + volume <= maxExposure + volume * 0.0001m;
	}

	private decimal GetPriceOffset(int steps)
	{
		if (steps <= 0)
		{
			return 0m;
		}

		if (_priceStep > 0m)
		{
			return steps * _priceStep;
		}

		return steps;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
	}
}