GitHub で見る

適格RSIトレーディング戦略

概要

この戦略は、StockSharpの高レベルAPIを使用してMetaTraderエキスパートアドバイザー「Trade on qualified RSI」を再現します。逆張りシステムとして動作します:RSI(相対力指数)の長期的な読みを枯渇として解釈し、数本のローソク足にわたってモメンタムが持続した後、主要な動きに対して反対方向にポジションを開きます。トレーリングストップは価格ステップで管理され、ストップは価格が取引の有利方向に動いたときのみ追従します。

シグナルロジック

インジケーター

  • 設定可能な期間のRSI(デフォルト:28)。
  • 選択されたローソク足購読で計算(デフォルト:15分足)。

ショートエントリー

  1. 最後に閉じたローソク足がRSI上限しきい値以上(デフォルト:55)。
  2. 直前のCountBars本の閉じたローソク足も同じしきい値を上回るRSIを持っていた。内部的に戦略は連続バーをカウントし、カウンターがCountBars + 1に達するとシグナルが発動します。
  3. アクティブなポジションが開いていない。発動すると、戦略は設定されたボリュームで成行売りし、ローソク足の終値をエントリー価格として保存します。

ロングエントリー

  1. 最後に閉じたローソク足がRSI下限しきい値以下(デフォルト:45)。
  2. 直前のCountBars本の閉じたローソク足も同じしきい値を下回るRSIを持っていた(合計CountBars + 1連続読みが必要)。
  3. 開いているポジションが存在しない。発動すると、戦略は設定されたボリュームで成行買いし、エントリー価格を記録します。

ポジション管理

  • 初期ストップ: エントリー直後、ストップ価格はエントリー終値からStopLossPoints価格ステップ離れた位置に配置されます(ロングは下、ショートは上)。価格ステップはSecurity.PriceStepから取得します;証券がそれを定義しない場合、戦略は1にフォールバックします。
  • トレーリング: 完成したローソク足ごとに、ストップは現在の終値方向に締まります。ロングポジションの場合、その値が前のストップを上回るときにストップが終値 - StopLossPoints * PriceStepになります。ショートポジションの場合、その値が前のストップを下回るときにストップが終値 + StopLossPoints * PriceStepになります。
  • 終了: ロング中にローソク足の安値がストップを下回る、またはショート中にローソク足の高値がストップを上回る場合、戦略は市場価格で全ポジションを終了します。追加の利益目標や反転シグナルはありません;新しいエントリーは前のポジションが閉じた後にのみ発生します。

パラメーター

名前 説明 デフォルト値
RsiPeriod RSIインジケーターのルックバック長。 28
UpperThreshold ショートセットアップを適格とするRSIレベル。 55
LowerThreshold ロングセットアップを適格とするRSIレベル。 45
CountBars しきい値を超えて維持する必要のある前のバーの数(合計CountBars + 1連続バー)。 5
StopLossPoints 価格ステップで表現されたストップ距離。実際の価格オフセットはStopLossPoints * PriceStepと同等。 21
TradeVolume 各エントリー注文で送信するボリューム。 1
CandleType インジケーター計算に使用するローソク足購読。 15分足

すべてのパラメーターを最適化できます。しきい値は小数値を許可するため、RSI境界の細かい調整が可能です。

実装ノート

  • 戦略はSubscribeCandles(...).Bind(...)を使用してRSIインジケーターを供給し、ローソク足が完全に形成されたときのみ反応します。
  • RSI値はインジケーターからインデックスで読み返されません;代わりにカウンターがしきい値を尊重する連続した完成ローソク足の数を追跡します。
  • 保護ストップは戦略内部でシミュレートされます。別のストップ注文を配置する代わりに、ストップレベルが交差したときに注文が市場価格で閉じられます。
  • エントリーと終了のログメッセージが生成され、元のエキスパートアドバイザーの詳細な出力を反映します。

使用方法

  1. StockSharpアプリケーションに戦略を追加し、希望する証券とポートフォリオを割り当て、ローソク足シリーズを設定します。
  2. ターゲット銘柄のボラティリティに合わせてRSIしきい値、適格バー数、ストップ距離を調整します。
  3. 戦略を開始します。シグナルがいつ発生し、トレーリングストップがどのように進化するかを確認するためにログを監視します。
  4. 特定の市場のしきい値またはストップ距離のより良い組み合わせを検索するために、組み込みオプティマイザーの実行を検討してください。
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>
/// Contrarian RSI strategy converted from the "Trade on qualified RSI" expert advisor.
/// </summary>
public class TradeOnQualifiedRSIStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _upperThreshold;
	private readonly StrategyParam<decimal> _lowerThreshold;
	private readonly StrategyParam<int> _countBars;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _stopPrice;
	private decimal _entryPrice;
	private int _aboveCounter;
	private int _belowCounter;

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

	/// <summary>
	/// Upper RSI threshold used to qualify short entries.
	/// </summary>
	public decimal UpperThreshold
	{
		get => _upperThreshold.Value;
		set => _upperThreshold.Value = value;
	}

	/// <summary>
	/// Lower RSI threshold used to qualify long entries.
	/// </summary>
	public decimal LowerThreshold
	{
		get => _lowerThreshold.Value;
		set => _lowerThreshold.Value = value;
	}

	/// <summary>
	/// Number of previous RSI bars that must stay beyond the threshold.
	/// </summary>
	public int CountBars
	{
		get => _countBars.Value;
		set => _countBars.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="TradeOnQualifiedRSIStrategy"/>.
	/// </summary>
	public TradeOnQualifiedRSIStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 28)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI calculation.", "RSI")
			
			.SetOptimize(10, 50, 2);

		_upperThreshold = Param(nameof(UpperThreshold), 65m)
			.SetDisplay("Upper Threshold", "RSI level used to qualify short signals.", "RSI")

			.SetOptimize(50m, 70m, 1m);

		_lowerThreshold = Param(nameof(LowerThreshold), 35m)
			.SetDisplay("Lower Threshold", "RSI level used to qualify long signals.", "RSI")

			.SetOptimize(30m, 50m, 1m);

		_countBars = Param(nameof(CountBars), 8)
			.SetGreaterThanZero()
			.SetDisplay("Qualification Bars", "How many previous RSI bars must stay beyond the threshold.", "Signals")

			.SetOptimize(1, 10, 1);

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Stop loss distance expressed in price steps.", "Risk")

			.SetOptimize(5, 50, 5);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume used for entries.", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Source timeframe for RSI calculation.", "General");
	}

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

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

		Volume = TradeVolume;
		_stopPrice = null;
		_entryPrice = 0m;
		_aboveCounter = 0;
		_belowCounter = 0;
	}

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

		Volume = TradeVolume;

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

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

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

		if (_rsi == null || !_rsi.IsFormed)
		{
			_aboveCounter = 0;
			_belowCounter = 0;
			return;
		}

		if (Volume <= 0)
			return;

		var distance = CalculateStopDistance();
		if (distance <= 0)
			return;

		UpdateCounters(rsiValue);

		var requiredBars = CountBars + 1;

		if (Position == 0)
		{
			_stopPrice = null;
			_entryPrice = 0m;

			var shortSignal = rsiValue >= UpperThreshold && _aboveCounter >= requiredBars;
			var longSignal = rsiValue <= LowerThreshold && _belowCounter >= requiredBars;

			if (shortSignal)
			{
				this.LogInfo($"Open short: RSI={rsiValue:F2}, counter={_aboveCounter}");
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = candle.ClosePrice + distance;
				return;
			}

			if (longSignal)
			{
				this.LogInfo($"Open long: RSI={rsiValue:F2}, counter={_belowCounter}");
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_stopPrice = candle.ClosePrice - distance;
			}

			return;
		}

		if (Position > 0)
		{
			if (_stopPrice == null)
				_stopPrice = _entryPrice - distance;

			var newStop = candle.ClosePrice - distance;
			if (_stopPrice == null || newStop > _stopPrice)
				_stopPrice = newStop;

			if (_stopPrice != null && candle.LowPrice <= _stopPrice)
			{
				this.LogInfo($"Exit long via stop at {_stopPrice:F5}");
				SellMarket();
				_stopPrice = null;
				_entryPrice = 0m;
			}

			return;
		}

		if (Position < 0)
		{
			if (_stopPrice == null)
				_stopPrice = _entryPrice + distance;

			var newStop = candle.ClosePrice + distance;
			if (_stopPrice == null || newStop < _stopPrice)
				_stopPrice = newStop;

			if (_stopPrice != null && candle.HighPrice >= _stopPrice)
			{
				this.LogInfo($"Exit short via stop at {_stopPrice:F5}");
				BuyMarket();
				_stopPrice = null;
				_entryPrice = 0m;
			}
		}
	}

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

		return StopLossPoints * step;
	}

	private void UpdateCounters(decimal rsiValue)
	{
		// Track consecutive closes above and below the thresholds.
		if (rsiValue >= UpperThreshold)
		{
			_aboveCounter++;
		}
		else
		{
			_aboveCounter = 0;
		}

		if (rsiValue <= LowerThreshold)
		{
			_belowCounter++;
		}
		else
		{
			_belowCounter = 0;
		}
	}
}