The Bobnaley strategy reproduces the MetaTrader 5 expert advisor "bobnaley" using the StockSharp high level API. It combines a simple moving average trend filter with the stochastic oscillator to search for reversal opportunities. The original expert evaluated tick prices; the port uses completed candles and keeps the order management rules intact.
How It Works
Indicators
A simple moving average with the configured period filters the prevailing direction.
A stochastic oscillator (main and signal lines) identifies oversold and overbought situations. Only the main line is needed for signals; the signal line is calculated for completeness.
Entry Conditions
The strategy waits until the current candle is finished and all indicators are formed.
Long entries require the moving average to be strictly decreasing during the last three samples while price closes above the latest average. At the same time the stochastic main line must be below the oversold level and its previous value must be higher than the one before it, mirroring the original EA requirement stochVal[1] > stochVal[2].
Short entries are the mirror image: the moving average must be rising in the last three samples while the price closes below it, and the stochastic main line must be above the overbought level while its previous value is lower than the earlier one.
New trades are opened only when no position is currently active, replicating the PositionSelect guard from MetaTrader.
Risk Management
When a position is opened the strategy relies on StockSharp's protection service to place a take-profit and a stop-loss in absolute price units. These distances match the MetaTrader inputs (0.007 and 0.0035 by default).
Before every decision the portfolio value is compared against the Minimum Balance parameter, mirroring the free-margin filter (ACCOUNT_FREEMARGIN > 5000) of the original code. If the account value is known and below the threshold, the entry is skipped.
Volume Handling
Orders use a fixed Base Volume parameter. This reproduces the lot setting that the MetaTrader script used after applying its own rounding routine.
Parameters
Category
Name
Description
Default
General
Candle Type
Candle data type used for indicator calculations.
5-minute time frame
Trading
Base Volume
Fixed order volume applied to every new position.
5
Indicators
MA Period
Length of the simple moving average.
76
Indicators
Stochastic Period
Lookback for the stochastic main line.
5
Indicators
Stochastic %K
Smoothing length for the %K line.
3
Indicators
Stochastic %D
Smoothing length for the %D line.
3
Indicators
Stochastic Oversold
Threshold that defines oversold territory for the main line.
30
Indicators
Stochastic Overbought
Threshold that defines overbought territory for the main line.
70
Risk Management
Take Profit
Distance between the entry price and the take-profit in price units.
0.007
Risk Management
Stop Loss
Distance between the entry price and the stop-loss in price units.
0.0035
Risk Management
Minimum Balance
Minimal portfolio value required before a new order can be sent.
5000
Notes
The original expert used Bid/Ask quotes; in StockSharp the candle close is used as the execution price proxy.
No trailing exits are implemented—the trade is closed only by the protective orders.
Stochastic calculations follow the default MetaTrader settings (5/3/3) but can be optimized via the exposed parameters.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Bobnaley strategy: EMA trend with ATR volatility filter.
/// </summary>
public class BobnaleyStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public BobnaleyStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_emaPeriod = Param(nameof(EmaPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA period", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR period", "Indicators");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
decimal? prevClose = null;
decimal? prevEma = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, atr, (candle, emaVal, atrVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
if (prevClose.HasValue && prevEma.HasValue)
{
var crossUp = prevClose.Value <= prevEma.Value && close > emaVal;
var crossDown = prevClose.Value >= prevEma.Value && close < emaVal;
if (crossUp && Position <= 0)
BuyMarket();
else if (crossDown && Position >= 0)
SellMarket();
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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 ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class bobnaley_strategy(Strategy):
def __init__(self):
super(bobnaley_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._ema_period = self.Param("EmaPeriod", 14) \
.SetDisplay("EMA Period", "EMA period", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period", "Indicators")
self._prev_close = None
self._prev_ema = None
@property
def candle_type(self):
return self._candle_type.Value
@property
def ema_period(self):
return self._ema_period.Value
@property
def atr_period(self):
return self._atr_period.Value
def OnReseted(self):
super(bobnaley_strategy, self).OnReseted()
self._prev_close = None
self._prev_ema = None
def OnStarted2(self, time):
super(bobnaley_strategy, self).OnStarted2(time)
self._prev_close = None
self._prev_ema = None
ema = ExponentialMovingAverage()
ema.Length = self.ema_period
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._prev_close is not None and self._prev_ema is not None:
cross_up = self._prev_close <= self._prev_ema and close > float(ema_val)
cross_down = self._prev_close >= self._prev_ema and close < float(ema_val)
if cross_up and self.Position <= 0:
self.BuyMarket()
elif cross_down and self.Position >= 0:
self.SellMarket()
self._prev_close = close
self._prev_ema = float(ema_val)
def CreateClone(self):
return bobnaley_strategy()