在 GitHub 上查看

Rabbit M2 策略

概述

Rabbit M2 是一套顺势交易策略,结合了动量震荡指标、唐奇安通道突破以及自适应仓位管理。原版 MetaTrader 5 智能交易系统由 Peter Byrom 编写,通过 1 小时时间框架上的指数移动平均线 (EMA) 切换做多或做空模式。在允许的方向下,策略等待威廉指标 (%R) 穿越关键阈值,并使用商品通道指数 (CCI) 进行确认后才开仓。每笔交易都设置固定距离的止损和止盈,当价格突破相反方向的唐奇安通道边界时也会立即平仓。若一次平仓收益超过预设的盈利阈值,基础下单手数会按步长增加,同时盈利阈值翻倍,从而复刻原策略的递进加仓逻辑。

指标与行情

  • 快速 EMA (40) 与慢速 EMA (80):基于 1 小时 K 线,用于判定趋势并在趋势反转时关闭仓位。
  • CCI (14):使用主时间框架的价格数据,确认市场是否处于超买或超卖状态。
  • Williams %R (50):同样基于主时间框架,当指标穿越 -20/-80 水平时给出触发信号。
  • 唐奇安通道 (100):基于主时间框架计算,若价格突破过去 100 根 K 线的最高或最低,触发强制平仓。
  • 固定止损与止盈:距离开仓价 50 点 (根据 3/5 位报价自动换算为标准点值)。

策略需要两路行情:主时间框架用于 CCI、Williams %R 与唐奇安通道,另一路为 1 小时行情供 EMA 趋势过滤使用。

交易规则

趋势控制

  1. 当 40 周期 EMA 跌破 80 周期 EMA 时,立即平掉所有多单,并且仅允许寻找做空机会。
  2. 当 40 周期 EMA 上穿 80 周期 EMA 时,立即平掉所有空单,并且仅允许寻找做多机会。

入场条件

  • 做空
    • Williams %R 跌破 -20,且上一根数值位于 -20 与 0 之间。
    • CCI 高于卖出阈值 (默认 101)。
    • 当前处于允许做空的模式,且净头寸未达到 MaxOpenPositions 限制。
  • 做多
    • Williams %R 上穿 -80,且上一根数值位于 -100 与 -80 之间。
    • CCI 低于买入阈值 (默认 99)。
    • 当前处于允许做多的模式,且净头寸未达到 MaxOpenPositions 限制。

每次信号触发时,策略会先平掉反向持仓,再按照当前基础手数建立新仓位。

出场条件

  1. 每根完成的 K 线都会检查止损与止盈:多单若最低价跌破止损或最高价触及止盈则平仓,空单规则相反。
  2. 无论是否命中止损/止盈,只要价格收盘突破前 100 根 K 线的最高点(空单)或最低点(多单),立即平仓。
  3. 趋势方向翻转(快 EMA 与慢 EMA 金叉/死叉)时,无条件清空持仓。

仓位管理

  • 基础下单量由 InitialVolume (默认 0.01) 指定,并会自动遵循交易所的最小手数、步长与上限要求。
  • 每当一次平仓的实际盈利大于 BigWinTarget,基础下单量增加 VolumeStep,同时盈利阈值翻倍,形成阶梯式放大利润的效果。
  • MaxOpenPositions 控制净持仓倍数。在 StockSharp 版本中采用净持仓模式,达到上限前不会再追加仓位。

参数

名称 默认值 说明
CciSellLevel 101 触发做空前 CCI 必须达到的最小数值。
CciBuyLevel 99 触发做多前 CCI 允许的最大数值。
CciPeriod 14 主时间框架上 CCI 的周期。
DonchianPeriod 100 唐奇安通道的回溯长度。
MaxOpenPositions 1 以基础手数计的最大允许净头寸。
BigWinTarget 1.50 触发加仓所需的实际盈利(账户货币)。
VolumeStep 0.01 每次达标后增加的基础手数步长。
WprPeriod 50 Williams %R 的周期。
FastEmaPeriod 40 1 小时趋势过滤的快速 EMA 周期。
SlowEmaPeriod 80 1 小时趋势过滤的慢速 EMA 周期。
TakeProfitPips 50 止盈距离(点)。
StopLossPips 50 止损距离(点)。
InitialVolume 0.01 启动时的基础下单量。
CandleType 15 分钟 K 线 主时间框架,供 CCI / Williams %R / 唐奇安通道计算。

实现说明

  • StockSharp 版本通过监测 K 线高低点来模拟 MT5 的止损与止盈,而非挂出带保护的委托。
  • 当标的价格精度为 3 位或 5 位小数时,会自动将最小报价步长乘以 10 来换算标准点值。
  • 策略依赖成交回报更新实时盈亏 (PnL) 来判断“大赢”事件,请确保成交回报能正确回传到策略中。
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;
	}
}