Billy Expert Pullback Buyer
Обзор
Billy Expert — конвертированная из MetaTrader 5 долгосрочная стратегия откатов. Она ожидает последовательность из четырёх свечей на базовом таймфрейме с понижающимися максимумами и ценами открытия, после чего проверяет подтверждение импульса двумя стохастиками на разных старших таймфреймах. Когда оба индикатора показывают, что покупатели вернулись, робот добавляет новую длинную позицию, пока не достигнет установленного лимита.
Конвертация выполнена с использованием высокоуровневого API StockSharp. Все ключевые параметры — объём сделки, количество одновременных входов, стоп-лосс и тейк-профит — доступны в настройках, чтобы поведение максимально соответствовало оригинальному советнику MQL.
Как работает стратегия
- Подписывается на основную свечную серию (по умолчанию 1 минута) и две дополнительные серии для стохастиков (по умолчанию 5 и 6 минут).
- Отслеживает четыре последние завершённые свечи базового таймфрейма. Корректный откат требует строго убывающих максимумов и цен открытия.
- Рассчитывает быстрый и медленный стохастические осцилляторы. Для входа необходимо, чтобы у обоих %K находился выше %D на текущем и предыдущем значениях.
- При выполнении ценового и импульсного фильтров и если открыто меньше
MaxPositionsсделок, отправляется рыночная заявка на покупку объёмомTradeVolume. - Необязательные стоп-лосс и тейк-профит задаются в пунктах и преобразуются в абсолютное расстояние по
PriceStep. Нулевая величина отключает соответствующий уровень защиты. - Выход из позиции происходит только по защитным уровням, как и в оригинальном советнике.
Параметры
TradeVolume– размер каждой сделки (по умолчанию0.01).StopLossPips– дистанция стоп-лосса в пунктах (по умолчанию0, отключено).TakeProfitPips– дистанция тейк-профита в пунктах (по умолчанию32).MaxPositions– максимум одновременно открытых длинных позиций (по умолчанию6).Signal Candle– базовый таймфрейм для анализа цены (по умолчанию1минута).Fast Stochastic TF– таймфрейм быстрого стохастика (по умолчанию5минут).Slow Stochastic TF– таймфрейм медленного стохастика (по умолчанию6минут), должен быть длиннее быстрого.
Фильтры и поведение
- Направление: только покупки.
- Условие входа: четыре подряд свечи с падающими максимумами и открытиями.
- Импульсный фильтр: оба стохастика дают бычий сигнал (%K выше %D на двух последних значениях).
- Управление риском: опциональные стоп-лосс и тейк-профит в пунктах, без трейлинг-механизма.
- Размер позиции: фиксированный
TradeVolumeна вход, ограниченныйMaxPositions. - Рынки: оптимально для форекс-инструментов с дробными пунктами, но работает с любым активом, имеющим корректный
PriceStep.
Рекомендации
- Убедитесь, что
Fast Stochastic TFстрого меньшеSlow Stochastic TF, иначе стратегия остановится при запуске. - Настраивайте
StopLossPipsиTakeProfitPipsпод волатильность конкретного инструмента, так как других выходов нет. - Стратегия не торгует шорт и не сокращает объём — при необходимости подключайте внешние лимиты риска.
- Для тестирования обеспечьте достаточную историю, чтобы оба стохастика успели сформироваться до первого сигнала.
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()