Auf GitHub ansehen

Rabbit M3

Overview

Rabbit M3 is a port of the MetaTrader 4 expert advisor RabbitM3 (also released under the name "Petes Party Trick"). The system flips between long-only and short-only regimes using a pair of hourly exponential moving averages. Momentum confirmation comes from a Williams %R cross combined with a CCI level filter, while an extremely long Donchian channel watches for price breakouts that invalidate the current trend bias. Position size can optionally grow after large winners, replicating the lot-scaling rule contained in the original code.

Strategy logic

Trend regime filter

  • When the fast EMA closes below the slow EMA, any existing long exposure is liquidated and new signals are restricted to the short side.
  • When the fast EMA closes above the slow EMA, any existing short exposure is closed and only long setups remain eligible.
  • If the EMAs are equal the previous regime is kept, mirroring the MetaTrader logic that only toggles on strict inequalities.

Entry rules

  • Short trades
    • Regime must be short-only (fast EMA below slow EMA).
    • Williams %R (length = WilliamsPeriod) must cross down through the WilliamsSellLevel on the most recent candle while the previous value was still below zero.
    • CCI (length = CciPeriod) must be greater than or equal to CciSellLevel.
    • Net position must be flat; the strategy opens at most MaxOpenPositions trades and defaults to a single market order of size EntryVolume.
  • Long trades
    • Regime must be long-only (fast EMA above slow EMA).
    • Williams %R must cross up through the WilliamsBuyLevel while the previous value was still below zero.
    • CCI must be less than or equal to CciBuyLevel.
    • Net position must be flat before a new long is initiated.

Exit rules

  • Hard stopsStopLossPips and TakeProfitPips are converted into price offsets using the instrument’s price step. A value of 0 disables the corresponding protective level.
  • Donchian breakout – if price closes above the previous Donchian upper band (length = DonchianLength) any short position is closed immediately. A close below the previous lower band closes longs. The channel uses the prior completed value to reproduce the iHighest/iLowest lag from the EA.
  • Regime flip – whenever the EMA relationship reverses the strategy liquidates the opposing exposure before allowing new trades in the fresh direction.

Money management

  • Starts with EntryVolume units per trade.
  • When a realized profit greater than BigWinThreshold occurs while the strategy is flat, volume is increased by VolumeIncrement and the threshold doubles (4 → 8 → 16, etc.). If either parameter is set to 0 the scaling rule is disabled.

Parameters

  • Fast EMA Period – length of the fast trend filter (default: 33).
  • Slow EMA Period – length of the slow trend filter (default: 70).
  • Williams %R Period – lookback for the Williams %R oscillator (default: 62).
  • Williams Sell Level – upper bound that must be crossed downward for short signals (default: −20).
  • Williams Buy Level – lower bound that must be crossed upward for long signals (default: −80).
  • CCI Period – lookback for the Commodity Channel Index (default: 26).
  • CCI Sell Level – minimum CCI value required to permit shorts (default: 101).
  • CCI Buy Level – maximum CCI value required to permit longs (default: 99).
  • Donchian Length – number of completed candles sampled for the breakout exit (default: 410).
  • Max Open Positions – maximum simultaneous trades; the classic setup uses one contract (default: 1).
  • Take Profit (pips) – profit target measured in price steps (default: 360).
  • Stop Loss (pips) – protective stop measured in price steps (default: 20).
  • Entry Volume – starting order size (default: 0.01).
  • Big Win Threshold – realized profit required before increasing size (default: 4.0).
  • Volume Increment – additional volume added after beating the threshold (default: 0.01).
  • Candle Type – timeframe used for all indicator calculations (default: hourly candles).

Additional notes

  • Pip conversion relies on the security’s PriceStep. Instruments without a price step fall back to a unitary pip value.
  • Donchian levels are intentionally lagged by one candle so the exit mirrors the shift=1 logic of the original MetaTrader calls.
  • Volume scaling only evaluates realized PnL while the position is flat, preventing floating gains from triggering false positives.
  • The UI label objects present in the source EA are omitted because StockSharp visualizes state through charts and logs.
  • Only the C# implementation is provided in this package; there is no Python version.
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>
/// Port of the MetaTrader expert advisor RabbitM3 (aka "Petes Party Trick").
/// Switches between long-only and short-only modes using hourly exponential moving averages.
/// Uses Williams %R momentum crosses and CCI thresholds for entries, Donchian channel for emergency exits,
/// and optional position sizing that grows after large winning trades.
/// </summary>
public class RabbitM3Strategy : Strategy
{
	private readonly StrategyParam<int> _fastEmaPeriod;
	private readonly StrategyParam<int> _slowEmaPeriod;
	private readonly StrategyParam<int> _williamsPeriod;
	private readonly StrategyParam<decimal> _williamsSellLevel;
	private readonly StrategyParam<decimal> _williamsBuyLevel;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciSellLevel;
	private readonly StrategyParam<decimal> _cciBuyLevel;
	private readonly StrategyParam<int> _donchianLength;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _entryVolume;
	private readonly StrategyParam<decimal> _bigWinThreshold;
	private readonly StrategyParam<decimal> _volumeIncrement;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma = null!;
	private ExponentialMovingAverage _slowEma = null!;
	private CommodityChannelIndex _cci = null!;
	private WilliamsR _williams = null!;
	private DonchianChannels _donchian = null!;

	private decimal _pipSize;
	private decimal _currentVolume;
	private decimal _currentBigWinTarget;

	private decimal? _previousWilliams;
	private decimal? _currentDonchianUpper;
	private decimal? _currentDonchianLower;
	private decimal? _previousDonchianUpper;
	private decimal? _previousDonchianLower;

	private TrendDirections _trendDirection;
	private bool _allowBuy;
	private bool _allowSell;

	private bool _longActive;
	private bool _shortActive;
	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal _longStopPrice;
	private decimal _longTakeProfitPrice;
	private decimal _shortStopPrice;
	private decimal _shortTakeProfitPrice;

	private enum TrendDirections
	{
		Neutral,
		Bullish,
		Bearish,
	}

	/// <summary>
	/// Initializes strategy parameters with RabbitM3 defaults.
	/// </summary>
	public RabbitM3Strategy()
	{
		_fastEmaPeriod = Param(nameof(FastEmaPeriod), 33)
		.SetGreaterThanZero()
		.SetDisplay("Fast EMA Period", "Length of the fast trend filter (H1 EMA)", "Trend Filter")
		
		.SetOptimize(10, 80, 5);

		_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 70)
		.SetGreaterThanZero()
		.SetDisplay("Slow EMA Period", "Length of the slow trend filter (H1 EMA)", "Trend Filter")
		
		.SetOptimize(20, 120, 5);

		_williamsPeriod = Param(nameof(WilliamsPeriod), 62)
		.SetGreaterThanZero()
		.SetDisplay("Williams %R Period", "Lookback for Williams %R momentum", "Entry Filter")
		
		.SetOptimize(20, 100, 5);

		_williamsSellLevel = Param(nameof(WilliamsSellLevel), -20m)
		.SetDisplay("Williams Sell Level", "Upper threshold crossed downward to trigger shorts", "Entry Filter");

		_williamsBuyLevel = Param(nameof(WilliamsBuyLevel), -80m)
		.SetDisplay("Williams Buy Level", "Lower threshold crossed upward to trigger longs", "Entry Filter");

		_cciPeriod = Param(nameof(CciPeriod), 26)
		.SetGreaterThanZero()
		.SetDisplay("CCI Period", "Commodity Channel Index period", "Entry Filter")
		
		.SetOptimize(10, 60, 5);

		_cciSellLevel = Param(nameof(CciSellLevel), 101m)
		.SetDisplay("CCI Sell Level", "Minimum CCI value required for short entries", "Entry Filter");

		_cciBuyLevel = Param(nameof(CciBuyLevel), 99m)
		.SetDisplay("CCI Buy Level", "Maximum CCI value allowed for long entries", "Entry Filter");

		_donchianLength = Param(nameof(DonchianLength), 410)
		.SetGreaterThanZero()
		.SetDisplay("Donchian Length", "History depth used for stop-and-reverse exits", "Risk")
		
		.SetOptimize(100, 600, 50);

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
		.SetGreaterThanZero()
		.SetDisplay("Max Open Positions", "Maximum simultaneous trades (net position based)", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 360m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Fixed profit target distance from entry", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 20m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Protective stop distance from entry", "Risk");

		_entryVolume = Param(nameof(EntryVolume), 0.01m)
		.SetGreaterThanZero()
		.SetDisplay("Entry Volume", "Initial position size for each trade", "Money Management");

		_bigWinThreshold = Param(nameof(BigWinThreshold), 4m)
		.SetNotNegative()
		.SetDisplay("Big Win Threshold", "Profit required to increase volume; doubles after each trigger", "Money Management");

		_volumeIncrement = Param(nameof(VolumeIncrement), 0.01m)
		.SetNotNegative()
		.SetDisplay("Volume Increment", "Increment added to volume after beating Big Win Threshold", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for all indicators", "General");
	}

	/// <summary>
	/// Fast EMA period (hourly in the original EA).
	/// </summary>
	public int FastEmaPeriod
	{
		get => _fastEmaPeriod.Value;
		set => _fastEmaPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period (hourly in the original EA).
	/// </summary>
	public int SlowEmaPeriod
	{
		get => _slowEmaPeriod.Value;
		set => _slowEmaPeriod.Value = value;
	}

	/// <summary>
	/// Williams %R lookback length.
	/// </summary>
	public int WilliamsPeriod
	{
		get => _williamsPeriod.Value;
		set => _williamsPeriod.Value = value;
	}

	/// <summary>
	/// Level (in %R units) that must be crossed downward to arm short entries.
	/// </summary>
	public decimal WilliamsSellLevel
	{
		get => _williamsSellLevel.Value;
		set => _williamsSellLevel.Value = value;
	}

	/// <summary>
	/// Level (in %R units) that must be crossed upward to arm long entries.
	/// </summary>
	public decimal WilliamsBuyLevel
	{
		get => _williamsBuyLevel.Value;
		set => _williamsBuyLevel.Value = value;
	}

	/// <summary>
	/// Commodity Channel Index period.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Minimum CCI value required before a short setup is valid.
	/// </summary>
	public decimal CciSellLevel
	{
		get => _cciSellLevel.Value;
		set => _cciSellLevel.Value = value;
	}

	/// <summary>
	/// Maximum CCI value allowed before a long setup is valid.
	/// </summary>
	public decimal CciBuyLevel
	{
		get => _cciBuyLevel.Value;
		set => _cciBuyLevel.Value = value;
	}

	/// <summary>
	/// Number of candles considered for Donchian exit levels.
	/// </summary>
	public int DonchianLength
	{
		get => _donchianLength.Value;
		set => _donchianLength.Value = value;
	}

	/// <summary>
	/// Maximum net open positions. RabbitM3 defaults to a single trade.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips (chart points).
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips (chart points).
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initial order volume.
	/// </summary>
	public decimal EntryVolume
	{
		get => _entryVolume.Value;
		set => _entryVolume.Value = value;
	}

	/// <summary>
	/// Profit threshold that increases the trading volume after a winning trade.
	/// </summary>
	public decimal BigWinThreshold
	{
		get => _bigWinThreshold.Value;
		set => _bigWinThreshold.Value = value;
	}

	/// <summary>
	/// Volume increment applied when the big win logic is triggered.
	/// </summary>
	public decimal VolumeIncrement
	{
		get => _volumeIncrement.Value;
		set => _volumeIncrement.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_pipSize = 0m;
		_currentVolume = 0m;
		_currentBigWinTarget = 0m;
		_previousWilliams = null;
		_currentDonchianUpper = null;
		_currentDonchianLower = null;
		_previousDonchianUpper = null;
		_previousDonchianLower = null;
		_trendDirection = TrendDirections.Neutral;
		_allowBuy = false;
		_allowSell = false;
		_longActive = false;
		_shortActive = false;
		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_longStopPrice = 0m;
		_longTakeProfitPrice = 0m;
		_shortStopPrice = 0m;
		_shortTakeProfitPrice = 0m;
	}

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

		_fastEma = new EMA
		{
			Length = FastEmaPeriod,
		};

		_slowEma = new EMA
		{
			Length = SlowEmaPeriod,
		};

		_cci = new CommodityChannelIndex
		{
			Length = CciPeriod,
		};

		_williams = new WilliamsR
		{
			Length = WilliamsPeriod,
		};

		_donchian = new DonchianChannels
		{
			Length = DonchianLength,
		};

		_pipSize = Security?.PriceStep ?? 0m;
		if (_pipSize <= 0m)
			_pipSize = 1m;

		_currentVolume = EntryVolume;
		Volume = _currentVolume;

		_currentBigWinTarget = BigWinThreshold > 0m && VolumeIncrement > 0m
			? BigWinThreshold
			: decimal.MaxValue;

		var subscription = SubscribeCandles(CandleType);

		subscription.Bind(_fastEma, _slowEma, _cci, _williams, ProcessCandle);
		subscription.BindEx(_donchian, UpdateDonchian);
		subscription.Start();

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal cciValue, decimal williamsValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		UpdateTrendState(fastValue, slowValue);

		if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_cci.IsFormed || !_williams.IsFormed)
		{
			_previousWilliams = williamsValue;
			return;
		}

		if (_previousDonchianUpper is not decimal exitUpper || _previousDonchianLower is not decimal exitLower)
		{
			_previousWilliams = williamsValue;
			return;
		}

		ManageExits(candle, exitUpper, exitLower);

		TryEnterPosition(candle, cciValue, williamsValue);

		_previousWilliams = williamsValue;
	}

	private void UpdateDonchian(ICandleMessage candle, IIndicatorValue donchianValue)
	{
		if (!donchianValue.IsFinal)
			return;

		var channels = (DonchianChannelsValue)donchianValue;

		if (channels.UpperBand is not decimal upper || channels.LowerBand is not decimal lower)
			return;

		if (_currentDonchianUpper.HasValue && _currentDonchianLower.HasValue)
		{
			_previousDonchianUpper = _currentDonchianUpper;
			_previousDonchianLower = _currentDonchianLower;
		}

		_currentDonchianUpper = upper;
		_currentDonchianLower = lower;
	}

	private void UpdateTrendState(decimal fastValue, decimal slowValue)
	{
		if (fastValue < slowValue)
		{
			if (_trendDirection == TrendDirections.Bearish)
				return;

			if (Position > 0m)
				CloseLongPosition("EMA trend flipped bearish");

			_allowSell = true;
			_allowBuy = false;
			_trendDirection = TrendDirections.Bearish;
		}
		else if (fastValue > slowValue)
		{
			if (_trendDirection == TrendDirections.Bullish)
				return;

			if (Position < 0m)
				CloseShortPosition("EMA trend flipped bullish");

			_allowSell = false;
			_allowBuy = true;
			_trendDirection = TrendDirections.Bullish;
		}
	}

	private void ManageExits(ICandleMessage candle, decimal exitUpper, decimal exitLower)
	{
		if (Position < 0m)
		{
			if (_shortActive)
			{
				if (TakeProfitPips > 0m && candle.LowPrice <= _shortTakeProfitPrice)
				{
					CloseShortPosition("Take profit reached");
					return;
				}

				if (StopLossPips > 0m && candle.HighPrice >= _shortStopPrice)
				{
					CloseShortPosition("Stop loss hit");
					return;
				}
			}

			if (candle.ClosePrice >= exitUpper)
			{
				CloseShortPosition("Donchian breakout above upper band");
			}
		}
		else if (Position > 0m)
		{
			if (_longActive)
			{
				if (TakeProfitPips > 0m && candle.HighPrice >= _longTakeProfitPrice)
				{
					CloseLongPosition("Take profit reached");
					return;
				}

				if (StopLossPips > 0m && candle.LowPrice <= _longStopPrice)
				{
					CloseLongPosition("Stop loss hit");
					return;
				}
			}

			if (candle.ClosePrice <= exitLower)
			{
				CloseLongPosition("Donchian breakout below lower band");
			}
		}
	}

	private void TryEnterPosition(ICandleMessage candle, decimal cciValue, decimal williamsValue)
	{
		if (Position != 0m)
			return;

		if (_previousWilliams is not decimal previousWilliams)
			return;

		if (MaxOpenPositions <= 0)
			return;

		var canShort = _allowSell && cciValue > CciSellLevel && previousWilliams > WilliamsSellLevel && previousWilliams < 0m && williamsValue < WilliamsSellLevel;
		if (canShort)
		{
			_shortEntryPrice = candle.ClosePrice;
			_shortStopPrice = StopLossPips > 0m ? _shortEntryPrice + StopLossPips * _pipSize : 0m;
			_shortTakeProfitPrice = TakeProfitPips > 0m ? _shortEntryPrice - TakeProfitPips * _pipSize : 0m;
			_shortActive = true;
			_longActive = false;
			SellMarket(_currentVolume);
			return;
		}

		var canLong = _allowBuy && cciValue < CciBuyLevel && previousWilliams < WilliamsBuyLevel && previousWilliams < 0m && williamsValue > WilliamsBuyLevel;
		if (canLong)
		{
			_longEntryPrice = candle.ClosePrice;
			_longStopPrice = StopLossPips > 0m ? _longEntryPrice - StopLossPips * _pipSize : 0m;
			_longTakeProfitPrice = TakeProfitPips > 0m ? _longEntryPrice + TakeProfitPips * _pipSize : 0m;
			_longActive = true;
			_shortActive = false;
			BuyMarket(_currentVolume);
		}
	}

	private void CloseLongPosition(string reason)
	{
		if (Position <= 0m)
			return;

		LogInfo($"Closing long position: {reason}");
		SellMarket(Position);
		_longActive = false;
		_longEntryPrice = 0m;
		_longStopPrice = 0m;
		_longTakeProfitPrice = 0m;
	}

	private void CloseShortPosition(string reason)
	{
		if (Position >= 0m)
			return;

		LogInfo($"Closing short position: {reason}");
		BuyMarket(-Position);
		_shortActive = false;
		_shortEntryPrice = 0m;
		_shortStopPrice = 0m;
		_shortTakeProfitPrice = 0m;
	}

}