在 GitHub 上查看

Rubber Bands Grid 策略

概述

  • 将 MetaTrader 4 专家顾问 RUBBERBANDS_2.mq4 转换到 StockSharp 高级 API。
  • 使用最优买卖价构建对称网格,不依赖蜡烛或指标。
  • 长短头寸分别记录,从而复制原始 EA 的对冲持仓方式。
  • 支持会话级收益/亏损控制以及与原参数一致的暂停和停止开关。

交易逻辑

  1. 订阅 SubscribeLevel1(),对每一次最优 bid/ask 的变化做出响应。
  2. _upperExtreme_lowerExtreme 保存自上次重置以来的最高与最低 Ask。若启用 UseInitialValues,则在启动时读取提供的极值,否则以首个 Ask 初始化。
  3. 当没有持仓且服务器时间进入新分钟(秒数为 0)时,同时发送一笔买入市价单和卖出市价单,模拟 MT4 中“每分钟入场一次”的触发器。
  4. Ask 比记录的高点高出 GridStepPoints 个点时发送新的卖单;Ask 比低点低出同样的点数时发送新的买单。触发后极值更新为当前 Ask,使得网格随价格延伸。
  5. MaxTrades 限制多空两侧的未平仓订单总数。
  6. 浮动盈亏基于当前 bid/ask 计算:多头使用 Bid 减去平均买入价,空头使用平均卖出价减去 Ask。PriceToMoney 在存在 PriceStep/StepPrice 时自动换算为账户货币。
  7. 当浮盈达到 SessionTakeProfitPerLot * OrderVolume 且开启 UseSessionTakeProfit 时,策略平掉全部仓位;当浮亏跌破 -SessionStopLossPerLot * OrderVolume 且开启 UseSessionStopLoss 时同样执行全平。
  8. CloseNow 在启动时立即清仓,QuiesceMode 在空仓状态下保持静默,StopNow 禁止新的入场但不会干预已有持仓。

参数说明

参数 说明
OrderVolume 每次市场委托的交易量(对应 MT4 的 Lots)。
MaxTrades 多空总持仓数量上限(对应 maxcount)。
GridStepPoints 网格间距,单位为价格点(对应 pipstep)。
QuiesceMode 空仓时保持静默(quiescenow)。
TriggerImmediateEntries 启动后立即同时买入和卖出(donow)。
StopNow 暂停新的交易信号(stopnow)。
CloseNow 启动即平仓(closenow)。
UseSessionTakeProfitSessionTakeProfitPerLot 会话级浮盈目标,按每标准手计算。
UseSessionStopLossSessionStopLossPerLot 会话级浮亏阈值,按每标准手计算。
UseInitialValuesInitialMaxInitialMin 重启时恢复上一次的极值(useinvaluesinmaxinmin)。

实现细节

  • 按仓位方向分别维护体量与均价,所有字段使用制表符缩进以符合仓库要求。
  • 通过 _activeBuyOrder_activeSellOrder 防止重复发送市场委托。
  • OnOwnTradeReceived 中更新多空平均价并计算浮动盈亏,为风险阈值提供实时数据。
  • TryCloseAll() 模拟 MT4 的 close1by1():连续提交反向单直到多空均归零,然后将极值重置为最新 Ask。
  • 仅使用高级 API:Level1 订阅与 BuyMarket/SellMarket,未直接访问指标或底层集合。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Rubber Bands Grid: Mean reversion grid using SMA+ATR bands.
/// </summary>
public class RubberBandsGridStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _entryPrice;
	private int _gridCount;

	public RubberBandsGridStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_smaLength = Param(nameof(SmaLength), 20)
			.SetDisplay("SMA Length", "SMA period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int SmaLength
	{
		get => _smaLength.Value;
		set => _smaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_entryPrice = 0;
		_gridCount = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_entryPrice = 0;
		_gridCount = 0;

		var sma = new SimpleMovingAverage { Length = SmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(sma, atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, sma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal smaVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (atrVal <= 0)
			return;

		var close = candle.ClosePrice;
		var lower = smaVal - atrVal * 2m;
		var upper = smaVal + atrVal * 2m;

		if (Position > 0)
		{
			if (close >= smaVal)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close <= _entryPrice - atrVal * 4m)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close <= _entryPrice - atrVal)
			{
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				BuyMarket();
			}
		}
		else if (Position < 0)
		{
			if (close <= smaVal)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close >= _entryPrice + atrVal * 4m)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close >= _entryPrice + atrVal)
			{
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				SellMarket();
			}
		}

		if (Position == 0)
		{
			if (close <= lower)
			{
				_entryPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (close >= upper)
			{
				_entryPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}
	}
}