在 GitHub 上查看
Rubber Bands Grid 策略
概述
- 将 MetaTrader 4 专家顾问 RUBBERBANDS_2.mq4 转换到 StockSharp 高级 API。
- 使用最优买卖价构建对称网格,不依赖蜡烛或指标。
- 长短头寸分别记录,从而复制原始 EA 的对冲持仓方式。
- 支持会话级收益/亏损控制以及与原参数一致的暂停和停止开关。
交易逻辑
- 订阅
SubscribeLevel1(),对每一次最优 bid/ask 的变化做出响应。
_upperExtreme 与 _lowerExtreme 保存自上次重置以来的最高与最低 Ask。若启用 UseInitialValues,则在启动时读取提供的极值,否则以首个 Ask 初始化。
- 当没有持仓且服务器时间进入新分钟(秒数为 0)时,同时发送一笔买入市价单和卖出市价单,模拟 MT4 中“每分钟入场一次”的触发器。
- Ask 比记录的高点高出
GridStepPoints 个点时发送新的卖单;Ask 比低点低出同样的点数时发送新的买单。触发后极值更新为当前 Ask,使得网格随价格延伸。
MaxTrades 限制多空两侧的未平仓订单总数。
- 浮动盈亏基于当前 bid/ask 计算:多头使用 Bid 减去平均买入价,空头使用平均卖出价减去 Ask。
PriceToMoney 在存在 PriceStep/StepPrice 时自动换算为账户货币。
- 当浮盈达到
SessionTakeProfitPerLot * OrderVolume 且开启 UseSessionTakeProfit 时,策略平掉全部仓位;当浮亏跌破 -SessionStopLossPerLot * OrderVolume 且开启 UseSessionStopLoss 时同样执行全平。
CloseNow 在启动时立即清仓,QuiesceMode 在空仓状态下保持静默,StopNow 禁止新的入场但不会干预已有持仓。
参数说明
| 参数 |
说明 |
OrderVolume |
每次市场委托的交易量(对应 MT4 的 Lots)。 |
MaxTrades |
多空总持仓数量上限(对应 maxcount)。 |
GridStepPoints |
网格间距,单位为价格点(对应 pipstep)。 |
QuiesceMode |
空仓时保持静默(quiescenow)。 |
TriggerImmediateEntries |
启动后立即同时买入和卖出(donow)。 |
StopNow |
暂停新的交易信号(stopnow)。 |
CloseNow |
启动即平仓(closenow)。 |
UseSessionTakeProfit、SessionTakeProfitPerLot |
会话级浮盈目标,按每标准手计算。 |
UseSessionStopLoss、SessionStopLossPerLot |
会话级浮亏阈值,按每标准手计算。 |
UseInitialValues、InitialMax、InitialMin |
重启时恢复上一次的极值(useinvalues、inmax、inmin)。 |
实现细节
- 按仓位方向分别维护体量与均价,所有字段使用制表符缩进以符合仓库要求。
- 通过
_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();
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class rubber_bands_grid_strategy(Strategy):
def __init__(self):
super(rubber_bands_grid_strategy, self).__init__()
self._sma_length = self.Param("SmaLength", 20).SetDisplay("SMA Length", "SMA period", "Indicators")
self._atr_length = self.Param("AtrLength", 14).SetDisplay("ATR Length", "ATR period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(rubber_bands_grid_strategy, self).OnReseted()
self._entry_price = 0
self._grid_count = 0
def OnStarted2(self, time):
super(rubber_bands_grid_strategy, self).OnStarted2(time)
self._entry_price = 0
self._grid_count = 0
sma = SimpleMovingAverage()
sma.Length = self._sma_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(sma, atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def OnProcess(self, candle, sma_val, atr_val):
if candle.State != CandleStates.Finished:
return
if atr_val <= 0:
return
close = float(candle.ClosePrice)
lower = sma_val - atr_val * 2.0
upper = sma_val + atr_val * 2.0
if self.Position > 0:
if close >= sma_val:
self.SellMarket()
self._entry_price = 0
self._grid_count = 0
elif close <= self._entry_price - atr_val * 4.0:
self.SellMarket()
self._entry_price = 0
self._grid_count = 0
elif self._grid_count < 3 and close <= self._entry_price - atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.BuyMarket()
elif self.Position < 0:
if close <= sma_val:
self.BuyMarket()
self._entry_price = 0
self._grid_count = 0
elif close >= self._entry_price + atr_val * 4.0:
self.BuyMarket()
self._entry_price = 0
self._grid_count = 0
elif self._grid_count < 3 and close >= self._entry_price + atr_val:
self._entry_price = (self._entry_price + close) / 2.0
self._grid_count += 1
self.SellMarket()
if self.Position == 0:
if close <= lower:
self._entry_price = close
self._grid_count = 0
self.BuyMarket()
elif close >= upper:
self._entry_price = close
self._grid_count = 0
self.SellMarket()
def CreateClone(self):
return rubber_bands_grid_strategy()