Открыть на GitHub

Стратегия Rabbit M2

Описание

Rabbit M2 — это советник Питера Байрома для MetaTrader 4, перенесённый на StockSharp. Алгоритм чередует «бычий» и «медвежий» режимы, которые определяются пересечением экспоненциальных средних на часовом таймфрейме. В активном режиме стратегия ждёт колебаний Williams %R и проверяет их показания с помощью индикатора CCI, после чего открывает рыночные сделки. Защитный блок повторяет оригинальный код: фиксированные стоп-лосс и тейк-профит выставляются на заданном расстоянии, а позиции принудительно закрываются, если цена пробивает противоположную границу канала Дончиана. Денежное управление увеличивает базовый объём после каждой крупной прибыли и удваивает порог для следующего повышения.

Рыночные данные и индикаторы

  • Основной таймфрейм (по умолчанию минутные свечи) используется для расчёта Williams %R, CCI и канала Дончиана.
  • Часовой таймфрейм служит для вычисления быстрой (40) и медленной (80) EMA, которые задают торговое направление.
  • Williams %R (50) отслеживает выход из зон -20/-80 и даёт импульсный сигнал.
  • CCI (14) подтверждает, что цена перекуплена или перепродана.
  • Канал Дончиана (100) формирует уровни для принудительного выхода по пробитию предыдущего диапазона.
  • Фиксированные стоп и профит пересчитываются из пунктов (по умолчанию 50) в абсолютную цену с учётом шага котировки и форекс-инструментов с 3 или 5 знаками после запятой.

Логика торговли

Управление режимами

  1. Если EMA(40) на часовиках опускается ниже EMA(80), все длинные позиции закрываются, и разрешается только работа в шорт.
  2. Если EMA(40) поднимается выше EMA(80), закрываются короткие позиции, и стратегия ищет только покупки.

Условия входа

  • Продажа возможна, когда:
    • Williams %R переходит из диапазона -20..0 в зону перепроданности (< -20).
    • Значение CCI превышает уровень CciSellLevel (по умолчанию 101).
    • Текущий шорт не превысил лимит MaxTrades (каждый сигнал добавляет один базовый объём).
  • Покупка выполняется, когда:
    • Williams %R выходит из зоны -100..-80 и поднимается выше -80.
    • CCI опускается ниже CciBuyLevel (по умолчанию 99).
    • Накопленная длинная позиция меньше ограничения MaxTrades.

Новые заявки увеличивают нетто-позицию. На платформе StockSharp позиции неттируются, поэтому повторные сигналы лишь наращивают объём до установленного предела.

Условия выхода

  1. Стоп и профит контролируются на каждой завершённой свече. При пробое уровня позиция закрывается рыночной заявкой.
  2. Независимо от стопов: лонг закрывается, если цена закрытия опускается ниже предыдущей нижней границы канала Дончиана; шорт — если закрытие выше верхней границы.
  3. Смена режима по EMA немедленно ликвидирует позиции, идущие против нового тренда.

Управление объёмом

  • Базовый объём стартует со значения InitialVolume (по умолчанию 0.01) и приводится к шагу и ограничениям инструмента.
  • После каждой реализованной прибыли больше BigWinTarget (15 денежных единиц) объём увеличивается на VolumeIncrement (0.01), а порог «крупного выигрыша» удваивается, повторяя поведение оригинального советника.
  • При выходе в ноль вспомогательные значения стопа и профита сбрасываются, чтобы избежать устаревших уровней.

Параметры

Параметр Значение по умолчанию Описание
CciSellLevel 101 Минимальный CCI для подтверждения продажи.
CciBuyLevel 99 Максимальный CCI для подтверждения покупки.
CciPeriod 14 Период индикатора CCI.
DonchianPeriod 100 Длина канала Дончиана для выходов.
MaxTrades 1 Максимальное количество базовых объёмов в нетто-позиции.
BigWinTarget 15 Прибыль, после которой увеличивается базовый объём.
VolumeIncrement 0.01 Прирост объёма после «крупного выигрыша».
WprPeriod 50 Период Williams %R.
FastEmaPeriod 40 Период быстрой EMA на часовом таймфрейме.
SlowEmaPeriod 80 Период медленной EMA на часовом таймфрейме.
TakeProfitPoints 50 Дистанция тейк-профита в пунктах.
StopLossPoints 50 Дистанция стоп-лосса в пунктах.
InitialVolume 0.01 Стартовый базовый объём.
CandleType Минутные свечи Основной таймфрейм для расчётов.

Особенности реализации

  • Стоп-лосс и тейк-профит контролируются внутри стратегии, что повторяет работу OrderSend с параметрами SL/TP в MetaTrader.
  • Изменение объёма зависит от данных о реализованной прибыли. Необходимо получать подтверждения сделок от брокерского адаптера StockSharp.
  • Метод CalculatePriceOffset корректирует шаг цены для инструментов с 3 и 5 знаками после запятой, чтобы соответствовать константе Point в MetaTrader.
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>
/// Port of the "Rabbit M2" MetaTrader expert advisor by Peter Byrom.
/// The strategy combines hourly EMA regime detection with Williams %R momentum
/// and Donchian channel exits on the primary timeframe.
/// </summary>
public class RabbitM2RegimeSwingStrategy : Strategy
{
	private static readonly DataType TrendCandleType = TimeSpan.FromHours(2).TimeFrame();

	private readonly StrategyParam<int> _cciSellLevel;
	private readonly StrategyParam<int> _cciBuyLevel;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<int> _donchianPeriod;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<decimal> _bigWinTarget;
	private readonly StrategyParam<decimal> _volumeIncrement;
	private readonly StrategyParam<int> _wprPeriod;
	private readonly StrategyParam<int> _fastEmaPeriod;
	private readonly StrategyParam<int> _slowEmaPeriod;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _baseVolume;
	private decimal _profitThreshold;
	private decimal _lastRealizedPnL;
	private decimal? _previousWpr;
	private decimal? _previousUpperBand;
	private decimal? _previousLowerBand;
	private bool _longRegimeEnabled;
	private bool _shortRegimeEnabled;
	private decimal _stopDistance;
	private decimal _takeDistance;
	private decimal _activeStop;
	private decimal _activeTake;

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

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

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

		_donchianPeriod = Param(nameof(DonchianPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Donchian Period", "Length of the Donchian channel used for exits", "Donchian")
			;

		_maxTrades = Param(nameof(MaxTrades), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Trades", "Maximum number of base-volume units that can be open", "Risk")
			;

		_bigWinTarget = Param(nameof(BigWinTarget), 15m)
			.SetGreaterThanZero()
			.SetDisplay("Big Win Target", "Profit needed before the volume increases", "Money Management")
			;

		_volumeIncrement = Param(nameof(VolumeIncrement), 0.01m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Increment", "How much to add 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")
			;

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

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance from entry price to the take profit", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance from entry price to the stop loss", "Risk")
			;

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

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Primary Candle Type", "Timeframe for Williams %R, CCI and Donchian calculations", "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>
	/// Lookback used for the Commodity Channel Index.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Donchian channel period that drives breakout exits.
	/// </summary>
	public int DonchianPeriod
	{
		get => _donchianPeriod.Value;
		set => _donchianPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of base-volume multiples allowed in the net position.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

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

	/// <summary>
	/// Volume increment added after a qualifying profit.
	/// </summary>
	public decimal VolumeIncrement
	{
		get => _volumeIncrement.Value;
		set => _volumeIncrement.Value = value;
	}

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

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

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

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Starting base volume used for each entry.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

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

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
	if (Security != null)
	{
	yield return (Security, CandleType);
	yield return (Security, TrendCandleType);
	}
	}

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

	_baseVolume = 0m;
	_profitThreshold = 0m;
	_lastRealizedPnL = 0m;
	_previousWpr = null;
	_previousUpperBand = null;
	_previousLowerBand = null;
	_longRegimeEnabled = false;
	_shortRegimeEnabled = false;
	_stopDistance = 0m;
	_takeDistance = 0m;
	_activeStop = 0m;
	_activeTake = 0m;
	}

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

	_baseVolume = InitialVolume;
	_profitThreshold = BigWinTarget;
	_lastRealizedPnL = PnL;
	_previousWpr = null;
	_previousUpperBand = null;
	_previousLowerBand = null;
	_longRegimeEnabled = false;
	_shortRegimeEnabled = false;
	_activeStop = 0m;
	_activeTake = 0m;

	_stopDistance = CalculatePriceOffset(StopLossPoints);
	_takeDistance = CalculatePriceOffset(TakeProfitPoints);

	NormalizeBaseVolume();

	var wpr = new WilliamsR { Length = WprPeriod };
	var cci = new CommodityChannelIndex { Length = CciPeriod };
	var donchian = new DonchianChannels { Length = DonchianPeriod };

	var emaFast = new EMA { Length = FastEmaPeriod };
	var emaSlow = new EMA { Length = SlowEmaPeriod };

	// The hourly subscription controls the trading regime and closes opposite positions when a cross happens.
	var trendSubscription = SubscribeCandles(TrendCandleType);
	trendSubscription
	.Bind(emaFast, emaSlow, ProcessTrend)
	.Start();

	// The primary subscription provides momentum signals and breakout exits.
	var primarySubscription = SubscribeCandles(CandleType);
	primarySubscription
	.BindEx(wpr, cci, donchian, ProcessPrimaryCandle)
	.Start();

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

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

	// Fast EMA below slow EMA activates the short regime and forces longs to exit.
	if (fastEma < slowEma)
	{
	_shortRegimeEnabled = true;
	_longRegimeEnabled = false;
	CloseLongPosition("Hourly trend turned bearish");
	}
	// Fast EMA above slow EMA activates the long regime and forces shorts to exit.
	else if (fastEma > slowEma)
	{
	_longRegimeEnabled = true;
	_shortRegimeEnabled = false;
	CloseShortPosition("Hourly trend turned bullish");
	}
	}

	private void ProcessPrimaryCandle(
	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 exit conditions before looking for new signals.
	HandleActivePosition(candle);

	var currentWpr = wprValue.ToDecimal();
	if (currentWpr == 0m)
	currentWpr = -1m;

	var previousWpr = _previousWpr;
	var currentCci = cciValue.ToDecimal();

	if (!IsFormedAndOnlineAndAllowTrading())
	{
	_previousWpr = currentWpr;
	_previousUpperBand = upperBand;
	_previousLowerBand = lowerBand;
	return;
	}

	if (previousWpr is null)
	{
	_previousWpr = currentWpr;
	_previousUpperBand = upperBand;
	_previousLowerBand = lowerBand;
	return;
	}

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

	if (_shortRegimeEnabled)
	TryOpenShort(candle, currentWpr, wprLag, currentCci);

	if (_longRegimeEnabled)
	TryOpenLong(candle, currentWpr, wprLag, currentCci);

	_previousWpr = currentWpr;
	_previousUpperBand = upperBand;
	_previousLowerBand = lowerBand;
	}

	private void HandleActivePosition(ICandleMessage candle)
	{
	if (Position > 0m)
	{
	// Long positions exit on take profit, stop loss or a Donchian breakout against the trade.
	if (_takeDistance > 0m && _activeTake > 0m && candle.HighPrice >= _activeTake)
	{
	CloseLongPosition("Take profit reached");
	}
	else if (_stopDistance > 0m && _activeStop > 0m && candle.LowPrice <= _activeStop)
	{
	CloseLongPosition("Stop loss reached");
	}
	else if (_previousLowerBand is decimal previousLower && candle.ClosePrice < previousLower)
	{
	CloseLongPosition("Closed below previous Donchian low");
	}
	}
	else if (Position < 0m)
	{
	// Short positions exit using mirrored conditions.
	if (_takeDistance > 0m && _activeTake > 0m && candle.LowPrice <= _activeTake)
	{
	CloseShortPosition("Take profit reached");
	}
	else if (_stopDistance > 0m && _activeStop > 0m && candle.HighPrice >= _activeStop)
	{
	CloseShortPosition("Stop loss reached");
	}
	else if (_previousUpperBand is decimal previousUpper && candle.ClosePrice > previousUpper)
	{
	CloseShortPosition("Closed above previous Donchian high");
	}
	}
	}

	private void TryOpenShort(ICandleMessage candle, decimal currentWpr, decimal previousWpr, decimal currentCci)
	{
	if (!(currentWpr < -20m && previousWpr > -20m && previousWpr < 0m && currentCci > CciSellLevel))
	return;

	if (_baseVolume <= 0m)
	return;

	// Net short exposure cannot exceed MaxTrades multiples of the base volume.
	var netVolume = Math.Abs(Position);
	var maxVolume = _baseVolume * MaxTrades;
	if (maxVolume <= 0m || netVolume >= maxVolume)
	return;

	var volume = Math.Min(_baseVolume, maxVolume - netVolume);
	volume = AlignVolume(volume);
	if (volume <= 0m)
	return;

	SellMarket(volume);
	_activeStop = _stopDistance > 0m ? candle.ClosePrice + _stopDistance : 0m;
	_activeTake = _takeDistance > 0m ? candle.ClosePrice - _takeDistance : 0m;
	}

	private void TryOpenLong(ICandleMessage candle, decimal currentWpr, decimal previousWpr, decimal currentCci)
	{
	if (!(currentWpr > -80m && previousWpr < -80m && previousWpr < 0m && currentCci < CciBuyLevel))
	return;

	if (_baseVolume <= 0m)
	return;

	var netVolume = Math.Abs(Position);
	var maxVolume = _baseVolume * MaxTrades;
	if (maxVolume <= 0m || netVolume >= maxVolume)
	return;

	var volume = Math.Min(_baseVolume, maxVolume - netVolume);
	volume = AlignVolume(volume);
	if (volume <= 0m)
	return;

	BuyMarket(volume);
	_activeStop = _stopDistance > 0m ? candle.ClosePrice - _stopDistance : 0m;
	_activeTake = _takeDistance > 0m ? candle.ClosePrice + _takeDistance : 0m;
	}

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

	SellMarket(volume);
	_activeStop = 0m;
	_activeTake = 0m;
	LogInfo($"Closing long position: {reason}.");
	}

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

	BuyMarket(volume);
	_activeStop = 0m;
	_activeTake = 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 trades with sufficiently large realized profits.
	if (realizedChange > _profitThreshold && VolumeIncrement > 0m)
	{
	_baseVolume += VolumeIncrement;
	NormalizeBaseVolume();

	if (_profitThreshold > 0m)
	_profitThreshold *= 2m;
	}

	if (Math.Abs(Position) == 0m)
	{
	_activeStop = 0m;
	_activeTake = 0m;
	}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
	base.OnPositionReceived(position);

	if (Position == 0m)
	{
	_activeStop = 0m;
	_activeTake = 0m;
	}
	}

	private void NormalizeBaseVolume()
	{
	if (_baseVolume <= 0m)
	{
	Volume = 0m;
	return;
	}

	_baseVolume = AlignVolume(_baseVolume);
	Volume = _baseVolume;
	}

	private decimal AlignVolume(decimal volume)
	{
	if (Security == null || volume <= 0m)
	return volume;

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

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

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

	return volume;
	}

	private decimal CalculatePriceOffset(int points)
	{
	if (points <= 0)
	return 0m;

	var step = Security?.PriceStep ?? 0m;
	if (step <= 0m)
	step = 0.0001m;

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

	return points * step;
	}
}