Стратегия представляет собой порт эксперта MetaTrader 4 RUBBERBANDS_3 на платформу StockSharp. Алгоритм отслеживает два экстремума цены, наращивает позицию при расширении диапазона на заданное число пунктов и полностью закрывает серию ордеров при обратном движении. После фиксации профита стратегия может перевернуться и начать набор позиций в противоположном направлении. Одновременно ведётся контроль суммарной прибыли и убытка в пределах торговой сессии.
Важно: в StockSharp позиции неттингуются. В оригинальном советнике могли одновременно существовать покупки и продажи. В портированной версии серия ордеров в одном направлении закрывается перед открытием противоположных сделок. Логика «растягивания резинки» и закрытия на откате при этом сохраняется.
Логика работы
При старте сохраняются текущие максимальное и минимальное значения цены закрытия (либо используются значения InitialMax и InitialMin).
Если цена поднимается выше максимума на величину PipStep, выставляется рыночная покупка объёмом OrderVolume, а максимум обновляется.
Если цена опускается ниже минимума на PipStep, выставляется рыночная продажа объёмом OrderVolume, минимум обновляется.
При обратном движении на BackStep пунктов вся серия сделок текущего направления закрывается. Как только позиции обнулены, стратегия готовится открыть сделки противоположного направления.
Реализован контроль сессионных результатов: при достижении SessionTakeProfit × OrderVolume все позиции закрываются. Если во время переворота плавающий убыток превышает SessionStopLoss × OrderVolume, серия также принудительно закрывается.
Флаг QuiesceNow переводит стратегию в «тихий» режим после закрытия всех позиций. StopNow полностью приостанавливает логику, CloseNow инициирует немедленное закрытие всех ордеров.
Обработка сигналов выполняется по завершённым свечам типа CandleType. По умолчанию используется минутный таймфрейм, что соответствует первоначальному советнику, срабатывавшему в начале каждой минуты.
Параметры
Параметр
Описание
Значение по умолчанию
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
Тип свечей, по которым ведётся расчёт.
TimeFrame(1m)
Управление сессией
Агрегация прибыли. После каждого полного закрытия серии фиксированный результат добавляется к накопленной прибыли _realizedProfit. Плавающий результат рассчитывается по средневзвешенным ценам открытых позиций.
Сессионный тейк-профит. При достижении целевого значения вся серия закрывается, экстремумы сбрасываются.
Сессионный стоп-лосс. Во время переворота отслеживается текущий убыток. Если он превышает порог SessionStopLoss, позиции закрываются и торговая сессия начинается заново.
Рекомендации по использованию
Для корректного пересчёта пунктов необходимо заполнить Security.PriceStep. При отсутствии данных используется резервное значение 0.0001.
Из-за неттинга обратное направление открывается только после полного закрытия предыдущей серии. Учтите это при сравнении отчётов с оригинальной версией.
Флаг DoNow влияет только на первое открытие. Дальнейшие сделки выполняются по правилам расширения диапазона.
QuiesceNow полезен, если нужно оставить стратегию запущенной, но не позволять ей начинать новые циклы после закрытия позиций.
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()