在 GitHub 上查看

Rabbit M3

概述

Rabbit M3 是 MetaTrader 4 智能交易系统 RabbitM3(又名“Petes Party Trick”)的移植版本。策略通过一对一小时指数移动平均线判断市场处于只做多还是只做空的状态;当快线在慢线之上时只寻找多头信号,在其之下时只允许空头信号。入场需要 Williams %R 动能穿越配合 CCI 水平过滤,而一个超长的唐奇安通道用于侦测价格是否突破原有趋势。策略还保留了原程序在大额盈利后提升下次手数的规则。

策略逻辑

趋势状态过滤

  • 当快 EMA 收盘价低于慢 EMA 时,立即平掉现有多单,并且之后只会寻找空头机会。
  • 当快 EMA 收盘价高于慢 EMA 时,立即平掉现有空单,并且之后只会寻找多头机会。
  • 若两条 EMA 相等,则保持上一根柱子的状态,与原始 EA 仅在严格大小关系变化时才切换模式的做法一致。

入场规则

  • 做空
    • 当前状态必须为只做空(快 EMA < 慢 EMA)。
    • Williams %R(周期 = WilliamsPeriod)最新一根K线向下穿越 WilliamsSellLevel,且前一根数值仍小于0。
    • CCI(周期 = CciPeriod)必须大于等于 CciSellLevel
    • 当前净仓位必须为空;策略最多只持有 MaxOpenPositions 笔仓位,默认以 EntryVolume 手数市价开仓。
  • 做多
    • 当前状态必须为只做多(快 EMA > 慢 EMA)。
    • Williams %R 向上穿越 WilliamsBuyLevel,且前一根数值仍小于0。
    • CCI 必须小于等于 CciBuyLevel
    • 入场前净仓位必须为空。

出场规则

  • 固定止盈止损StopLossPipsTakeProfitPips 会根据标的的价格最小变动单位转换成价格距离,设置为 0 即代表禁用该保护。
  • 唐奇安突破 – 若收盘价高于上一根唐奇安上轨(周期 = DonchianLength),则立即平掉空单;收盘价低于上一根下轨时立即平掉多单。使用上一根轨道值以复现 EA 中 shift=1 的调用方式。
  • 趋势反转 – 每当 EMA 关系翻转,策略会先平掉反向仓位,再允许按照新的方向寻找信号。

资金管理

  • 初始每次开仓数量为 EntryVolume
  • 当平仓实现盈利超过 BigWinThreshold 且当前没有持仓时,下一次下单量会增加 VolumeIncrement,同时阈值翻倍(4 → 8 → 16 ……)。如果任一参数设为 0 则关闭此手数递增机制。

参数

  • Fast EMA Period – 快速趋势过滤器周期(默认 33)。
  • Slow EMA Period – 慢速趋势过滤器周期(默认 70)。
  • Williams %R Period – Williams %R 动能指标周期(默认 62)。
  • Williams Sell Level – 触发空头信号的向下穿越水平(默认 −20)。
  • Williams Buy Level – 触发多头信号的向上穿越水平(默认 −80)。
  • CCI Period – 商品通道指数周期(默认 26)。
  • CCI Sell Level – 允许做空的最低 CCI 值(默认 101)。
  • CCI Buy Level – 允许做多的最高 CCI 值(默认 99)。
  • Donchian Length – 唐奇安通道取值的历史长度(默认 410)。
  • Max Open Positions – 同时持仓的最大数量,原策略为 1(默认 1)。
  • Take Profit (pips) – 以点数表示的止盈距离(默认 360)。
  • Stop Loss (pips) – 以点数表示的止损距离(默认 20)。
  • Entry Volume – 初始下单手数(默认 0.01)。
  • Big Win Threshold – 激活加仓机制所需的单次实现盈利(默认 4.0)。
  • Volume Increment – 达到阈值后增加的手数(默认 0.01)。
  • Candle Type – 指标使用的K线周期(默认 1 小时)。

补充说明

  • 点值换算依赖于品种的 PriceStep,若未提供则退化为 1 个价格单位。
  • 唐奇安通道故意滞后一根K线,以保持与原始 iHighest/iLowest 的偏移一致。
  • 手数递增逻辑只在仓位清零、且产生实现盈亏时评估,避免浮动盈利带来误判。
  • 原 EA 中用于显示信息的图形对象未移植,在 StockSharp 中可以通过图表和日志查看状态。
  • 本目录仅提供 C# 版本,没有 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;
	}

}