Billy Expert Pullback Buyer
概述
Billy Expert 是从 MetaTrader 5 专家顾问“Billy expert”移植而来的只做多回调策略。策略在基础周期上等待四根开盘价和最高价都持续下行的 K 线序列,然后在两个更高周期上的随机指标中寻找多头确认。当两个随机指标都显示动能转强时,系统在不超过最大持仓数的前提下加仓做多。
该实现遵循 StockSharp 的高级 API 要求,所有关键设置(下单量、最大仓位数量、止损和止盈)均通过策略参数暴露,以复刻原始 MQL 逻辑。
工作流程
- 订阅基础 K 线序列(默认 1 分钟)以及两个用于随机指标的更高周期(默认 5 分钟和 6 分钟)。
- 跟踪基础周期上最近四根已完成的 K 线。只有当这四根 K 线的最高价和开盘价都严格递减时才认为出现回调。
- 计算快慢两个随机指标,要求在当前值和上一根值上,%K 都位于 %D 之上,表明动能已经在两个周期上同步转向多头。
- 如果价格形态和动能过滤条件同时满足,且当前多头仓位数量少于
MaxPositions,则按TradeVolume下达市价买单。 - 可选的止损与止盈以点(pip)为单位设置,并根据品种的
PriceStep转换为绝对价差。若参数为 0,则不设置对应的保护单。 - 仓位仅通过这些保护水平离场,以保持与原始 EA 一致的管理方式。
参数
TradeVolume:每次下单的合约数量,默认0.01。StopLossPips:止损距离(点),默认0(不启用)。TakeProfitPips:止盈距离(点),默认32。MaxPositions:最多同时持有的多头仓位数,默认6。Signal Candle:用于形态判断的基础周期,默认1分钟。Fast Stochastic TF:快随机指标的周期,默认5分钟。Slow Stochastic TF:慢随机指标的周期,默认6分钟,必须长于快周期。
过滤条件与特性
- 方向:仅做多。
- 入场条件:四根连续 K 线的最高价和开盘价严格下降。
- 动能过滤:快、慢随机指标的 %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()