Ver en GitHub

Rabbit3 Strategy

Overview

  • Conversion of the original MetaTrader 5 expert advisor Rabbit3 (barabashkakvn's edition).
  • Implements the logic in the StockSharp high-level API with candle subscriptions and indicator bindings.
  • Focuses on a dual confirmation between Williams %R and the Commodity Channel Index (CCI) before stacking positions.
  • Adds dynamic position sizing: profits beyond a cash threshold increase the order volume for the next signal.

Trading Logic

Entry conditions

  1. Long
    • Current and previous closed candles report Williams %R below WilliamsOversold (default -80).
    • CCI value is below CciBuyLevel (default -80).
    • Current net position is non-negative and adding another position keeps the exposure within MaxPositions × BaseVolume (boosted volume is used when active).
  2. Short
    • Current and previous closed candles report Williams %R above WilliamsOverbought (default -20).
    • CCI value is above CciSellLevel (default 80).
    • Current net position is non-positive and the new order remains within the configured stacking limit.

Exit and risk control

  • Protective stop-loss and take-profit orders are registered automatically through StartProtection.
  • The distances are expressed in "adjusted points": when the instrument uses 3 or 5 decimals the strategy multiplies the price step by 10 to emulate MetaTrader pip arithmetic before applying StopLossPips and TakeProfitPips.
  • No additional manual exit rules are required; protective orders close the trades.

Position sizing

  • BaseVolume defines the initial trade size (default 0.01).
  • After each trade closes, the realized PnL delta is compared with ProfitThreshold (default 4 monetary units).
  • If the delta is strictly greater, the next signal uses BaseVolume × VolumeMultiplier (default 1.6). Otherwise the size resets back to BaseVolume.
  • The current volume is also exposed through the strategy Volume property for UI feedback.

Indicators and visualization

  • Williams %R, CCI, a fast EMA (FastEmaPeriod) and a slow EMA (SlowEmaPeriod) are bound to the candle feed for monitoring and charting.
  • A chart area is created automatically, plotting candles, indicators and executed trades.

Parameters

Name Default Description
CandleType 1h time frame Data type for candle subscription.
CciPeriod 15 Length of the Commodity Channel Index.
CciBuyLevel -80 CCI threshold that allows long entries.
CciSellLevel 80 CCI threshold that allows short entries.
WilliamsPeriod 62 Lookback period for Williams %R.
WilliamsOversold -80 Oversold threshold used for long confirmation.
WilliamsOverbought -20 Overbought threshold used for short confirmation.
FastEmaPeriod 17 Fast EMA plotted for trend context.
SlowEmaPeriod 30 Slow EMA plotted for trend context.
MaxPositions 2 Maximum number of stacked entries per direction.
ProfitThreshold 4 Realized profit required to boost the next order size (monetary units).
BaseVolume 0.01 Base order volume.
VolumeMultiplier 1.6 Multiplier applied when the boost condition is met.
StopLossPips 45 Stop-loss distance in adjusted points.
TakeProfitPips 110 Take-profit distance in adjusted points.

Notes

  • The strategy operates on net positions. Unlike the hedging-friendly MQL version, longs and shorts are not held simultaneously; signals in the opposite direction are ignored until the current exposure is closed by protective orders.
  • MaxPositions works as a cap on the aggregate position (base volume multiplied by the stacking factor). Adjust it carefully when changing the base or boosted volumes.
  • Volume tolerance uses half of the instrument volume step to absorb minor rounding differences when checking the stacking cap.
  • Python translation is not included yet and can be added later if needed.
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;


public class Rabbit3Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciBuyLevel;
	private readonly StrategyParam<decimal> _cciSellLevel;
	private readonly StrategyParam<int> _williamsPeriod;
	private readonly StrategyParam<decimal> _williamsOversold;
	private readonly StrategyParam<decimal> _williamsOverbought;
	private readonly StrategyParam<int> _fastEmaPeriod;
	private readonly StrategyParam<int> _slowEmaPeriod;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _profitThreshold;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

	private WilliamsR _williams;
	private CommodityChannelIndex _cci;
	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	
	// Store last Williams %R value to require two-bar confirmation.
	private decimal _previousWilliams;
	private bool _hasPrevWilliams;
	// Track whether the boosted volume should be used for the next order.
	private bool _useBoost;
	// Remember realized PnL to measure the delta after each closed trade.
	private decimal _lastRealizedPnL;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	public decimal CciBuyLevel
	{
		get => _cciBuyLevel.Value;
		set => _cciBuyLevel.Value = value;
	}

	public decimal CciSellLevel
	{
		get => _cciSellLevel.Value;
		set => _cciSellLevel.Value = value;
	}

	public int WilliamsPeriod
	{
		get => _williamsPeriod.Value;
		set => _williamsPeriod.Value = value;
	}

	public decimal WilliamsOversold
	{
		get => _williamsOversold.Value;
		set => _williamsOversold.Value = value;
	}

	public decimal WilliamsOverbought
	{
		get => _williamsOverbought.Value;
		set => _williamsOverbought.Value = value;
	}

	public int FastEmaPeriod
	{
		get => _fastEmaPeriod.Value;
		set => _fastEmaPeriod.Value = value;
	}

	public int SlowEmaPeriod
	{
		get => _slowEmaPeriod.Value;
		set => _slowEmaPeriod.Value = value;
	}

	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	public decimal ProfitThreshold
	{
		get => _profitThreshold.Value;
		set => _profitThreshold.Value = value;
	}

	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public Rabbit3Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for signals", "General");

		_cciPeriod = Param(nameof(CciPeriod), 15)
			.SetDisplay("CCI Period", "Commodity Channel Index length", "Indicators")
			.SetGreaterThanZero();

		_cciBuyLevel = Param(nameof(CciBuyLevel), -80m)
			.SetDisplay("CCI Buy Level", "CCI threshold to allow long entries", "Signals");

		_cciSellLevel = Param(nameof(CciSellLevel), 80m)
			.SetDisplay("CCI Sell Level", "CCI threshold to allow short entries", "Signals");

		_williamsPeriod = Param(nameof(WilliamsPeriod), 62)
			.SetDisplay("Williams %R Period", "Williams %R lookback", "Indicators")
			.SetGreaterThanZero();

		_williamsOversold = Param(nameof(WilliamsOversold), -80m)
			.SetDisplay("Williams Oversold", "Oversold threshold for confirmation", "Signals");

		_williamsOverbought = Param(nameof(WilliamsOverbought), -20m)
			.SetDisplay("Williams Overbought", "Overbought threshold for confirmation", "Signals");

		_fastEmaPeriod = Param(nameof(FastEmaPeriod), 17)
			.SetDisplay("Fast EMA Period", "Fast EMA plotted for context", "Indicators")
			.SetGreaterThanZero();

		_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 30)
			.SetDisplay("Slow EMA Period", "Slow EMA plotted for context", "Indicators")
			.SetGreaterThanZero();

		_maxPositions = Param(nameof(MaxPositions), 2)
			.SetDisplay("Max Positions", "Maximum stacked entries per direction", "Risk")
			.SetGreaterThanZero();

		_profitThreshold = Param(nameof(ProfitThreshold), 4m)
			.SetDisplay("Profit Threshold", "Realized profit that boosts volume", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.01m)
			.SetDisplay("Base Volume", "Initial trade volume", "Risk")
			.SetGreaterThanZero();

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.6m)
			.SetDisplay("Volume Multiplier", "Factor applied after profitable trades", "Risk")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 45)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in adjusted points", "Risk")
			.SetGreaterThanZero();

		_takeProfitPips = Param(nameof(TakeProfitPips), 110)
			.SetDisplay("Take Profit (pips)", "Target distance in adjusted points", "Risk")
			.SetGreaterThanZero();
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_previousWilliams = 0m;
		_hasPrevWilliams = false;
		_useBoost = false;
		_lastRealizedPnL = 0m;
	}

        protected override void OnStarted2(DateTime time)
        {
                base.OnStarted2(time);

                // Reset dynamic sizing state and expose the starting volume to UI.
                _useBoost = false;
                Volume = BaseVolume;
                _lastRealizedPnL = PnL;

                // Initialize indicators that will be bound to the candle feed.
                _williams = new WilliamsR { Length = WilliamsPeriod };
                _cci = new CommodityChannelIndex { Length = CciPeriod };
                _fastEma = new EMA { Length = FastEmaPeriod };
                _slowEma = new EMA { Length = SlowEmaPeriod };

                // Subscribe to candles and bind indicators plus the processing callback.
                var subscription = SubscribeCandles(CandleType);
                subscription.Bind(_williams, _cci, _fastEma, _slowEma, ProcessCandle).Start();

                var area = CreateChartArea();
                if (area != null)
                {
                        DrawCandles(area, subscription);
                        DrawIndicator(area, _williams);
                        DrawIndicator(area, _cci);
                        DrawIndicator(area, _fastEma);
                        DrawIndicator(area, _slowEma);
                        DrawOwnTrades(area);
                }

                var point = GetAdjustedPoint();
                var takeDistance = TakeProfitPips * point;
                var stopDistance = StopLossPips * point;

                // Register protective orders using MetaTrader-like pip distances.
                StartProtection(
                        new Unit(stopDistance, UnitTypes.Absolute),
                        new Unit(takeDistance, UnitTypes.Absolute));
        }

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

                // Update the current volume based on realized profit or loss.
                UpdateVolumeIfNeeded();

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

                if (!_hasPrevWilliams)
                {
                        _previousWilliams = williamsValue;
                        _hasPrevWilliams = true;
                        return;
                }

                // removed IFOAAT for backtesting

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

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

                // Require Williams %R confirmation on two consecutive closed candles and CCI agreement.
                var longSignal = williamsValue < WilliamsOversold
                        && cciValue < CciBuyLevel
                        && fastEmaValue > slowEmaValue
                        && CanEnterLong();

                var shortSignal = williamsValue > WilliamsOverbought
                        && cciValue > CciSellLevel
                        && fastEmaValue < slowEmaValue
                        && CanEnterShort();

                if (longSignal)
                {
                        // Stack another long position using the dynamically selected volume.
                        BuyMarket(Volume);
                }
                else if (shortSignal)
                {
                        // Stack another short position using the dynamically selected volume.
                        SellMarket(Volume);
                }

                _previousWilliams = williamsValue;
        }

        private void UpdateVolumeIfNeeded()
        {
                var realizedPnL = PnL;

                if (realizedPnL != _lastRealizedPnL)
                {
                        var delta = realizedPnL - _lastRealizedPnL;
                        // Boost the next order after reaching the desired profit, otherwise revert to base volume.
                        _useBoost = delta > ProfitThreshold;
                        _lastRealizedPnL = realizedPnL;
                }

                Volume = GetTradeVolume();
        }

        private bool CanEnterLong()
        {
                if (Position < 0m)
                        return false;

                // Limit the number of stacked long entries according to MaxPositions.
                var tradeVolume = GetTradeVolume();
                var targetVolume = Position + tradeVolume;
                var maxVolume = MaxPositions * tradeVolume;
                return targetVolume <= maxVolume + GetVolumeTolerance();
        }

        private bool CanEnterShort()
        {
                if (Position > 0m)
                        return false;

                // Limit stacked shorts in the same way as longs.
                var tradeVolume = GetTradeVolume();
                var targetVolume = Math.Abs(Position - tradeVolume);
                var maxVolume = MaxPositions * tradeVolume;
                return targetVolume <= maxVolume + GetVolumeTolerance();
        }

	private decimal GetTradeVolume()
	{
		var multiplier = _useBoost ? VolumeMultiplier : 1m;
		return BaseVolume * multiplier;
	}

	private decimal GetAdjustedPoint()
	{
		var step = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
		return step * adjust;
	}

	private decimal GetVolumeTolerance()
	{
		var step = Security?.VolumeStep;
		if (step == null || step == 0m)
			return 0.00000001m;
		return step.Value / 2m;
	}
}