Ver no GitHub

Rabbit M2 Strategy

Overview

Rabbit M2 is a trend-following strategy that combines momentum oscillators, Donchian breakouts and adaptive position sizing. The original MetaTrader 5 version by Peter Byrom toggles between buying and selling regimes based on higher timeframe exponential moving averages (EMAs). Within the active regime the strategy waits for Williams %R swings confirmed by the Commodity Channel Index (CCI) before opening a trade. Positions are protected with fixed-distance stop loss and take profit targets and are force-closed when price violates the opposite Donchian channel boundary. After each profitable exit above a configurable profit target the strategy increases its base order size and doubles the profit target threshold, mimicking the scaling logic from the MQL expert advisor.

Indicators and market data

  • Fast EMA (40) & Slow EMA (80) calculated on 1-hour candles steer the trading direction and close trades on regime flips.
  • Commodity Channel Index (14) measured on the primary timeframe confirms overbought or oversold momentum.
  • Williams %R (50) on the primary timeframe provides the trigger when it crosses the -20/-80 levels.
  • Donchian Channel (100) derived from the primary timeframe defines breakout exits when price breaches the previous 100-bar high or low.
  • Fixed stop loss and take profit are set 50 pips away from the entry price (pip size adapts to 3/5-digit instruments).

Two data streams are required: the configurable primary timeframe for CCI/Williams %R/Donchian calculations and a dedicated 1-hour stream for the EMA trend filter.

Trading rules

Regime control

  1. When the 40-period EMA on the H1 feed drops below the 80-period EMA, all long positions are closed and only short setups are allowed.
  2. When the 40-period EMA rises above the 80-period EMA, all short positions are closed and only long setups are allowed.

Entry conditions

  • Short entry
    • Williams %R falls below -20 while its previous value was between -20 and 0.
    • CCI is above the sell level (default 101).
    • Short regime is active and the current net position volume is below the MaxOpenPositions limit.
  • Long entry
    • Williams %R climbs above -80 while its previous value was between -100 and -80.
    • CCI is below the buy level (default 99).
    • Long regime is active and the current net position volume is below the MaxOpenPositions limit.

On every entry the strategy closes opposing exposure (if any) and opens the new position with the current base volume.

Exit conditions

  1. Stop loss and take profit are evaluated on every finished candle: longs exit if the low crosses the stop or the high reaches the target, shorts behave inversely.
  2. Independent of stop/target, shorts exit when price closes above the previous 100-bar high and longs exit when price closes below the previous 100-bar low.
  3. A regime flip (fast EMA crossing the slow EMA) immediately liquidates existing exposure.

Position sizing logic

  • The base order volume starts from InitialVolume (default 0.01) and follows exchange limits (step/min/max).
  • After each realized profit larger than BigWinTarget the base volume increases by VolumeStep and the threshold doubles, preserving the cascading growth pattern of the original expert advisor.
  • The MaxOpenPositions parameter limits net exposure. In the StockSharp port positions are netted, so hitting the limit means no additional volume is added until exposure drops.

Parameters

Name Default Description
CciSellLevel 101 Minimum CCI value required to confirm a short setup.
CciBuyLevel 99 Maximum CCI value required to confirm a long setup.
CciPeriod 14 Period of the Commodity Channel Index on the primary timeframe.
DonchianPeriod 100 Lookback for the Donchian channel used in exit logic.
MaxOpenPositions 1 Maximum allowed net position multiples of the base volume.
BigWinTarget 1.50 Profit (in account currency) needed to scale the volume.
VolumeStep 0.01 Increment added to the base volume after a qualifying win.
WprPeriod 50 Length of the Williams %R oscillator.
FastEmaPeriod 40 Fast EMA period on the 1-hour trend feed.
SlowEmaPeriod 80 Slow EMA period on the 1-hour trend feed.
TakeProfitPips 50 Distance of the take profit in pips.
StopLossPips 50 Distance of the stop loss in pips.
InitialVolume 0.01 Starting order volume before scaling rules.
CandleType 15-minute candles Primary timeframe used for CCI/Williams %R/Donchian calculations.

Implementation notes

  • The StockSharp port emulates MT5 stop loss and take profit by monitoring candle highs/lows instead of placing broker-attached orders.
  • Price steps and pip calculations automatically adjust for 3 or 5 decimal instruments by multiplying the reported tick size by 10.
  • The strategy relies on realized PnL updates to detect "big wins"; ensure trades are reported back to the strategy for scaling to work.
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;
	}
}