SwingTrader — порт советника MetaTrader 4 SwingTrader.mq4 на платформу StockSharp. Оригинальный эксперт ищет отскоки от
границ полос Боллинджера: если цена касается внешней границы и следующая свеча пересекает среднюю линию, советник открывает
позицию и строит мартингейловую сетку усреднений. Переведённая стратегия использует свечи StockSharp, индикатор Bollinger Bands
из StockSharp.Algo.Indicators и рыночные заявки BuyMarket / SellMarket, сохраняя логику МТ4 и одновременно учитывая биржевые
ограничения из свойств Security.
Логика торговли
Подписаться на таймфрейм, указанный в параметре CandleType, и рассчитать Bollinger Bands длиной BollingerPeriod со стандартным
отклонением, равным 2.
Обрабатывать только завершённые свечи — обработчик игнорирует частично сформированные бары, полностью повторяя проверку
IsNewCandle() из МТ4.
Отслеживать, касалась ли предыдущая свеча верхней или нижней полосы. Булевы флаги _upTouch и _downTouch реализуют оригинальную
схему взаимного исключения: активен только один флаг, пока не произойдёт противоположное касание.
При отсутствии открытой сетки:
открыть длинную позицию, если последняя завершённая свеча пересекла среднюю линию снизу вверх после касания нижней полосы;
открыть короткую позицию, если свеча пересекла среднюю линию сверху вниз после касания верхней полосы.
Объём первой сделки равен InitialVolume (после округления по биржевым шагам), а ширина сетки задаётся текущей дистанцией между
верхней и нижней полосами.
При наличии сетки следить за движением против позиции относительно цены первой сделки:
для лонгов: если минимум свечи находится не выше чем на ширину полосы ниже опорной цены, купить дополнительный объём,
умноженный на Multiplier;
для шортов: если максимум свечи находится на ширину полосы выше опорной цены, продать дополнительный объём с тем же множителем.
Продолжать усреднение, пока не сработает целевой профит или лимит убытка.
Управление капиталом и выходы
Метод CalculateUnrealizedProfit переводит ценовые изменения в тики с помощью Security.PriceStep и Security.StepPrice,
воспроизводя расчёт плавающей прибыли из МТ4.
Оценка вложенного капитала повторяет формулу Lots * Price / TickSize * TickValue / 30: под Lots понимается суммарный объём
сетки, а параметры тика берутся из Security.
Полное закрытие сетки происходит, когда плавающая прибыль превышает TakeProfitFactor * вложенный капитал.
Аварийное закрытие происходит при плавающем убытке 10 * TakeProfitFactor * вложенный капитал, что соответствует допуску
исходного советника.
Закрытия выполняются обратными рыночными заявками; после выхода состояние сетки сбрасывается и стратегия ждёт нового касания полос.
Параметры
Название
Тип
Значение по умолчанию
Описание
TakeProfitFactor
decimal
0.05
Коэффициент прибыли, умножаемый на вложенный капитал для определения цели.
Multiplier
decimal
1.5
Множитель объёма для каждой новой сделки сетки.
BollingerPeriod
int
20
Количество свечей в расчёте полос Боллинджера.
InitialVolume
decimal
1
Объём первой сделки в новой сетке (с учётом биржевых ограничений).
CandleType
DataType
таймфрейм 15 минут
Тип свечей, используемый для сигналов.
Отличия от оригинального советника
StockSharp работает в модели неттинга; стратегия хранит явный список сделок сетки, чтобы имитировать учёт сделок по тикетам в МТ4.
Биржевые ограничения по объёму (Security.MinVolume, Security.VolumeStep, Security.MaxVolume) применяются автоматически вместо
ручного вызова CheckVolumeValue.
Сигналы рассчитываются на закрытии свечи; внутренняя логика MT4 по тиковым событиям аппроксимируется использованием максимумов и
минимумов свечи при добавлении усреднений.
Заявки всегда отправляются как рыночные, тогда как в MT4 использовался OrderSend с явным указанием Bid/Ask.
Практические рекомендации
Заполните в коннекторе корректные сведения об инструменте (PriceStep, StepPrice, MinVolume, VolumeStep, MaxVolume),
чтобы расчёт прибыли, убытка и объёмов совпадал с MT4.
Мартингейловые сетки несут повышенный риск. Перед запуском на реальном счёте протестируйте стратегию на исторических данных и
оцените требования по марже.
Ширина сетки равна текущей ширине полос Боллинджера; изменение BollingerPeriod одновременно влияет на частоту входов и расстояние
между уровнями. Проведите оптимизацию, чтобы понять чувствительность.
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Simplified from "SwingTrader" MetaTrader expert.
/// Uses Bollinger Band touches to detect swing direction, then enters on middle-band cross.
/// </summary>
public class SwingTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerWidth;
private BollingerBands _bollinger;
private bool _upTouch;
private bool _downTouch;
private decimal? _prevClose;
private decimal? _prevMiddle;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
public decimal BollingerWidth
{
get => _bollingerWidth.Value;
set => _bollingerWidth.Value = value;
}
public SwingTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signals", "General");
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("BB Period", "Bollinger Bands period", "Indicators");
_bollingerWidth = Param(nameof(BollingerWidth), 2m)
.SetGreaterThanZero()
.SetDisplay("BB Width", "Bollinger Bands deviation", "Indicators");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bollinger = new BollingerBands { Length = BollingerPeriod, Width = BollingerWidth };
_upTouch = false;
_downTouch = false;
_prevClose = null;
_prevMiddle = null;
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_bollinger, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _bollinger);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bbValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!bbValue.IsFinal)
return;
if (bbValue is not BollingerBandsValue bbVal)
return;
if (bbVal.UpBand is not decimal upper || bbVal.LowBand is not decimal lower || bbVal.MovingAverage is not decimal middle)
return;
if (!_bollinger.IsFormed)
{
_prevClose = candle.ClosePrice;
_prevMiddle = middle;
return;
}
var close = candle.ClosePrice;
// Track Bollinger touches
if (candle.HighPrice > upper)
{
_upTouch = true;
_downTouch = false;
}
if (candle.LowPrice < lower)
{
_downTouch = true;
_upTouch = false;
}
if (_prevClose is null || _prevMiddle is null)
{
_prevClose = close;
_prevMiddle = middle;
return;
}
var volume = Volume;
if (volume <= 0)
volume = 1;
// Buy: had a lower band touch, now price crosses above middle
var buySignal = _downTouch && _prevClose.Value < _prevMiddle.Value && close > middle;
// Sell: had an upper band touch, now price crosses below middle
var sellSignal = _upTouch && _prevClose.Value > _prevMiddle.Value && close < middle;
if (buySignal)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
if (Position <= 0)
BuyMarket(volume);
_downTouch = false;
}
else if (sellSignal)
{
if (Position > 0)
SellMarket(Position);
if (Position >= 0)
SellMarket(volume);
_upTouch = false;
}
_prevClose = close;
_prevMiddle = middle;
}
/// <inheritdoc />
protected override void OnReseted()
{
_bollinger = null;
_upTouch = false;
_downTouch = false;
_prevClose = null;
_prevMiddle = null;
base.OnReseted();
}
}
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
class swing_trader_strategy(Strategy):
"""Bollinger Band touch swing: buy on lower touch + middle cross up, sell on upper touch + middle cross down."""
def __init__(self):
super(swing_trader_strategy, self).__init__()
self._bb_period = self.Param("BollingerPeriod", 20).SetGreaterThanZero().SetDisplay("BB Period", "Bollinger Bands period", "Indicators")
self._bb_width = self.Param("BollingerWidth", 2.0).SetGreaterThanZero().SetDisplay("BB Width", "Bollinger Bands deviation", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe for signals", "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(swing_trader_strategy, self).OnReseted()
self._up_touch = False
self._down_touch = False
self._prev_close = 0
self._prev_middle = 0
def OnStarted2(self, time):
super(swing_trader_strategy, self).OnStarted2(time)
self._up_touch = False
self._down_touch = False
self._prev_close = 0
self._prev_middle = 0
self._bb = BollingerBands()
self._bb.Length = self._bb_period.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 = float(bb_val.UpBand)
lower = float(bb_val.LowBand)
middle = float(bb_val.MovingAverage)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
# Track Bollinger touches
if high > upper:
self._up_touch = True
self._down_touch = False
if low < lower:
self._down_touch = True
self._up_touch = False
if self._prev_close == 0 or self._prev_middle == 0:
self._prev_close = close
self._prev_middle = middle
return
# Buy: had lower band touch, now price crosses above middle
buy_signal = self._down_touch and self._prev_close < self._prev_middle and close > middle
# Sell: had upper band touch, now price crosses below middle
sell_signal = self._up_touch and self._prev_close > self._prev_middle and close < middle
if buy_signal:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
self._down_touch = False
elif sell_signal:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._up_touch = False
self._prev_close = close
self._prev_middle = middle
def CreateClone(self):
return swing_trader_strategy()