在 GitHub 上查看

Rabbit M2 策略

概述

Rabbit M2 是 Peter Byrom 为 MetaTrader 4 编写的专家顾问,现已移植到 StockSharp。策略通过比较小时级 40/80 周期指数移动平均线来判断市场处于多头还是空头状态。在允许交易的方向上,程序等待 Williams %R 摆脱关键区间并由 CCI 指标确认后,以市价单入场。保护模块完全复制原始 EA:为每笔交易设置固定距离的 止损/止盈,并在价格突破相反方向的 Donchian 通道时强制平仓。资金管理逻辑会在获得超额利润后增加基础 手数,并将下一次加仓所需的利润目标加倍。

数据源与指标

  • 主周期(默认 1 分钟 K 线)用于计算 Williams %R、CCI 以及 Donchian 通道。
  • 小时周期提供 40 与 80 周期 EMA,用于切换交易方向并关闭反向头寸。
  • Williams %R (50) 监控 -20/-80 阈值附近的动量突破。
  • CCI (14) 确认市场处于超买或超卖状态。
  • Donchian 通道 (100) 在突破前高/前低时触发保护性平仓。
  • 固定止损和止盈 根据点数(默认 50)以及品种最小报价单位换算为价格距离,并针对三位或五位小数 的外汇品种进行调整。

交易规则

趋势控制

  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. 每根完成的 K 线都会检查止损和止盈,一旦价格触及目标即平仓。
  2. 若多单收盘价跌破上一根 Donchian 下轨,或空单收盘价突破上一根上轨,也会强制离场。
  3. 小时级 EMA 发生反向交叉时,所有反向仓位立即平掉。

资金管理

  • 基础手数由 InitialVolume(默认 0.01)初始化,并自动匹配交易品种的交易步长、最小与最大手数限制。
  • 每当实现利润超过 BigWinTarget(默认 15 账户货币单位)时,基础手数增加 VolumeIncrement(默认 0.01), 同时把利润阈值翻倍,模拟原始 EA 的递进加仓机制。
  • 当仓位归零时,会清除内部记录的止损/止盈价格,避免残留数据影响后续交易。

参数

名称 默认值 说明
CciSellLevel 101 触发做空所需的最小 CCI 值。
CciBuyLevel 99 触发做多所需的最大 CCI 值。
CciPeriod 14 CCI 指标的计算周期。
DonchianPeriod 100 Donchian 通道的回溯长度。
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 1 分钟 K 线 主要数据周期。

实现说明

  • 止损和止盈在策略内部根据价格监控执行,而不是发送独立的保护单,以贴近原始 EA 的 OrderSend 行为。
  • 基础手数的调整依赖 StockSharp 返回的已实现盈亏信息,请确保适配器能够推送成交结果。
  • CalculatePriceOffset 方法会根据品种的小数位数调整点值,模拟 MetaTrader 中的 Point 常量。
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;
	}
}