Ver en GitHub

Conejo M3

Descripción general

Rabbit M3 es una adaptación del MetaTrader 4 asesor experto RabbitM3 (también lanzado con el nombre "Petes Party Trick"). El sistema alterna entre regímenes de sólo largo y sólo de corto utilizando un par de medias móviles exponenciales horarias. La confirmación del impulso proviene de un cruce Williams %R combinado con un filtro de nivel CCI, mientras que un canal Donchian extremadamente largo busca desgloses de precios que invaliden el sesgo de tendencia actual. Opcionalmente, el tamaño de la posición puede crecer después de los grandes ganadores, replicando la regla de escala de lotes contenida en el código original.

Lógica estratégica

Filtro de régimen de tendencias

  • Cuando el EMA rápido cierra por debajo del EMA lento, cualquier exposición larga existente se liquida y las nuevas señales se restringen al lado corto.
  • Cuando el EMA rápido cierra por encima del EMA lento, cualquier exposición corta existente se cierra y solo las configuraciones largas siguen siendo elegibles.
  • Si las EMA son iguales, se mantiene el régimen anterior, reflejando la lógica MetaTrader que solo alterna desigualdades estrictas.

Reglas de entrada

  • Operaciones cortas
    • El régimen debe ser solo corto (EMA rápida debajo de EMA lenta).
    • Williams %R (longitud = WilliamsPeriod) debe cruzar hacia abajo a través del WilliamsSellLevel en la vela más reciente mientras el valor anterior aún estaba por debajo de cero.
    • CCI (longitud = CciPeriod) debe ser mayor o igual a CciSellLevel.
    • La posición neta debe ser plana; la estrategia abre como máximo MaxOpenPositions operaciones y de forma predeterminada utiliza una única orden de mercado de tamaño EntryVolume.
  • Operaciones largas
    • El régimen debe ser solo largo (EMA rápida arriba de EMA lenta).
    • Williams %R debe cruzar WilliamsBuyLevel mientras el valor anterior todavía estaba por debajo de cero.
    • CCI debe ser menor o igual a CciBuyLevel.
    • La posición neta debe ser plana antes de que se inicie una nueva posición larga.

reglas de salida

  • Paradas bruscas: StopLossPips y TakeProfitPips se convierten en compensaciones de precios utilizando el paso de precio del instrumento. Un valor de 0 desactiva el nivel de protección correspondiente.
  • Donchian ruptura: si el precio cierra por encima de la banda superior Donchian anterior (longitud = DonchianLength), cualquier posición corta se cierra inmediatamente. Un cierre por debajo de la banda inferior anterior cierra posiciones largas. El canal utiliza el valor completado anteriormente para reproducir el retraso iHighest/iLowest del EA.
  • Cambio de régimen: cada vez que la relación EMA se invierte, la estrategia liquida la exposición contraria antes de permitir nuevas operaciones en la nueva dirección.

gestión del dinero

  • Comienza con EntryVolume unidades por operación.
  • Cuando se produce una ganancia realizada mayor que BigWinThreshold mientras la estrategia es plana, el volumen aumenta en VolumeIncrement y el umbral se duplica (4 → 8 → 16, etc.). Si cualquiera de los parámetros se establece en 0, la regla de escala está deshabilitada.

Parámetros

  • Período EMA rápida: duración del filtro de tendencia rápida (predeterminado: 33).
  • Período EMA lenta: duración del filtro de tendencia lenta (predeterminado: 70).
  • Williams %R Period: búsqueda retrospectiva del oscilador Williams %R (predeterminado: 62).
  • Williams Nivel de venta: límite superior que debe cruzarse hacia abajo para señales cortas (predeterminado: −20).
  • Williams Nivel de compra: límite inferior que debe cruzarse hacia arriba para señales largas (predeterminado: −80).
  • CCI Período: retrospectiva del índice de canales de productos básicos (predeterminado: 26).
  • CCI Nivel de venta: valor mínimo de CCI requerido para permitir posiciones cortas (predeterminado: 101).
  • CCI Nivel de compra: valor máximo de CCI requerido para permitir posiciones largas (predeterminado: 99).
  • Donchian Longitud: número de velas completadas muestreadas para la salida de ruptura (predeterminado: 410).
  • Posiciones abiertas máximas: operaciones simultáneas máximas; la configuración clásica utiliza un contrato (predeterminado: 1).
  • Take Profit (pips): objetivo de beneficio medido en pasos de precio (predeterminado: 360).
  • Stop Loss (pips) – stop de protección medido en pasos de precio (predeterminado: 20).
  • Volumen de entrada: tamaño inicial del pedido (predeterminado: 0,01).
  • Umbral de grandes ganancias: se requieren ganancias obtenidas antes de aumentar el tamaño (predeterminado: 4.0).
  • Incremento de volumen: volumen adicional agregado después de superar el umbral (predeterminado: 0,01).
  • Tipo de vela: período de tiempo utilizado para todos los cálculos del indicador (predeterminado: velas por hora).

Notas adicionales

  • La conversión de pips se basa en el valor PriceStep. Los instrumentos sin un escalón de precio vuelven a tener un valor de pip unitario.
  • Los niveles de Donchian están retrasados intencionalmente una vela, por lo que la salida refleja la lógica shift=1 de las llamadas MetaTrader originales.
  • El escalado de volumen solo evalúa el PnL realizado mientras la posición es plana, lo que evita que las ganancias flotantes desencadenen falsos positivos.
  • Los objetos de etiqueta de la interfaz de usuario presentes en la fuente EA se omiten porque StockSharp visualiza el estado a través de gráficos y registros.
  • En este paquete sólo se proporciona la implementación de C#; no existe una versión de Python.
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;
	}

}