GitHub で見る

RM Stochastic バンド戦略

概要

RM Stochastic バンド戦略 は、Ronny Maheza による MetaTrader エキスパート アドバイザー EA RM Stochastic バンド の上位レベルの StockSharp 移植です。この戦略は、異なる時間枠 (基準、中間、高値) で計算された 3 つの確率オシレーターを観察し、3 つすべてが売られすぎまたは買われすぎの状態を確認した場合にのみ取引を開始します。エントリー時、エグジットレベルは、より高い時間枠で測定された平均トゥルーレンジ (ATR) から導出され、元のエキスパートアドバイザーの ATR ベースのストップロスおよびテイクプロフィットレベルを再現します。追加の実行フィルターには、マージンプロキシとして構成可能な最小ポートフォリオ値と、観察されたスプレッドに応じて許容範囲を調整するスプレッド制御が含まれます。

中核ロジック

  1. マルチタイムフレームの確率的確認

    • プライマリ実行タイムフレーム (デフォルトは M1) が取引シグナルを生成します。
    • 確認タイムフレーム (デフォルトの M5 および M15) は信号の方向と一致する必要があります。
    • 取引は、3 つのタイムフレームすべての確率的 %K 値が同時に売られすぎレベルを下回るか (ロングセットアップ)、買われすぎレベルを上回る場合 (ショートセットアップ) にのみ開始されます。
  2. ボラティリティに基づくエグジット(ATR)

    • ATR は最も高い時間枠 (デフォルトは M15) で計算されます。
    • ストップロス = entry price ± ATR * StopLossMultiplier
    • テイクプロフィット = entry price ± ATR * TakeProfitMultiplier
    • 価格は基本タイムフレームのローソク足で監視されます。ローソク足がいずれかのレベルに触れた場合、ポジションは市場でクローズされます。
  3. 実行フィルターと安全フィルター

    • 観測されたスプレッド (BestAsk - BestBid) が適応しきい値を超える場合、注文はスキップされます。スプレッドが標準制限よりも高い場合は、ソース EA ロジックを反映して、より緩やかなセントアカウント制限が適用されます。
    • ポートフォリオの値が MinMargin を下回っている間、取引はブロックされます。
    • 一度にオープンできるポジションは 1 つだけであり、アクティブな注文が存在する場合、新しい取引は開始されません。

インジケーターとサブスクリプション

インジケーター 時間枠 目的
Stochastic オシレーター 基本時間枠 (デフォルトは 1 分) プライマリ信号を生成します (%K のみが使用されます)。
Stochastic オシレーター 中間の時間枠 (デフォルトは 5 分) 主信号の方向を確認します。
Stochastic オシレーター 長い時間枠 (デフォルトは 15 分) 長期的な確認を提供します。
平均真の範囲 長い時間枠 (デフォルトは 15 分) ボラティリティを調整したストップロスとテイクプロフィットの距離を定義します。

レベル 1 データは、最良の入札を取得し、スプレッド評価を求めるためにサブスクライブされます。

エントリールール

  • 長いセットアップ: 3 つの確率的 %K 値はすべて OversoldLevel 未満です。トリガーされると、この戦略は市場出来高 OrderVolume で購入し、ATR ベースの決済レベルを保存します。
  • 簡単なセットアップ: 3 つの確率的 %K 値はすべて OverboughtLevel を超えています。成行売りは同量の取り扱いで実行されます。

終了ルール

  • ストップロス: ロングポジションの場合、ローソク足の安値が entry - ATR * StopLossMultiplier に触れたときに終了します。ショートポジションの場合は、ローソク足の高値が entry + ATR * StopLossMultiplier に達したら手仕舞いします。
  • 利益確定: ロングポジションの場合、ローソク足の高値が entry + ATR * TakeProfitMultiplier に触れたときに決済します。ショートポジションの場合は、ローソク足の安値が entry - ATR * TakeProfitMultiplier に達したら手仕舞いします。
  • 終了後、次の信号で新しいレベルを再計算できるように、内部ストップとターゲットのプレースホルダーがクリアされます。

パラメーター

パラメータ 説明 デフォルト
OrderVolume 各成行注文のボリューム。 0.1
StochasticLength %K ルックバック期間。 5
StochasticSmoothing %K に適用される平滑化。 3
StochasticSignalLength %D 長さ。 3
AtrPeriod ハイタイムフレームの ATR 期間。 14
StopLossMultiplier ATR ストップロスの乗数。 1.5
TakeProfitMultiplier テイクプロフィットの乗数は ATR です。 3.0
MinMargin 取引に必要な最小ポートフォリオ値。 100
MaxSpreadStandard スタンダード口座のスプレッド上限。 3
MaxSpreadCent 現在のスプレッドがすでに標準の上限を超えている場合に使用されるスプレッドの上限。 10
OversoldLevel 確率的 %K の売られすぎのしきい値。 20
OverboughtLevel 確率的 %K の買われすぎのしきい値。 80
BaseCandleType プライマリ タイムフレーム (デフォルトの 1 分ローソク足)。 1分
MidCandleType 確認時間枠 (デフォルトの 5 分ローソク足)。 5分
HighCandleType 確認 + ATR のタイムフレーム (デフォルトの 15 分ローソク足)。 15分

すべてのパラメータは、必要に応じて MetaTrader 入力と同じ最適化範囲をサポートします。

実装メモ

  • この戦略では、プロジェクト ガイドラインで義務付けられているように、SubscribeCandles(...).BindEx(...) を使用して、高レベルの API を通じて厳密にインジケーター値を取得します。
  • スプレッドは、ライブのレベル 1 更新から計算されます。買値/売値データがない場合、取引は無効なままとなり、相場を提供しないデータフィードでの安全な操作が保証されます。
  • ポジションは純粋に成行注文を通じて管理され、事前に計算されたストップロスとテイクプロフィットのレベルによる市場エントリーに依存していたオリジナルの EA を反映しています。
  • 関連する入力パラメータがあるにもかかわらず、MQL ソースはこれらの機能を実装していないため、損益分岐点ロジックやトレーリング ロジックはありません。

使用のヒント

  1. 戦略を目的の証券に付加し、レベル 1 (買い/売り) データが適切なスプレッド フィルタリングに利用できることを確認します。
  2. 確率的しきい値と ATR 乗数を調整して、ターゲット商品のボラティリティ プロファイルに一致させます。
  3. 最適化する際、取引する市場に元の M1/M5/M15 構造とは異なる支配的なサイクルがある場合は、異なる時間枠の組み合わせをテストすることを検討してください。
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>
/// Multi-timeframe stochastic oscillator strategy with ATR-based stop-loss and take-profit management.
/// Emulates the logic of the "EA RM Stochastic Band" MetaTrader expert advisor.
/// </summary>
public class RmStochasticBandStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticSmoothing;
	private readonly StrategyParam<int> _stochasticSignalLength;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _stopLossMultiplier;
	private readonly StrategyParam<decimal> _takeProfitMultiplier;
	private readonly StrategyParam<decimal> _minMargin;
	private readonly StrategyParam<decimal> _maxSpreadStandard;
	private readonly StrategyParam<decimal> _maxSpreadCent;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<DataType> _baseCandleType;
	private readonly StrategyParam<DataType> _midCandleType;
	private readonly StrategyParam<DataType> _highCandleType;

	private decimal? _stochM1;
	private decimal? _stochM5;
	private decimal? _stochM15;
	private decimal? _atrValue;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfit;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfit;
	private decimal? _bestBid;
	private decimal? _bestAsk;

	/// <summary>
	/// Trade volume used for market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// %K lookback for the stochastic oscillator.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// Smoothing period applied to %K.
	/// </summary>
	public int StochasticSmoothing
	{
		get => _stochasticSmoothing.Value;
		set => _stochasticSmoothing.Value = value;
	}

	/// <summary>
	/// %D moving average length.
	/// </summary>
	public int StochasticSignalLength
	{
		get => _stochasticSignalLength.Value;
		set => _stochasticSignalLength.Value = value;
	}

	/// <summary>
	/// ATR lookback used for volatility-based exits.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Multiplier applied to ATR for stop-loss calculation.
	/// </summary>
	public decimal StopLossMultiplier
	{
		get => _stopLossMultiplier.Value;
		set => _stopLossMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to ATR for take-profit calculation.
	/// </summary>
	public decimal TakeProfitMultiplier
	{
		get => _takeProfitMultiplier.Value;
		set => _takeProfitMultiplier.Value = value;
	}

	/// <summary>
	/// Minimum portfolio value required before placing trades.
	/// </summary>
	public decimal MinMargin
	{
		get => _minMargin.Value;
		set => _minMargin.Value = value;
	}

	/// <summary>
	/// Maximum spread (in price units) tolerated on standard accounts.
	/// </summary>
	public decimal MaxSpreadStandard
	{
		get => _maxSpreadStandard.Value;
		set => _maxSpreadStandard.Value = value;
	}

	/// <summary>
	/// Maximum spread (in price units) tolerated on cent accounts.
	/// </summary>
	public decimal MaxSpreadCent
	{
		get => _maxSpreadCent.Value;
		set => _maxSpreadCent.Value = value;
	}

	/// <summary>
	/// Threshold for oversold conditions.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// Threshold for overbought conditions.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Primary timeframe used for signal execution.
	/// </summary>
	public DataType BaseCandleType
	{
		get => _baseCandleType.Value;
		set => _baseCandleType.Value = value;
	}

	/// <summary>
	/// Intermediate timeframe used for stochastic confirmation.
	/// </summary>
	public DataType MidCandleType
	{
		get => _midCandleType.Value;
		set => _midCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used for stochastic confirmation and ATR calculation.
	/// </summary>
	public DataType HighCandleType
	{
		get => _highCandleType.Value;
		set => _highCandleType.Value = value;
	}

	public RmStochasticBandStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume of each market order", "Trading");

		_stochasticLength = Param(nameof(StochasticLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Length", "%K lookback period", "Indicators")
			
			.SetOptimize(3, 15, 1);

		_stochasticSmoothing = Param(nameof(StochasticSmoothing), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smoothing", "Smoothing period applied to %K", "Indicators")
			
			.SetOptimize(1, 7, 1);

		_stochasticSignalLength = Param(nameof(StochasticSignalLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Signal", "%D moving average length", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Lookback for ATR volatility filter", "Indicators")
			
			.SetOptimize(7, 30, 1);

		_stopLossMultiplier = Param(nameof(StopLossMultiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL Multiplier", "ATR multiplier for stop-loss", "Risk")
			
			.SetOptimize(0.5m, 3m, 0.25m);

		_takeProfitMultiplier = Param(nameof(TakeProfitMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("TP Multiplier", "ATR multiplier for take-profit", "Risk")
			
			.SetOptimize(1m, 6m, 0.5m);

		_minMargin = Param(nameof(MinMargin), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Margin", "Required portfolio value before trading", "Risk");

		_maxSpreadStandard = Param(nameof(MaxSpreadStandard), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Max Spread Standard", "Maximum spread allowed for standard accounts", "Filters");

		_maxSpreadCent = Param(nameof(MaxSpreadCent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Max Spread Cent", "Maximum spread allowed for cent accounts", "Filters");

		_oversoldLevel = Param(nameof(OversoldLevel), 20m)
			.SetDisplay("Oversold Level", "Threshold that defines oversold conditions", "Signals")
			
			.SetOptimize(5m, 40m, 5m);

		_overboughtLevel = Param(nameof(OverboughtLevel), 80m)
			.SetDisplay("Overbought Level", "Threshold that defines overbought conditions", "Signals")
			
			.SetOptimize(60m, 95m, 5m);

		_baseCandleType = Param(nameof(BaseCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Base Timeframe", "Primary execution timeframe", "General");

		_midCandleType = Param(nameof(MidCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Mid Timeframe", "Secondary confirmation timeframe", "General");

		_highCandleType = Param(nameof(HighCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("High Timeframe", "Higher confirmation timeframe", "General");
	}

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

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

		_stochM1 = null;
		_stochM5 = null;
		_stochM15 = null;
		_atrValue = null;
		_longStopPrice = null;
		_longTakeProfit = null;
		_shortStopPrice = null;
		_shortTakeProfit = null;
		_bestBid = null;
		_bestAsk = null;
	}

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

		var baseStochastic = CreateStochastic();
		var midStochastic = CreateStochastic();
		var highStochastic = CreateStochastic();
		var atr = new AverageTrueRange { Length = AtrPeriod };

		var baseSubscription = SubscribeCandles(BaseCandleType);
		baseSubscription.BindEx(baseStochastic, ProcessBaseCandle).Start();

		SubscribeCandles(MidCandleType)
			.BindEx(midStochastic, ProcessMidCandle)
			.Start();

		SubscribeCandles(HighCandleType)
			.BindEx(highStochastic, atr, ProcessHighCandle)
			.Start();

		// Level1 removed for backtest compatibility
	}

	private StochasticOscillator CreateStochastic()
	{
		return new StochasticOscillator
		{
			K = { Length = StochasticLength },
			D = { Length = StochasticSignalLength }
		};
	}

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bidValue))
			_bestBid = (decimal)bidValue;

		if (message.Changes.TryGetValue(Level1Fields.BestAskPrice, out var askValue))
			_bestAsk = (decimal)askValue;
	}

	private void ProcessMidCandle(ICandleMessage candle, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;

		if (stochastic.K is decimal kValue)
			_stochM5 = kValue;
	}

	private void ProcessHighCandle(ICandleMessage candle, IIndicatorValue stochValue, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal || !atrValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;

		if (stochastic.K is decimal kValue)
			_stochM15 = kValue;

		_atrValue = atrValue.ToDecimal();
	}

	private void ProcessBaseCandle(ICandleMessage candle, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;
		if (stochastic.K is not decimal kValue)
			return;

		_stochM1 = kValue;

		ManageOpenPosition(candle);
		TryEnterPosition(candle);
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			_longStopPrice = null;
			_longTakeProfit = null;
			_shortStopPrice = null;
			_shortTakeProfit = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				_longStopPrice = null;
				_longTakeProfit = null;
				return;
			}

			if (_longTakeProfit is decimal target && candle.HighPrice >= target)
			{
				SellMarket(Position);
				_longStopPrice = null;
				_longTakeProfit = null;
			}
		}
		else if (Position < 0)
		{
			var shortVolume = Math.Abs(Position);
			if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(shortVolume);
				_shortStopPrice = null;
				_shortTakeProfit = null;
				return;
			}

			if (_shortTakeProfit is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(shortVolume);
				_shortStopPrice = null;
				_shortTakeProfit = null;
			}
		}
	}

	private void TryEnterPosition(ICandleMessage candle)
	{
		if (!HasSufficientMargin())
			return;

		if (Position != 0)
			return;

		if (HasActiveOrders())
			return;

		if (_stochM1 is not decimal stochFast ||
			_stochM5 is not decimal stochMid ||
			_stochM15 is not decimal stochSlow ||
			_atrValue is not decimal atr)
		{
			return;
		}

		var oversold = OversoldLevel;
		var overbought = OverboughtLevel;

		if (stochFast < oversold && stochMid < oversold && stochSlow < oversold)
		{
			EnterLong(candle.ClosePrice, atr);
		}
		else if (stochFast > overbought && stochMid > overbought && stochSlow > overbought)
		{
			EnterShort(candle.ClosePrice, atr);
		}
	}

	private bool HasSufficientMargin()
	{
		var currentValue = Portfolio?.CurrentValue ?? 0m;
		return currentValue >= MinMargin;
	}

	private bool IsSpreadAcceptable()
	{
		if (_bestBid is not decimal bid || _bestAsk is not decimal ask)
			return false;

		var spread = ask - bid;
		if (spread <= 0m)
			return true;

		var limit = spread > MaxSpreadStandard ? MaxSpreadCent : MaxSpreadStandard;
		return spread <= limit;
	}

	private bool HasActiveOrders()
	{
		foreach (var order in Orders)
		{
			if (!order.State.IsFinal())
				return true;
		}

		return false;
	}

	private void EnterLong(decimal price, decimal atr)
	{
		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_longStopPrice = price - atr * StopLossMultiplier;
		_longTakeProfit = price + atr * TakeProfitMultiplier;
		_shortStopPrice = null;
		_shortTakeProfit = null;
	}

	private void EnterShort(decimal price, decimal atr)
	{
		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_shortStopPrice = price + atr * StopLossMultiplier;
		_shortTakeProfit = price - atr * TakeProfitMultiplier;
		_longStopPrice = null;
		_longTakeProfit = null;
	}
}