Billy Expert Pullback Buyer
Overview
Billy Expert is a long-only pullback strategy converted from the MetaTrader 5 Expert Advisor "Billy expert". It waits for a sequence of falling highs and opens on the base timeframe, then checks bullish confirmations from two Stochastic oscillators calculated on different higher timeframes. When both oscillators agree that upside momentum is present, the system adds a new long position, up to a configurable limit.
The conversion follows the StockSharp high-level API guidelines. Trade volume, maximum simultaneous entries, protective stops and take profits are controlled through strategy parameters so the behaviour matches the original MQL logic.
How It Works
- Subscribe to the primary candle series (default 1 minute) and two higher timeframes for the Stochastic oscillators (defaults 5 and 6 minutes).
- Track the latest four completed candles on the base timeframe. A valid pullback requires strictly decreasing highs and opens across those four bars.
- Evaluate the fast and slow Stochastic oscillators. The strategy demands that for each oscillator both the latest and the previous values of %K stay above %D, signalling that momentum has already flipped to the upside on both timeframes.
- If the pullback and momentum filters confirm and the number of open long trades is below
MaxPositions, send a market buy order with sizeTradeVolume. - Optional stop-loss and take-profit levels, expressed in pips, are converted to absolute price distances using the instrument's
PriceStep. If either distance is set to zero the corresponding protective order is omitted. - Positions are closed only via those protective levels, mimicking the original expert advisor behaviour.
Parameters
TradeVolume– order size for each entry (default0.01).StopLossPips– stop distance in pips (default0, disabled).TakeProfitPips– profit target in pips (default32).MaxPositions– maximum simultaneous long trades (default6).Signal Candle– base timeframe used for price patterns (default1minute).Fast Stochastic TF– timeframe for the fast oscillator (default5minutes).Slow Stochastic TF– timeframe for the slow oscillator (default6minutes). Must be longer than the fast timeframe.
Filters and Behaviour
- Direction: Long only.
- Entry trigger: Four-bar pullback with both opens and highs decreasing.
- Momentum filter: Dual Stochastic oscillators with %K above %D on the current and previous readings.
- Risk management: Optional pip-based stop-loss and take-profit. No trailing logic.
- Position sizing: Fixed
TradeVolumeper entry, capped byMaxPositions. - Markets: Designed for forex pairs quoted with fractional pips, but works with any instrument providing a valid
PriceStep.
Usage Notes
- Ensure
Fast Stochastic TFis strictly shorter thanSlow Stochastic TF, otherwise the strategy stops on launch. - Because exits rely solely on protective orders, tune
StopLossPipsandTakeProfitPipsto the instrument's volatility. - The strategy ignores bearish signals and does not scale out; use portfolio-level risk controls for additional protection.
- For backtesting, provide enough warm-up candles so both Stochastic oscillators can form before the first trade.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Billy Expert strategy converted from MetaTrader 5 Expert Advisor.
/// Focuses on buying during pullbacks confirmed by dual timeframe Stochastic signals.
/// </summary>
public class BillyExpertStrategy : Strategy
{
private readonly StrategyParam<decimal> _volumeTolerance;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<TimeSpan> _stochasticTimeFrame1;
private readonly StrategyParam<TimeSpan> _stochasticTimeFrame2;
private StochasticOscillator _fastStochastic = null!;
private StochasticOscillator _slowStochastic = null!;
private decimal _open1;
private decimal _open2;
private decimal _open3;
private decimal _open4;
private decimal _high1;
private decimal _high2;
private decimal _high3;
private decimal _high4;
private int _historyCount;
private decimal _fastMainCurrent;
private decimal _fastMainPrevious;
private decimal _fastSignalCurrent;
private decimal _fastSignalPrevious;
private bool _fastHasCurrent;
private bool _fastHasPrevious;
private decimal _slowMainCurrent;
private decimal _slowMainPrevious;
private decimal _slowSignalCurrent;
private decimal _slowSignalPrevious;
private bool _slowHasCurrent;
private bool _slowHasPrevious;
private decimal _pipSize;
/// <summary>
/// Volume tolerance used to compare accumulated volumes.
/// </summary>
public decimal VolumeTolerance
{
get => _volumeTolerance.Value;
set => _volumeTolerance.Value = value;
}
/// <summary>
/// Trade volume used for each entry.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Maximum number of simultaneous long entries.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Primary candle type that drives the price pattern checks.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Timeframe for the faster Stochastic oscillator.
/// </summary>
public TimeSpan StochasticTimeFrame1
{
get => _stochasticTimeFrame1.Value;
set => _stochasticTimeFrame1.Value = value;
}
/// <summary>
/// Timeframe for the slower Stochastic oscillator.
/// </summary>
public TimeSpan StochasticTimeFrame2
{
get => _stochasticTimeFrame2.Value;
set => _stochasticTimeFrame2.Value = value;
}
/// <summary>
/// Initializes parameters for the strategy.
/// </summary>
public BillyExpertStrategy()
{
_volumeTolerance = Param(nameof(VolumeTolerance), 0.0000001m)
.SetGreaterThanZero()
.SetDisplay("Volume Tolerance", "Tolerance for comparing volume sums", "Risk");
_tradeVolume = Param(nameof(TradeVolume), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order size for each entry", "General");
_stopLossPips = Param(nameof(StopLossPips), 0)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 320)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");
_maxPositions = Param(nameof(MaxPositions), 6)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum number of open trades", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Signal Candle", "Primary timeframe used for price filters", "General");
_stochasticTimeFrame1 = Param(nameof(StochasticTimeFrame1), TimeSpan.FromHours(1))
.SetDisplay("Fast Stochastic TF", "Timeframe for the fast Stochastic", "Indicators");
_stochasticTimeFrame2 = Param(nameof(StochasticTimeFrame2), TimeSpan.FromHours(4))
.SetDisplay("Slow Stochastic TF", "Timeframe for the slow Stochastic", "Indicators");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return
[
(Security, CandleType),
(Security, StochasticTimeFrame1.TimeFrame()),
(Security, StochasticTimeFrame2.TimeFrame())
];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_open1 = _open2 = _open3 = _open4 = 0m;
_high1 = _high2 = _high3 = _high4 = 0m;
_historyCount = 0;
_fastMainCurrent = _fastMainPrevious = 0m;
_fastSignalCurrent = _fastSignalPrevious = 0m;
_fastHasCurrent = false;
_fastHasPrevious = false;
_slowMainCurrent = _slowMainPrevious = 0m;
_slowSignalCurrent = _slowSignalPrevious = 0m;
_slowHasCurrent = false;
_slowHasPrevious = false;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (StochasticTimeFrame1 >= StochasticTimeFrame2)
{
LogError("Fast stochastic timeframe must be shorter than the slow timeframe.");
Stop();
return;
}
Volume = TradeVolume;
_fastStochastic = new StochasticOscillator { K = { Length = 14 }, D = { Length = 3 } };
_slowStochastic = new StochasticOscillator { K = { Length = 14 }, D = { Length = 3 } };
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription
.Bind(ProcessSignalCandle)
.Start();
var fastSubscription = SubscribeCandles(StochasticTimeFrame1.TimeFrame());
fastSubscription
.BindEx(_fastStochastic, ProcessFastStochastic)
.Start();
var slowSubscription = SubscribeCandles(StochasticTimeFrame2.TimeFrame());
slowSubscription
.BindEx(_slowStochastic, ProcessSlowStochastic)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candleSubscription);
DrawOwnTrades(area);
}
_pipSize = CalculatePipSize();
var takeProfit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null;
var stopLoss = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null;
if (takeProfit != null || stopLoss != null)
{
StartProtection(takeProfit, stopLoss);
}
}
private void ProcessFastStochastic(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished)
return;
if (!value.IsFinal)
return;
if (!_fastStochastic.IsFormed)
return;
if (_fastHasCurrent)
{
_fastMainPrevious = _fastMainCurrent;
_fastSignalPrevious = _fastSignalCurrent;
_fastHasPrevious = true;
}
var typed = (StochasticOscillatorValue)value;
_fastMainCurrent = typed.K ?? 0m;
_fastSignalCurrent = typed.D ?? 0m;
_fastHasCurrent = true;
}
private void ProcessSlowStochastic(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished)
return;
if (!value.IsFinal)
return;
if (!_slowStochastic.IsFormed)
return;
if (_slowHasCurrent)
{
_slowMainPrevious = _slowMainCurrent;
_slowSignalPrevious = _slowSignalCurrent;
_slowHasPrevious = true;
}
var typed = (StochasticOscillatorValue)value;
_slowMainCurrent = typed.K ?? 0m;
_slowSignalCurrent = typed.D ?? 0m;
_slowHasCurrent = true;
}
private void ProcessSignalCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_historyCount >= 4 && _fastHasPrevious && _slowHasPrevious)
{
var decreasingHighs = _high1 < _high2 && _high2 < _high3 && _high3 < _high4;
var decreasingOpens = _open1 < _open2 && _open2 < _open3 && _open3 < _open4;
var fastBullish = _fastMainPrevious > _fastSignalPrevious && _fastMainCurrent > _fastSignalCurrent;
var slowBullish = _slowMainPrevious > _slowSignalPrevious && _slowMainCurrent > _slowSignalCurrent;
var maxLongVolume = MaxPositions * TradeVolume;
var currentLongVolume = Math.Max(Position, 0m);
var projectedVolume = currentLongVolume + TradeVolume;
if (decreasingHighs && decreasingOpens && fastBullish && slowBullish && projectedVolume <= maxLongVolume + VolumeTolerance)
{
BuyMarket();
}
}
_high4 = _high3;
_high3 = _high2;
_high2 = _high1;
_high1 = candle.HighPrice;
_open4 = _open3;
_open3 = _open2;
_open2 = _open1;
_open1 = candle.OpenPrice;
if (_historyCount < 4)
{
_historyCount++;
}
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return 1m;
var decimals = GetDecimalPlaces(priceStep);
var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
return priceStep * adjust;
}
private static int GetDecimalPlaces(decimal value)
{
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import StochasticOscillator
class billy_expert_strategy(Strategy):
"""Billy Expert: dual timeframe Stochastic with decreasing highs/opens pattern for long entries."""
def __init__(self):
super(billy_expert_strategy, self).__init__()
self._volume_tolerance = self.Param("VolumeTolerance", 0.0000001) \
.SetGreaterThanZero() \
.SetDisplay("Volume Tolerance", "Tolerance for comparing volume sums", "Risk")
self._trade_volume = self.Param("TradeVolume", 0.01) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Order size for each entry", "General")
self._stop_loss_pips = self.Param("StopLossPips", 0) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 320) \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
self._max_positions = self.Param("MaxPositions", 6) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Maximum number of open trades", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Signal Candle", "Primary timeframe used for price filters", "General")
self._stochastic_time_frame1 = self.Param("StochasticTimeFrame1", TimeSpan.FromHours(1)) \
.SetDisplay("Fast Stochastic TF", "Timeframe for the fast Stochastic", "Indicators")
self._stochastic_time_frame2 = self.Param("StochasticTimeFrame2", TimeSpan.FromHours(4)) \
.SetDisplay("Slow Stochastic TF", "Timeframe for the slow Stochastic", "Indicators")
self._open1 = 0.0
self._open2 = 0.0
self._open3 = 0.0
self._open4 = 0.0
self._high1 = 0.0
self._high2 = 0.0
self._high3 = 0.0
self._high4 = 0.0
self._history_count = 0
self._fast_main_current = 0.0
self._fast_main_previous = 0.0
self._fast_signal_current = 0.0
self._fast_signal_previous = 0.0
self._fast_has_current = False
self._fast_has_previous = False
self._slow_main_current = 0.0
self._slow_main_previous = 0.0
self._slow_signal_current = 0.0
self._slow_signal_previous = 0.0
self._slow_has_current = False
self._slow_has_previous = False
self._pip_size = 0.0
@property
def VolumeTolerance(self):
return float(self._volume_tolerance.Value)
@property
def TradeVolume(self):
return float(self._trade_volume.Value)
@property
def StopLossPips(self):
return int(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return int(self._take_profit_pips.Value)
@property
def MaxPositions(self):
return int(self._max_positions.Value)
@property
def CandleType(self):
return self._candle_type.Value
@property
def StochasticTimeFrame1(self):
return self._stochastic_time_frame1.Value
@property
def StochasticTimeFrame2(self):
return self._stochastic_time_frame2.Value
def _calc_pip_size(self):
sec = self.Security
if sec is None or sec.PriceStep is None:
return 1.0
step = float(sec.PriceStep)
if step <= 0:
return 1.0
decimals = 0
if sec.Decimals is not None:
decimals = int(sec.Decimals)
else:
v = abs(step)
while v != int(v) and decimals < 10:
v *= 10
decimals += 1
return step * 10.0 if (decimals == 3 or decimals == 5) else step
def OnStarted2(self, time):
super(billy_expert_strategy, self).OnStarted2(time)
self._open1 = 0.0
self._open2 = 0.0
self._open3 = 0.0
self._open4 = 0.0
self._high1 = 0.0
self._high2 = 0.0
self._high3 = 0.0
self._high4 = 0.0
self._history_count = 0
self._fast_main_current = 0.0
self._fast_main_previous = 0.0
self._fast_signal_current = 0.0
self._fast_signal_previous = 0.0
self._fast_has_current = False
self._fast_has_previous = False
self._slow_main_current = 0.0
self._slow_main_previous = 0.0
self._slow_signal_current = 0.0
self._slow_signal_previous = 0.0
self._slow_has_current = False
self._slow_has_previous = False
self._fast_stochastic = StochasticOscillator()
self._fast_stochastic.K.Length = 14
self._fast_stochastic.D.Length = 3
self._slow_stochastic = StochasticOscillator()
self._slow_stochastic.K.Length = 14
self._slow_stochastic.D.Length = 3
candle_subscription = self.SubscribeCandles(self.CandleType)
candle_subscription.Bind(self.process_signal_candle).Start()
fast_subscription = self.SubscribeCandles(DataType.TimeFrame(self.StochasticTimeFrame1))
fast_subscription.BindEx(self._fast_stochastic, self.process_fast_stochastic).Start()
slow_subscription = self.SubscribeCandles(DataType.TimeFrame(self.StochasticTimeFrame2))
slow_subscription.BindEx(self._slow_stochastic, self.process_slow_stochastic).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candle_subscription)
self.DrawOwnTrades(area)
self._pip_size = self._calc_pip_size()
tp = Unit(self.TakeProfitPips * self._pip_size, UnitTypes.Absolute) if self.TakeProfitPips > 0 else Unit()
sl = Unit(self.StopLossPips * self._pip_size, UnitTypes.Absolute) if self.StopLossPips > 0 else Unit()
self.StartProtection(tp, sl)
def process_fast_stochastic(self, candle, value):
if candle.State != CandleStates.Finished:
return
if not self._fast_stochastic.IsFormed:
return
if self._fast_has_current:
self._fast_main_previous = self._fast_main_current
self._fast_signal_previous = self._fast_signal_current
self._fast_has_previous = True
self._fast_main_current = float(value.K) if value.K is not None else 0.0
self._fast_signal_current = float(value.D) if value.D is not None else 0.0
self._fast_has_current = True
def process_slow_stochastic(self, candle, value):
if candle.State != CandleStates.Finished:
return
if not self._slow_stochastic.IsFormed:
return
if self._slow_has_current:
self._slow_main_previous = self._slow_main_current
self._slow_signal_previous = self._slow_signal_current
self._slow_has_previous = True
self._slow_main_current = float(value.K) if value.K is not None else 0.0
self._slow_signal_current = float(value.D) if value.D is not None else 0.0
self._slow_has_current = True
def process_signal_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._history_count >= 4 and self._fast_has_previous and self._slow_has_previous:
decreasing_highs = (self._high1 < self._high2 and
self._high2 < self._high3 and
self._high3 < self._high4)
decreasing_opens = (self._open1 < self._open2 and
self._open2 < self._open3 and
self._open3 < self._open4)
fast_bullish = (self._fast_main_previous > self._fast_signal_previous and
self._fast_main_current > self._fast_signal_current)
slow_bullish = (self._slow_main_previous > self._slow_signal_previous and
self._slow_main_current > self._slow_signal_current)
max_long_volume = self.MaxPositions * self.TradeVolume
current_long_volume = max(self.Position, 0.0)
projected_volume = current_long_volume + self.TradeVolume
if (decreasing_highs and decreasing_opens and fast_bullish and slow_bullish and
projected_volume <= max_long_volume + self.VolumeTolerance):
self.BuyMarket()
self._high4 = self._high3
self._high3 = self._high2
self._high2 = self._high1
self._high1 = float(candle.HighPrice)
self._open4 = self._open3
self._open3 = self._open2
self._open2 = self._open1
self._open1 = float(candle.OpenPrice)
if self._history_count < 4:
self._history_count += 1
def OnReseted(self):
super(billy_expert_strategy, self).OnReseted()
self._open1 = 0.0
self._open2 = 0.0
self._open3 = 0.0
self._open4 = 0.0
self._high1 = 0.0
self._high2 = 0.0
self._high3 = 0.0
self._high4 = 0.0
self._history_count = 0
self._fast_main_current = 0.0
self._fast_main_previous = 0.0
self._fast_signal_current = 0.0
self._fast_signal_previous = 0.0
self._fast_has_current = False
self._fast_has_previous = False
self._slow_main_current = 0.0
self._slow_main_previous = 0.0
self._slow_signal_current = 0.0
self._slow_signal_previous = 0.0
self._slow_has_current = False
self._slow_has_previous = False
self._pip_size = 0.0
def CreateClone(self):
return billy_expert_strategy()