GitHub で見る

Rabbit M2 戦略

概要

Rabbit M2 は、モメンタムオシレーター、ドンチャンブレイクアウト、適応的なポジションサイジングを組み合わせたトレンドフォロー戦略です。Peter Byrom によるオリジナルの MetaTrader 5 バージョンは、高い時間軸の指数移動平均(EMA)に基づいて買いと売りのレジームを切り替えます。アクティブなレジームでは、戦略はトレードを開く前に Commodity Channel Index(CCI)によって確認された Williams %R のスイングを待ちます。ポジションは固定距離のストップロスとテイクプロフィットのターゲットで保護され、価格が反対側のドンチャンチャネル境界に違反した場合に強制クローズされます。設定可能な利益ターゲットを超える収益性の高い出口のたびに、戦略はベース注文サイズを増加させ、利益ターゲット閾値を倍増させ、MQL エキスパートアドバイザーのスケーリングロジックを模倣します。

インジケーターと市場データ

  • 高速 EMA (40) と低速 EMA (80) 1時間ローソク足で計算され、取引方向を制御し、レジーム転換時にトレードをクローズします。
  • Commodity Channel Index (14) プライマリ時間軸で計算され、買われ過ぎまたは売られ過ぎのモメンタムを確認します。
  • Williams %R (50) プライマリ時間軸で、-20/-80 レベルをクロスするとトリガーを提供します。
  • ドンチャンチャネル (100) プライマリ時間軸から派生し、価格が前の 100 バーの高値または安値を突破したときのブレイクアウト出口を定義します。
  • 固定ストップロスとテイクプロフィット はエントリー価格から 50 pips 離れた場所に設定されます(pip サイズは 3/5 桁の商品に合わせて調整されます)。

2 つのデータストリームが必要です:CCI/Williams %R/ドンチャン計算用の設定可能なプライマリ時間軸と、EMA トレンドフィルター用の専用 1 時間ストリームです。

取引ルール

レジームコントロール

  1. H1 フィードの 40 期間 EMA が 80 期間 EMA を下回ると、すべてのロングポジションがクローズされ、ショートセットアップのみが許可されます。
  2. 40 期間 EMA が 80 期間 EMA を上回ると、すべてのショートポジションがクローズされ、ロングセットアップのみが許可されます。

エントリー条件

  • ショートエントリー
    • Williams %R が -20 を下回り、前の値が -20 と 0 の間にあること。
    • CCI が売りレベル(デフォルト 101)を上回ること。
    • ショートレジームがアクティブで、現在のネットポジションボリュームが MaxOpenPositions 制限を下回ること。
  • ロングエントリー
    • Williams %R が -80 を上回り、前の値が -100 と -80 の間にあること。
    • CCI が買いレベル(デフォルト 99)を下回ること。
    • ロングレジームがアクティブで、現在のネットポジションボリュームが MaxOpenPositions 制限を下回ること。

各エントリーで、戦略は反対側のエクスポージャーをクローズし(ある場合)、現在のベースボリュームで新しいポジションをオープンします。

エグジット条件

  1. ストップロスとテイクプロフィットは完了したローソク足ごとに評価されます:ロングは安値がストップをクロスするか、高値がターゲットに達すると出る;ショートは逆。
  2. ストップ/ターゲットとは独立して、ショートは価格が前の 100 バー高値を超えてクローズしたとき、ロングは価格が前の 100 バー安値を下回ってクローズしたときに出ます。
  3. レジーム転換(高速 EMA が低速 EMA をクロス)は既存のエクスポージャーを即座に清算します。

ポジションサイジングロジック

  • ベース注文ボリュームは InitialVolume(デフォルト 0.01)から始まり、取引所の制限(ステップ/最小/最大)に従います。
  • BigWinTarget を超えた実現利益のたびに、ベースボリュームは VolumeStep 分増加し、閾値は倍増し、オリジナルのエキスパートアドバイザーのカスケード成長パターンを維持します。
  • MaxOpenPositions パラメーターはネットエクスポージャーを制限します。StockSharp ポートではポジションがネットされるため、制限に達するとエクスポージャーが下がるまで追加ボリュームは加算されません。

パラメーター

名前 デフォルト 説明
CciSellLevel 101 ショートセットアップを確認するために必要な最小 CCI 値。
CciBuyLevel 99 ロングセットアップを確認するために必要な最大 CCI 値。
CciPeriod 14 プライマリ時間軸の Commodity Channel Index の期間。
DonchianPeriod 100 出口ロジックで使用されるドンチャンチャネルのルックバック。
MaxOpenPositions 1 ベースボリュームの最大許可ネットポジション倍数。
BigWinTarget 1.50 ボリュームをスケーリングするために必要な利益(口座通貨)。
VolumeStep 0.01 適格な勝利後にベースボリュームに追加される増分。
WprPeriod 50 Williams %R オシレーターの長さ。
FastEmaPeriod 40 1 時間トレンドフィードの高速 EMA 期間。
SlowEmaPeriod 80 1 時間トレンドフィードの低速 EMA 期間。
TakeProfitPips 50 テイクプロフィットの pips 単位の距離。
StopLossPips 50 ストップロスの pips 単位の距離。
InitialVolume 0.01 スケーリングルール前の開始注文ボリューム。
CandleType 15 分ローソク足 CCI/Williams %R/ドンチャン計算に使用されるプライマリ時間軸。

実装上の注意

  • StockSharp ポートは、ブローカー付きの注文を配置する代わりにローソク足の高値/安値を監視することで MT5 のストップロスとテイクプロフィットをエミュレートします。
  • 価格ステップと pip 計算は、報告されたティックサイズに 10 を掛けることで 3 桁または 5 桁の商品に自動的に調整されます。
  • 戦略は「大きな勝利」を検出するために実現 PnL 更新に依存しています;スケーリングが機能するように、トレードが戦略に報告されることを確認してください。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Rabbit M2 strategy converted from the MetaTrader 5 expert advisor.
/// Combines EMA trend gating, Williams %R momentum and adaptive position sizing.
/// </summary>
public class RabbitM2Strategy : Strategy
{
	private readonly StrategyParam<int> _cciSellLevel;
	private readonly StrategyParam<int> _cciBuyLevel;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<int> _donchianPeriod;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<decimal> _bigWinTarget;
	private readonly StrategyParam<decimal> _volumeStep;
	private readonly StrategyParam<int> _wprPeriod;
	private readonly StrategyParam<int> _fastEmaPeriod;
	private readonly StrategyParam<int> _slowEmaPeriod;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _tradeVolume;
	private decimal _bigWinThreshold;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal? _previousWpr;
	private decimal? _previousDonchianUpper;
	private decimal? _previousDonchianLower;
	private bool _buyAllowed;
	private bool _sellAllowed;
	private decimal _lastRealizedPnL;
	private decimal _currentStop;
	private decimal _currentTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="RabbitM2Strategy"/> class.
	/// </summary>
	public RabbitM2Strategy()
	{
		_cciSellLevel = Param(nameof(CciSellLevel), 101)
			.SetDisplay("CCI Sell Level", "CCI threshold confirming short signals", "CCI")
			;

		_cciBuyLevel = Param(nameof(CciBuyLevel), 99)
			.SetDisplay("CCI Buy Level", "CCI threshold confirming long signals", "CCI")
			;

		_cciPeriod = Param(nameof(CciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Lookback for the Commodity Channel Index", "CCI")
			;

		_donchianPeriod = Param(nameof(DonchianPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Donchian Period", "Lookback used for breakout exits", "Donchian")
			;

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum net exposure in base volume multiples", "Risk")
			;

		_bigWinTarget = Param(nameof(BigWinTarget), 1.50m)
			.SetGreaterThanZero()
			.SetDisplay("Big Win Target", "Profit needed before increasing position size", "Money Management")
			;

		_volumeStep = Param(nameof(VolumeStep), 0.01m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Step", "Increment applied to the base volume after a big win", "Money Management")
			;

		_wprPeriod = Param(nameof(WprPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Williams %R Period", "Length of the Williams %R oscillator", "Momentum")
			;

		_fastEmaPeriod = Param(nameof(FastEmaPeriod), 40)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA Period", "Fast EMA period on the hourly trend feed", "Trend Filter")
			;

		_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 80)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA Period", "Slow EMA period on the hourly trend feed", "Trend Filter")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance from entry to take profit", "Risk")
			;

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance from entry to stop loss", "Risk")
			;

		_initialVolume = Param(nameof(InitialVolume), 0.01m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Starting base order size", "Money Management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Primary Candle Type", "Timeframe for CCI, Williams %R and Donchian", "General");
	}

	/// <summary>
	/// Minimum CCI value required to confirm a short setup.
	/// </summary>
	public int CciSellLevel
	{
		get => _cciSellLevel.Value;
		set => _cciSellLevel.Value = value;
	}

	/// <summary>
	/// Maximum CCI value required to confirm a long setup.
	/// </summary>
	public int CciBuyLevel
	{
		get => _cciBuyLevel.Value;
		set => _cciBuyLevel.Value = value;
	}

	/// <summary>
	/// CCI calculation period.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Donchian channel lookback length.
	/// </summary>
	public int DonchianPeriod
	{
		get => _donchianPeriod.Value;
		set => _donchianPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of net position multiples that can be opened.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Profit threshold that triggers a volume increase.
	/// </summary>
	public decimal BigWinTarget
	{
		get => _bigWinTarget.Value;
		set => _bigWinTarget.Value = value;
	}

	/// <summary>
	/// Volume increment applied after a qualifying win.
	/// </summary>
	public decimal VolumeStep
	{
		get => _volumeStep.Value;
		set => _volumeStep.Value = value;
	}

	/// <summary>
	/// Williams %R period.
	/// </summary>
	public int WprPeriod
	{
		get => _wprPeriod.Value;
		set => _wprPeriod.Value = value;
	}

	/// <summary>
	/// Fast EMA period used for the hourly trend filter.
	/// </summary>
	public int FastEmaPeriod
	{
		get => _fastEmaPeriod.Value;
		set => _fastEmaPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period used for the hourly trend filter.
	/// </summary>
	public int SlowEmaPeriod
	{
		get => _slowEmaPeriod.Value;
		set => _slowEmaPeriod.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Base order size before scaling logic is applied.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Primary candle type used for CCI, Williams %R and Donchian calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
		yield return (Security, TimeSpan.FromHours(4).TimeFrame());
	}

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

		_tradeVolume = 0m;
		_bigWinThreshold = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_previousWpr = null;
		_previousDonchianUpper = null;
		_previousDonchianLower = null;
		_buyAllowed = false;
		_sellAllowed = false;
		_lastRealizedPnL = 0m;
		_currentStop = 0m;
		_currentTake = 0m;
	}

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

		_tradeVolume = InitialVolume;
		_bigWinThreshold = BigWinTarget;
		EnsureVolumeBoundaries();
		_lastRealizedPnL = PnL;

		var pipSize = GetPipSize();
		_stopLossDistance = StopLossPips * pipSize;
		_takeProfitDistance = TakeProfitPips * pipSize;

		// Initialize indicators that operate on the primary timeframe.
		var wpr = new WilliamsR { Length = WprPeriod };
		var cci = new CommodityChannelIndex { Length = CciPeriod };
		var donchian = new DonchianChannels { Length = DonchianPeriod };

		// Initialize hourly EMA indicators for trend gating.
		var emaFast = new EMA { Length = FastEmaPeriod };
		var emaSlow = new EMA { Length = SlowEmaPeriod };

		var trendSubscription = SubscribeCandles(TimeSpan.FromHours(4).TimeFrame());
		trendSubscription
			.Bind(emaFast, emaSlow, ProcessTrend)
			.Start();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(wpr, cci, donchian, ProcessCandle)
			.Start();

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

	private void ProcessTrend(ICandleMessage candle, decimal emaFast, decimal emaSlow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (emaFast < emaSlow)
		{
			_sellAllowed = true;
			_buyAllowed = false;
			CloseLongPosition("EMA trend flipped to bearish mode");
		}
		else if (emaFast > emaSlow)
		{
			_buyAllowed = true;
			_sellAllowed = false;
			CloseShortPosition("EMA trend flipped to bullish mode");
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue wprValue, IIndicatorValue cciValue, IIndicatorValue donchianValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!wprValue.IsFinal || !cciValue.IsFinal || !donchianValue.IsFinal)
			return;

		var donchian = (DonchianChannelsValue)donchianValue;
		if (donchian.UpperBand is not decimal upperBand || donchian.LowerBand is not decimal lowerBand)
			return;

		// Always evaluate protective exits before considering new entries.
		ManageExistingPosition(candle, upperBand, lowerBand);

		var wprCurrent = wprValue.ToDecimal();
		var wprPrevious = _previousWpr;
		var cciCurrent = cciValue.ToDecimal();

		if (wprCurrent == 0m)
			wprCurrent = -1m;

		_previousWpr = wprCurrent;

		// indicators bound via .BindEx()

		if (_tradeVolume <= 0m)
			return;

		if (wprPrevious is null)
			return;

		var wprLag = wprPrevious.Value;
		if (wprLag == 0m)
			wprLag = -1m;

		// Check for short entries when the short regime is active.
		var canAddShort = Position <= 0m && Math.Abs(Position) < _tradeVolume * MaxOpenPositions;
		if (_sellAllowed && canAddShort && wprCurrent < -20m && wprLag > -20m && wprLag < 0m && cciCurrent > CciSellLevel)
		{
			var volume = _tradeVolume + Math.Max(0m, Position);
			if (volume > 0m)
			{
				SellMarket();
				_currentStop = candle.ClosePrice + _stopLossDistance;
				_currentTake = candle.ClosePrice - _takeProfitDistance;
			}
			return;
		}

		// Check for long entries when the long regime is active.
		var canAddLong = Position >= 0m && Math.Abs(Position) < _tradeVolume * MaxOpenPositions;
		if (_buyAllowed && canAddLong && wprCurrent > -80m && wprLag < -80m && wprLag < 0m && cciCurrent < CciBuyLevel)
		{
			var volume = _tradeVolume + Math.Max(0m, -Position);
			if (volume > 0m)
			{
				BuyMarket();
				_currentStop = candle.ClosePrice - _stopLossDistance;
				_currentTake = candle.ClosePrice + _takeProfitDistance;
			}
		}
	}

	private void ManageExistingPosition(ICandleMessage candle, decimal currentUpper, decimal currentLower)
	{
		if (Position > 0m)
		{
			// Protect long positions with take profit, stop loss and Donchian breakout checks.
			if (_currentTake > 0m && candle.HighPrice >= _currentTake)
			{
				CloseLongPosition("Take profit reached");
			}
			else if (_currentStop > 0m && candle.LowPrice <= _currentStop)
			{
				CloseLongPosition("Stop loss reached");
			}
			else if (_previousDonchianLower is decimal previousLower && candle.ClosePrice < previousLower)
			{
				CloseLongPosition("Donchian breakout against long position");
			}
		}
		else if (Position < 0m)
		{
			// Protect short positions using the same logic mirrored for shorts.
			if (_currentTake > 0m && candle.LowPrice <= _currentTake)
			{
				CloseShortPosition("Take profit reached");
			}
			else if (_currentStop > 0m && candle.HighPrice >= _currentStop)
			{
				CloseShortPosition("Stop loss reached");
			}
			else if (_previousDonchianUpper is decimal previousUpper && candle.ClosePrice > previousUpper)
			{
				CloseShortPosition("Donchian breakout against short position");
			}
		}

		_previousDonchianUpper = currentUpper;
		_previousDonchianLower = currentLower;
	}

	private void CloseLongPosition(string reason)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket();
		_currentStop = 0m;
		_currentTake = 0m;
		LogInfo($"Closing long position: {reason}.");
	}

	private void CloseShortPosition(string reason)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket();
		_currentStop = 0m;
		_currentTake = 0m;
		LogInfo($"Closing short position: {reason}.");
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var realizedChange = PnL - _lastRealizedPnL;
		_lastRealizedPnL = PnL;

		// Increase the base volume after sufficiently profitable exits.
		if (realizedChange > _bigWinThreshold)
		{
			_tradeVolume += VolumeStep;
			EnsureVolumeBoundaries();
			_bigWinThreshold *= 2m;
		}

		if (Math.Abs(Position) == 0m)
		{
			_currentStop = 0m;
			_currentTake = 0m;
		}
	}

	private void EnsureVolumeBoundaries()
	{
		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			var steps = Math.Floor(_tradeVolume / step.Value);
			_tradeVolume = steps * step.Value;
		}

		var max = Security?.MaxVolume;
		if (max.HasValue && max.Value > 0m && _tradeVolume > max.Value)
			_tradeVolume = max.Value;

		var min = Security?.MinVolume;
		if (min.HasValue && min.Value > 0m && _tradeVolume < min.Value)
			_tradeVolume = 0m;

		Volume = _tradeVolume;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			priceStep = 0.0001m;

		var decimals = Security?.Decimals;
		if (decimals == 3 || decimals == 5)
			priceStep *= 10m;

		return priceStep;
	}
}