在 GitHub 上查看
Rubberbands 3 策略
本策略是 MetaTrader 4 专家顾问 RUBBERBANDS_3 的 StockSharp 版本。算法跟踪价格的最高值与最低值,当价格按设定的点数向外扩张时逐步加仓,并在出现指定幅度的回撤时一次性平掉整组仓位。如果触发了反向信号,策略会在平仓后尝试在相反方向重新建立仓位,同时监控本交易日的累计盈亏。
提示: StockSharp 使用净持仓模式。原始的 MT4 脚本可以同时持有多笔多头和空头,而此移植版本会在开仓反向之前先把当前方向的仓位全部平掉,从而保持整体行为一致。
交易逻辑
- 启动时记录当前收盘价作为初始的最高价和最低价,或使用手工指定的
InitialMax、InitialMin。
- 当价格较最高价上涨
PipStep 个点时,以 OrderVolume 的数量买入,并把最高价更新为最新值。
- 当价格较最低价下跌
PipStep 个点时,以 OrderVolume 的数量卖出,并把最低价更新为最新值。
- 如果行情出现
BackStep 个点的回撤,则立即平掉当前方向的全部仓位,并准备在另一方向重新开始建仓,前提是原有仓位已经全部退出。
- 同时跟踪累计盈亏:当已实现利润加未实现利润达到
SessionTakeProfit × OrderVolume 时,关闭整场交易;如果在反向过程中浮动亏损超过 SessionStopLoss × OrderVolume,同样触发强制平仓。
QuiesceNow 让策略在空仓时保持静默,StopNow 会暂停所有逻辑,CloseNow 请求立即平仓。
策略基于 CandleType 指定的收盘完成的 K 线进行决策,默认使用 1 分钟周期,与原始 EA 每分钟检查一次的行为一致。
参数
| 参数 |
说明 |
默认值 |
OrderVolume |
每次市场订单的基础手数。 |
0.02 |
MaxOrders |
单一方向允许同时持有的最多仓位数。 |
10 |
PipStep |
触发加仓的点数距离。 |
100 |
BackStep |
触发整组平仓和准备反向的回撤距离。 |
20 |
QuiesceNow |
为空仓且为 true 时禁止重新开仓。 |
false |
DoNow |
启动后立即打开第一笔多单。 |
false |
StopNow |
暂停所有决策逻辑,保留已有仓位。 |
false |
CloseNow |
请求尽快平掉所有仓位。 |
false |
UseSessionTakeProfit |
启用累计盈利目标。 |
true |
SessionTakeProfit |
以账户货币表示的单手盈利目标。 |
2000 |
UseSessionStopLoss |
启用累计亏损限制。 |
true |
SessionStopLoss |
在反向时允许的最大亏损(按单手计算)。 |
4000 |
UseInitialValues |
重新启动时是否使用手工输入的极值。 |
false |
InitialMax |
当 UseInitialValues 为 true 时使用的最高价。 |
0 |
InitialMin |
当 UseInitialValues 为 true 时使用的最低价。 |
0 |
CandleType |
用于计算的 K 线类型,默认 1 分钟。 |
TimeFrame(1m) |
会话管理
- 利润累计: 每次整组平仓后,把实现盈亏加入
_realizedProfit,未实现盈亏按剩余仓位的加权平均价即时计算。
- 会话止盈: 达到
SessionTakeProfit 后,策略会关闭所有仓位并重置最高价、最低价。
- 会话止损: 反向过程中若浮亏超过
SessionStopLoss,立刻平仓并重新开始下一轮。
使用建议
- 点值换算依赖
Security.PriceStep,如果该字段为空会退回使用 0.0001。
- 由于净持仓机制,策略会先平仓再在反向方向开仓,因此报表可能与原版的对冲模式存在差异。
DoNow 仅影响第一笔交易,后续加仓完全遵循价格突破逻辑。
- 若需要在平仓后暂时停用策略,可将
QuiesceNow 设置为 true。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Rubberbands 3: Grid expansion strategy using SMA+ATR bands.
/// Enters at band extremes, adds on continuation, exits at mean.
/// </summary>
public class Rubberbands3Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _entryPrice;
private int _gridCount;
public Rubberbands3Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).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;
}
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 upper = smaVal + atrVal * 1.5m;
var lower = smaVal - atrVal * 1.5m;
if (Position > 0)
{
if (close >= smaVal)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (close <= _entryPrice - atrVal * 5m)
{
SellMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (_gridCount < 4 && close <= _entryPrice - atrVal * 0.8m)
{
_entryPrice = (_entryPrice + close) / 2m;
_gridCount++;
BuyMarket();
}
}
else if (Position < 0)
{
if (close <= smaVal)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (close >= _entryPrice + atrVal * 5m)
{
BuyMarket();
_entryPrice = 0;
_gridCount = 0;
}
else if (_gridCount < 4 && close >= _entryPrice + atrVal * 0.8m)
{
_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 rubberbands_3_strategy(Strategy):
def __init__(self):
super(rubberbands_3_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.FromMinutes(15))).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(rubberbands_3_strategy, self).OnReseted()
self._entry_price = 0
self._grid_count = 0
def OnStarted2(self, time):
super(rubberbands_3_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)
upper = sma_val + atr_val * 1.5
lower = sma_val - atr_val * 1.5
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 * 5.0:
self.SellMarket()
self._entry_price = 0
self._grid_count = 0
elif self._grid_count < 4 and close <= self._entry_price - atr_val * 0.8:
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 * 5.0:
self.BuyMarket()
self._entry_price = 0
self._grid_count = 0
elif self._grid_count < 4 and close >= self._entry_price + atr_val * 0.8:
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 rubberbands_3_strategy()