Стратегия повторяет логику советника MetaTrader "Tunnel gen4" с использованием высокоуровневого API StockSharp. Она поддерживает нейтральный хедж, открывая стартовый набор из покупок и продаж, удваивает объём в направлении прорыва после прохождения ценой заданного числа пунктов и закрывает весь портфель, когда такая же дистанция пройдена за пределы второй опорной точки.
Логика торговли
Начальный хедж: Когда активных позиций нет, стратегия одновременно отправляет рыночные заявки Buy и Sell объёмом StartVolume. Первая сделка определяет опорную цену, относительно которой измеряются все шаги.
Контроль шага: Параметр StepPips переводится в ценовое смещение с учётом минимального шага цены инструмента и поправки для трёх- и пятизнаковых валютных котировок. Обновления лучшей цены bid/ask из Level 1 сравниваются с этим смещением.
Усиление позиции: Если лучшая цена bid выросла минимум на один шаг от первой сделки, отправляется рыночная заявка Sell объёмом в два раза больше базового. Если лучшая цена ask упала на один шаг, то отправляется Buy такого же размера. Первая сделка по этой заявке фиксирует вторую опору.
Завершение цикла: После появления второй опоры любое дальнейшее смещение цены на величину шага в любую сторону запускает полное закрытие всех позиций. После закрытия обеих сторон состояние сбрасывается, и стратегия готова открыть новый хедж.
Проверка объёмов: При запуске стратегия удостоверяется, что стартовый и удвоенный объёмы соответствуют ограничениям инструмента по минимуму, максимуму и шагу, чтобы каждый ордер мог быть исполнен коннектором.
Условия входа
Усиление покупки
Существует хотя бы одна позиция из начального хеджа.
Вторая опора ещё не создана.
Текущая лучшая цена ask меньше либо равна первая_сделка - StepPips_в_цене.
Усиление продажи
Существует хотя бы одна позиция из начального хеджа.
Вторая опора ещё не создана.
Текущая лучшая цена bid больше либо равна первая_сделка + StepPips_в_цене.
Управление выходом
Закрытие корзины: После появления второй опоры, если лучшая цена bid превышает вторая_опора + StepOffset или лучшая цена ask опускается ниже вторая_опора - StepOffset, стратегия отправляет рыночные заявки для закрытия суммарного длинного и короткого объёмов. Закрывающие заявки отслеживаются до полного исполнения, чтобы сброс состоялся только после подтверждённых сделок.
Сброс состояния: Когда обе стороны закрыты и активных заявок на выход нет, внутренние опоры очищаются и стратегия ожидает возможности открыть новый хедж.
Данные и индикаторы
Подписка Level 1 предоставляет лучшие цены bid и ask, по которым оцениваются шаги.
Дополнительные индикаторы не используются; логика полностью базируется на потоке котировок.
Конвертация шага цены повторяет поправку MetaTrader из point в pip, поэтому трёх- и пятизнаковые валютные пары ведут себя так же, как в исходном советнике.
Параметры
Параметр
Описание
StartVolume
Объём заявок Buy и Sell, формирующих первоначальный хедж.
StepPips
Дистанция в пунктах, которая запускает усиление и последующее закрытие корзины.
Особенности реализации
StockSharp ведёт сводную позицию по инструменту. Стратегия хранит внутренние счётчики длинного и короткого объёмов, чтобы имитировать отдельные тики MetaTrader, и при закрытии корзины отправляет рыночные заявки на суммарные объёмы.
Для корректной работы необходимы актуальные спреды, поэтому и в тестах, и в реальной торговле нужно предоставлять поток Level 1. При отсутствии bid/ask торговый цикл приостанавливается.
Убедитесь, что торговый счёт поддерживает одновременные покупки и продажи одного инструмента, так как алгоритм предполагает сосуществование обеих сторон до наступления условия выхода.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Tunnel strategy that uses Bollinger Bands to define a price channel.
/// Buys when price crosses above the lower band (reversal from oversold),
/// sells when price crosses below the upper band (reversal from overbought).
/// </summary>
public class TunnelGen4Strategy : Strategy
{
private readonly StrategyParam<int> _bbLength;
private readonly StrategyParam<decimal> _bbWidth;
private readonly StrategyParam<decimal> _stepPips;
private BollingerBands _bb;
private decimal _prevClose;
private decimal _prevUpper;
private decimal _prevLower;
private decimal _entryPrice;
/// <summary>
/// Bollinger Bands period length.
/// </summary>
public int BbLength
{
get => _bbLength.Value;
set => _bbLength.Value = value;
}
/// <summary>
/// Bollinger Bands width (standard deviations).
/// </summary>
public decimal BbWidth
{
get => _bbWidth.Value;
set => _bbWidth.Value = value;
}
/// <summary>
/// Step distance expressed in pips for profit target.
/// </summary>
public decimal StepPips
{
get => _stepPips.Value;
set => _stepPips.Value = value;
}
/// <summary>
/// Initialize strategy parameters.
/// </summary>
public TunnelGen4Strategy()
{
_bbLength = Param(nameof(BbLength), 20)
.SetGreaterThanZero()
.SetDisplay("BB Length", "Bollinger Bands period", "Indicator");
_bbWidth = Param(nameof(BbWidth), 2.0m)
.SetGreaterThanZero()
.SetDisplay("BB Width", "Bollinger Bands width", "Indicator");
_stepPips = Param(nameof(StepPips), 50m)
.SetGreaterThanZero()
.SetDisplay("Step (pips)", "Distance between tunnel anchors", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bb = null;
_prevClose = 0;
_prevUpper = 0;
_prevLower = 0;
_entryPrice = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bb = new BollingerBands
{
Length = BbLength,
Width = BbWidth
};
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.BindEx(_bb, OnProcess);
subscription.Start();
}
private void OnProcess(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished)
return;
var bb = (BollingerBandsValue)value;
if (bb.UpBand is not decimal upper ||
bb.LowBand is not decimal lower)
return;
if (!_bb.IsFormed)
{
_prevClose = candle.ClosePrice;
_prevUpper = upper;
_prevLower = lower;
return;
}
var close = candle.ClosePrice;
// Buy signal: price crosses above lower band from below
if (_prevClose < _prevLower && close >= lower && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
}
// Sell signal: price crosses below upper band from above
else if (_prevClose > _prevUpper && close <= upper && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
}
// Exit on profit target if in position
if (Position > 0 && _entryPrice > 0)
{
var pipValue = Security?.PriceStep ?? 1m;
var target = _entryPrice + StepPips * pipValue;
if (close >= target)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0 && _entryPrice > 0)
{
var pipValue = Security?.PriceStep ?? 1m;
var target = _entryPrice - StepPips * pipValue;
if (close <= target)
{
BuyMarket();
_entryPrice = 0;
}
}
_prevClose = close;
_prevUpper = upper;
_prevLower = lower;
}
}
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 BollingerBands
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class tunnel_gen4_strategy(Strategy):
"""Bollinger Band tunnel: buy on cross above lower band, sell on cross below upper."""
def __init__(self):
super(tunnel_gen4_strategy, self).__init__()
self._bb_length = self.Param("BbLength", 20).SetGreaterThanZero().SetDisplay("BB Length", "BB period", "Indicator")
self._bb_width = self.Param("BbWidth", 2.0).SetGreaterThanZero().SetDisplay("BB Width", "BB std devs", "Indicator")
self._step_pips = self.Param("StepPips", 50.0).SetGreaterThanZero().SetDisplay("Step Pips", "Profit target distance", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).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(tunnel_gen4_strategy, self).OnReseted()
self._prev_close = 0
self._prev_upper = 0
self._prev_lower = 0
self._entry_price = 0
def OnStarted2(self, time):
super(tunnel_gen4_strategy, self).OnStarted2(time)
self._prev_close = 0
self._prev_upper = 0
self._prev_lower = 0
self._entry_price = 0
self._bb = BollingerBands()
self._bb.Length = self._bb_length.Value
self._bb.Width = self._bb_width.Value
sub = self.SubscribeCandles(self.CandleType)
sub.BindEx(self._bb, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, self._bb)
self.DrawOwnTrades(area)
def OnProcess(self, candle, bb_val):
if candle.State != CandleStates.Finished:
return
if not self._bb.IsFormed:
return
upper = None
lower = None
for inner in bb_val.InnerValues:
name = str(inner.Key.Name) if hasattr(inner.Key, 'Name') else str(inner.Key)
if "Up" in name or "up" in name:
upper = float(inner.Value) if not inner.Value.IsEmpty else None
elif "Low" in name or "low" in name or "Down" in name or "down" in name:
lower = float(inner.Value) if not inner.Value.IsEmpty else None
if upper is None or lower is None:
return
close = float(candle.ClosePrice)
if self._prev_close == 0:
self._prev_close = close
self._prev_upper = upper
self._prev_lower = lower
return
# Buy: price crosses above lower band from below
if self._prev_close < self._prev_lower and close >= lower and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
# Sell: price crosses below upper band from above
elif self._prev_close > self._prev_upper and close <= upper and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
# Exit on profit target
step = self._step_pips.Value
if self.Position > 0 and self._entry_price > 0:
if close >= self._entry_price + step:
self.SellMarket()
self._entry_price = 0
elif self.Position < 0 and self._entry_price > 0:
if close <= self._entry_price - step:
self.BuyMarket()
self._entry_price = 0
self._prev_close = close
self._prev_upper = upper
self._prev_lower = lower
def CreateClone(self):
return tunnel_gen4_strategy()