Billy Expert Pullback-Käufer
Überblick
Billy Expert ist eine Long-only-Pullback-Strategie, die aus dem MetaTrader 5-Experten «Billy expert» konvertiert wurde. Sie wartet auf eine Sequenz fallender Hochs und öffnet auf dem Basis-Zeitrahmen, dann prüft sie bullische Bestätigungen von zwei Stochastik-Oszillatoren, die auf verschiedenen höheren Zeitrahmen berechnet werden. Wenn beide Oszillatoren übereinstimmen, dass Aufwärtsmomentum vorhanden ist, fügt das System eine neue Long-Position hinzu, bis zu einem konfigurierbaren Limit.
Die Konvertierung folgt den StockSharp-High-Level-API-Richtlinien. Handelsvolumen, maximale gleichzeitige Einstiege, Schutz-Stops und Take-Profits werden durch Strategieparameter gesteuert, sodass das Verhalten der ursprünglichen MQL-Logik entspricht.
Funktionsweise
- Abonnieren der primären Kerzenserie (Standard 1 Minute) und zwei höherer Zeitrahmen für die Stochastik-Oszillatoren (Standard 5 und 6 Minuten).
- Verfolgen der letzten vier abgeschlossenen Kerzen auf dem Basis-Zeitrahmen. Ein gültiger Pullback erfordert streng fallende Hochs und Eröffnungen über diese vier Bars.
- Bewertung der schnellen und langsamen Stochastik-Oszillatoren. Die Strategie verlangt, dass für jeden Oszillator sowohl der aktuelle als auch der vorherige Wert von %K über %D bleibt, was signalisiert, dass das Momentum bereits auf beiden Zeitrahmen aufwärts gedreht hat.
- Wenn der Pullback und die Momentum-Filter bestätigen und die Anzahl der offenen Long-Trades unter
MaxPositions liegt, wird eine Kaufmarktorder mit der Größe TradeVolume gesendet.
- Optionale Stop-Loss- und Take-Profit-Levels, in Pips ausgedrückt, werden über den
PriceStep des Instruments in absolute Preisabstände umgerechnet. Wenn eine Distanz auf null gesetzt wird, wird die entsprechende Schutzorder weggelassen.
- Positionen werden nur über diese Schutzlevels geschlossen, was das ursprüngliche Expertenverhalten nachahmt.
Parameter
TradeVolume – Ordergröße für jeden Einstieg (Standard 0.01).
StopLossPips – Stop-Distanz in Pips (Standard 0, deaktiviert).
TakeProfitPips – Gewinnziel in Pips (Standard 32).
MaxPositions – maximale gleichzeitige Long-Trades (Standard 6).
Signal Candle – Basis-Zeitrahmen für Preismuster (Standard 1 Minute).
Fast Stochastic TF – Zeitrahmen für den schnellen Oszillator (Standard 5 Minuten).
Slow Stochastic TF – Zeitrahmen für den langsamen Oszillator (Standard 6 Minuten). Muss länger als der schnelle Zeitrahmen sein.
Filter und Verhalten
- Richtung: Nur Long.
- Einstiegsauslöser: Vier-Bar-Pullback mit fallenden Eröffnungen und Hochs.
- Momentum-Filter: Doppelter Stochastik-Oszillator mit %K über %D bei aktuellen und vorherigen Werten.
- Risikomanagement: Optionaler pip-basierter Stop-Loss und Take-Profit. Keine Trailing-Logik.
- Positionsgröße: Festes
TradeVolume pro Einstieg, begrenzt durch MaxPositions.
- Märkte: Entwickelt für Forex-Paare mit fraktionalen Pips, funktioniert aber mit jedem Instrument mit einem gültigen
PriceStep.
Verwendungshinweise
- Stellen Sie sicher, dass
Fast Stochastic TF streng kürzer als Slow Stochastic TF ist, sonst stoppt die Strategie beim Start.
- Da Ausstiege ausschließlich auf Schutzorders beruhen, passen Sie
StopLossPips und TakeProfitPips an die Volatilität des Instruments an.
- Die Strategie ignoriert bärische Signale und skaliert nicht aus; verwenden Sie Portfolio-Level-Risikokontrollen für zusätzlichen Schutz.
- Für Backtesting stellen Sie genügend Aufwärmkerzen bereit, damit beide Stochastik-Oszillatoren vor dem ersten Trade gebildet werden können.
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()