Ver en GitHub

Billy Expert Comprador de Retrocesos

Descripción general

Billy Expert es una estrategia de retrocesos solo larga convertida del Asesor Experto MetaTrader 5 «Billy expert». Espera una secuencia de máximos decrecientes y abre en el marco temporal base, luego verifica confirmaciones alcistas de dos osciladores Estocásticos calculados en diferentes marcos temporales superiores. Cuando ambos osciladores coinciden en que hay impulso alcista presente, el sistema agrega una nueva posición larga, hasta un límite configurable.

La conversión sigue las pautas de la API de alto nivel de StockSharp. El volumen de la operación, el máximo de entradas simultáneas, los stops protectores y los take profits se controlan a través de los parámetros de la estrategia para que el comportamiento coincida con la lógica MQL original.

Cómo funciona

  1. Suscribirse a la serie de velas primaria (por defecto 1 minuto) y dos marcos temporales superiores para los osciladores Estocásticos (por defecto 5 y 6 minutos).
  2. Rastrear las últimas cuatro velas completadas en el marco temporal base. Un retroceso válido requiere máximos y aperturas estrictamente decrecientes a lo largo de esas cuatro barras.
  3. Evaluar los osciladores Estocásticos rápido y lento. La estrategia exige que para cada oscilador tanto el último como el anterior valor de %K se mantenga por encima de %D, señalando que el impulso ya se ha invertido al alza en ambos marcos temporales.
  4. Si el retroceso y los filtros de impulso confirman y el número de operaciones largas abiertas está por debajo de MaxPositions, enviar una orden de compra a mercado con tamaño TradeVolume.
  5. Los niveles opcionales de stop-loss y take-profit, expresados en pips, se convierten en distancias de precio absolutas usando el PriceStep del instrumento. Si cualquier distancia se establece en cero, la orden protectora correspondiente se omite.
  6. Las posiciones se cierran solo a través de esos niveles protectores, imitando el comportamiento del asesor experto original.

Parámetros

  • TradeVolume – tamaño de la orden para cada entrada (predeterminado 0.01).
  • StopLossPips – distancia de stop en pips (predeterminado 0, deshabilitado).
  • TakeProfitPips – objetivo de ganancia en pips (predeterminado 32).
  • MaxPositions – máximo de operaciones largas simultáneas (predeterminado 6).
  • Signal Candle – marco temporal base usado para patrones de precio (predeterminado 1 minuto).
  • Fast Stochastic TF – marco temporal para el oscilador rápido (predeterminado 5 minutos).
  • Slow Stochastic TF – marco temporal para el oscilador lento (predeterminado 6 minutos). Debe ser más largo que el marco temporal rápido.

Filtros y comportamiento

  • Dirección: Solo largos.
  • Disparador de entrada: Retroceso de cuatro barras con aperturas y máximos decrecientes.
  • Filtro de impulso: Doble oscilador Estocástico con %K por encima de %D en las lecturas actual y anterior.
  • Gestión de riesgos: Stop-loss y take-profit opcionales basados en pips. Sin lógica de trailing.
  • Dimensionamiento de posición: TradeVolume fijo por entrada, limitado por MaxPositions.
  • Mercados: Diseñado para pares de forex cotizados con pips fraccionarios, pero funciona con cualquier instrumento que proporcione un PriceStep válido.

Notas de uso

  • Asegúrese de que Fast Stochastic TF sea estrictamente más corto que Slow Stochastic TF, de lo contrario la estrategia se detiene al iniciar.
  • Dado que las salidas dependen únicamente de las órdenes protectoras, ajuste StopLossPips y TakeProfitPips a la volatilidad del instrumento.
  • La estrategia ignora las señales bajistas y no reduce parcialmente; use controles de riesgo a nivel de cartera para protección adicional.
  • Para backtesting, proporcione suficientes velas de calentamiento para que ambos osciladores Estocásticos puedan formarse antes de la primera operación.
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;
	}
}