在 GitHub 上查看

Rabbit3 策略

概述

  • 将原始 MetaTrader 5 专家顾问 Rabbit3 (barabashkakvn's edition) 转换为 StockSharp 实现。
  • 使用高层 API:订阅蜡烛数据、绑定指标并在图表上绘制结果。
  • 通过 Williams %R 与 CCI 的双重确认过滤信号,仅在连续两根已完成蜡烛满足条件时加仓。
  • 添加动态仓位控制:若上一笔交易的实现盈亏增量大于现金阈值,则下一次信号使用放大后的下单量。

交易逻辑

入场条件

  1. 做多
    • 当前与上一根完成蜡烛的 Williams %R 均低于 WilliamsOversold(默认 -80)。
    • CCI 低于 CciBuyLevel(默认 -80)。
    • 当前净持仓≥0,且加入新的交易后仍低于 MaxPositions × BaseVolume 的上限(若启用放大,则使用放大后的下单量)。
  2. 做空
    • 当前与上一根完成蜡烛的 Williams %R 均高于 WilliamsOverbought(默认 -20)。
    • CCI 高于 CciSellLevel(默认 80)。
    • 当前净持仓≤0,且新的加仓不会超出设定的堆叠上限。

离场与风控

  • 通过 StartProtection 自动挂出止损与止盈订单。
  • 距离以“调整后的点数”计算:若合约价格保留 3 或 5 位小数,则在应用 StopLossPipsTakeProfitPips 前先将价格步长乘以 10,从而模拟 MetaTrader 的 pip 计算方式。
  • 无需额外的手动离场条件,保护性订单会负责平仓。

仓位管理

  • BaseVolume 为初始下单量(默认 0.01)。
  • 每次平仓后比较新的实现盈亏与上一笔的差值。
  • 当差值大于 ProfitThreshold(默认 4 货币单位)时,下一次下单量使用 BaseVolume × VolumeMultiplier(默认 1.6)。否则恢复到基础仓位。
  • 当前的下单量同步写入策略的 Volume 属性,方便在界面中查看。

指标与可视化

  • 绑定 Williams %R、CCI 以及快慢 EMA(FastEmaPeriodSlowEmaPeriod),既可驱动交易逻辑又可在图表上展示。
  • 自动创建图表区域,绘制蜡烛、指标轨迹以及实际成交。

参数

参数 默认值 说明
CandleType 1 小时 订阅的蜡烛数据类型。
CciPeriod 15 CCI 计算周期。
CciBuyLevel -80 多头 CCI 阈值。
CciSellLevel 80 空头 CCI 阈值。
WilliamsPeriod 62 Williams %R 回溯长度。
WilliamsOversold -80 多头确认所用的超卖阈值。
WilliamsOverbought -20 空头确认所用的超买阈值。
FastEmaPeriod 17 快速 EMA,用于趋势背景。
SlowEmaPeriod 30 慢速 EMA,用于趋势背景。
MaxPositions 2 同方向最多允许的加仓次数。
ProfitThreshold 4 触发加仓倍数所需的实现盈亏增量(货币单位)。
BaseVolume 0.01 基础下单量。
VolumeMultiplier 1.6 盈利后使用的仓位倍数。
StopLossPips 45 调整点数中的止损距离。
TakeProfitPips 110 调整点数中的止盈距离。

备注

  • 策略以净持仓模式运行,与原 MQL 版本不同,不会同时持有多空仓位。若存在反向信号且尚未平仓,将忽略该信号直至保护性订单完成出场。
  • MaxPositions 限制总仓位大小(考虑当前的基础或放大仓位)。调整 BaseVolumeVolumeMultiplier 时应同步审视该限制。
  • 堆叠检查时允许半个成交量步长的误差,以避免因四舍五入导致的拒单。
  • 目前未提供 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;


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;
	}
}