The SwingTrader Strategy is a StockSharp port of the MetaTrader 4 expert advisor SwingTrader.mq4. The original EA looks for
Bollinger Band reversals: when price bounces from the outer band and the next bar crosses the middle line, the advisor opens a
position and starts building a martingale-style averaging grid. The translated strategy reproduces the same high-level behaviour
using StockSharp candles, Bollinger Bands from StockSharp.Algo.Indicators, and the framework's order helpers (BuyMarket,
SellMarket). Volume scaling, the width of the grid and the liquidation rules mirror the MT4 code while respecting the exchange
limits provided by the Security metadata.
Trading logic
Subscribe to the configured timeframe (CandleType) and feed a Bollinger Bands indicator with BollingerPeriod length and a
fixed standard deviation multiplier of 2.
Work only with finished candles; the indicator callback ignores partially formed bars to replicate the MT4 IsNewCandle()
guard.
Track whether the previous candle touched the upper or lower band. The boolean pair _upTouch / _downTouch follows the
original toggling logic that keeps only one side active until the opposite band is touched.
When no basket is open:
open a long position if the last completed bar crossed above the middle band after previously touching the lower band;
open a short position if the bar crossed below the middle band after touching the upper band.
The first order volume equals InitialVolume (after exchange rounding) and the initial grid width equals the latest distance
between the upper and lower Bollinger bands.
When a basket exists, watch for adverse movement of one full band width from the very first fill:
for longs, if the candle's low is at least one band width below the anchor price, buy another slice whose size is multiplied
by Multiplier with each new level;
for shorts, if the candle's high is one band width above the anchor price, sell an additional slice using the same
multiplier logic.
Keep aggregating new orders until either the profit or the maximum tolerated loss target is hit.
Money management and exits
The helper CalculateUnrealizedProfit reproduces the MT4 floating PnL calculation by converting price differences to price
steps (Security.PriceStep) and step value (Security.StepPrice).
The invested capital proxy uses the original formula Lots * Price / TickSize * TickValue / 30, where Lots becomes the sum
of grid volumes and the tick parameters are sourced from Security.
Close the entire basket once the floating profit exceeds TakeProfitFactor * invested capital.
Force an emergency liquidation when the floating loss reaches 10 * TakeProfitFactor * invested capital (same ratio as the
MT4 code).
All exits are executed with market orders in the opposite direction; once flat, the grid state is reset and new touches must be
detected before another entry can trigger.
Parameters
Name
Type
Default
Description
TakeProfitFactor
decimal
0.05
Multiplier applied to invested capital to define the profit target.
Multiplier
decimal
1.5
Volume multiplier for every additional averaging order.
BollingerPeriod
int
20
Number of candles used by the Bollinger Bands indicator.
InitialVolume
decimal
1
Base volume of the first trade in a new basket (rounded to venue limits).
CandleType
DataType
15-minute timeframe
Timeframe used for signal generation.
Differences from the original EA
StockSharp works with net positions; the strategy maintains explicit lists of grid entries to emulate MT4's ticket-based order
handling.
Exchange volume filters (Security.MinVolume, Security.VolumeStep, Security.MaxVolume) are applied automatically instead
of manually calling CheckVolumeValue.
Signals are evaluated on closed candles; intrabar triggers from the MT4 version are approximated by using candle highs and lows
for averaging decisions.
Orders are always sent as market instructions, whereas MT4 used OrderSend with explicit bid/ask parameters.
Usage notes
Provide realistic metadata for the traded instrument: PriceStep, StepPrice, MinVolume, VolumeStep and MaxVolume must
be populated for the profit, loss and volume calculations to match the MT4 behaviour.
Because the averaging grid scales geometrically, test the configuration on historical data and consider broker margin
requirements before running it live.
The grid width equals the current Bollinger Band width; changing BollingerPeriod directly affects both entry timing and grid
spacing. Validate the sensitivity during optimisation.
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()